Merge "Fix error prone warnings" am: 8f15a06f5a am: 953a3b3276 am: 2f916f658c am: c7ab3f5e3f

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1863056

Change-Id: I1f202571d03db61cef3e1d4c75412c7d0a79ebaf
diff --git a/Android.bp b/Android.bp
index d976b91..e0679f2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -112,6 +112,7 @@
         ":framework_native_aidl",
         ":gatekeeper_aidl",
         ":gsiservice_aidl",
+        ":guiconstants_aidl",
         ":idmap2_aidl",
         ":idmap2_core_aidl",
         ":incidentcompanion_aidl",
@@ -347,6 +348,8 @@
         "modules-utils-preconditions",
         "modules-utils-os",
         "framework-permission-aidl-java",
+        "spatializer-aidl-java",
+        "audiopolicy-types-aidl-java",
     ],
 }
 
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
index a2dc1c2..452bb0a 100644
--- a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
+++ b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
@@ -29,6 +29,7 @@
 import android.view.InputChannel;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
@@ -82,7 +83,7 @@
 
     private static class TestWindow extends BaseIWindow {
         final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
-        final InsetsState mRequestedVisibility = new InsetsState();
+        final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
         final InsetsState mOutInsetsState = new InsetsState();
         final InsetsSourceControl[] mOutControls = new InsetsSourceControl[0];
 
@@ -102,7 +103,7 @@
 
                 long startTime = SystemClock.elapsedRealtimeNanos();
                 session.addToDisplay(this, mLayoutParams, View.VISIBLE,
-                        Display.DEFAULT_DISPLAY, mRequestedVisibility, inputChannel,
+                        Display.DEFAULT_DISPLAY, mRequestedVisibilities, inputChannel,
                         mOutInsetsState, mOutControls);
                 final long elapsedTimeNsOfAdd = SystemClock.elapsedRealtimeNanos() - startTime;
                 state.addExtraResult("add", elapsedTimeNsOfAdd);
diff --git a/core/api/current.txt b/core/api/current.txt
index 0eba9d7..5f5490f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -99,6 +99,7 @@
     field public static final String INTERACT_ACROSS_PROFILES = "android.permission.INTERACT_ACROSS_PROFILES";
     field public static final String INTERNET = "android.permission.INTERNET";
     field public static final String KILL_BACKGROUND_PROCESSES = "android.permission.KILL_BACKGROUND_PROCESSES";
+    field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK";
     field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
     field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
     field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS";
@@ -1297,6 +1298,7 @@
     field public static final int shortcutLongLabel = 16844074; // 0x101052a
     field public static final int shortcutShortLabel = 16844073; // 0x1010529
     field public static final int shouldDisableView = 16843246; // 0x10101ee
+    field public static final int shouldUseDefaultUnfoldTransition;
     field public static final int showAsAction = 16843481; // 0x10102d9
     field public static final int showDefault = 16843258; // 0x10101fa
     field public static final int showDividers = 16843561; // 0x1010329
@@ -2012,6 +2014,9 @@
   public static final class R.id {
     ctor public R.id();
     field public static final int accessibilityActionContextClick = 16908348; // 0x102003c
+    field public static final int accessibilityActionDragCancel;
+    field public static final int accessibilityActionDragDrop;
+    field public static final int accessibilityActionDragStart;
     field public static final int accessibilityActionHideTooltip = 16908357; // 0x1020045
     field public static final int accessibilityActionImeEnter = 16908372; // 0x1020054
     field public static final int accessibilityActionMoveWindow = 16908354; // 0x1020042
@@ -6707,6 +6712,7 @@
   }
 
   public class TaskInfo {
+    method public boolean isVisible();
     field @Nullable public android.content.ComponentName baseActivity;
     field @NonNull public android.content.Intent baseIntent;
     field public boolean isRunning;
@@ -6922,6 +6928,7 @@
     method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager);
     method public CharSequence loadLabel(android.content.pm.PackageManager);
     method public android.graphics.drawable.Drawable loadThumbnail(android.content.pm.PackageManager);
+    method public boolean shouldUseDefaultUnfoldTransition();
     method public boolean supportsMultipleDisplays();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.WallpaperInfo> CREATOR;
@@ -17960,6 +17967,7 @@
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> EDGE_AVAILABLE_EDGE_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> FLASH_INFO_AVAILABLE;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES;
+    field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.DeviceStateSensorOrientationMap> INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> INFO_SUPPORTED_HARDWARE_LEVEL;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.String> INFO_VERSION;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Size[]> JPEG_AVAILABLE_THUMBNAIL_SIZES;
@@ -18654,6 +18662,12 @@
     method public android.util.Rational getElement(int, int);
   }
 
+  public final class DeviceStateSensorOrientationMap {
+    method public int getSensorOrientation(long);
+    field public static final long FOLDED = 4L; // 0x4L
+    field public static final long NORMAL = 0L; // 0x0L
+  }
+
   public final class ExtensionSessionConfiguration {
     ctor public ExtensionSessionConfiguration(int, @NonNull java.util.List<android.hardware.camera2.params.OutputConfiguration>, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.StateCallback);
     method @NonNull public java.util.concurrent.Executor getExecutor();
@@ -20195,8 +20209,10 @@
     method public int getAllowedCapturePolicy();
     method public int getContentType();
     method public int getFlags();
+    method public int getSpatializationBehavior();
     method public int getUsage();
     method public int getVolumeControlStream();
+    method public boolean isContentSpatialized();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int ALLOW_CAPTURE_BY_ALL = 1; // 0x1
     field public static final int ALLOW_CAPTURE_BY_NONE = 3; // 0x3
@@ -20210,6 +20226,8 @@
     field public static final int FLAG_AUDIBILITY_ENFORCED = 1; // 0x1
     field public static final int FLAG_HW_AV_SYNC = 16; // 0x10
     field @Deprecated public static final int FLAG_LOW_LATENCY = 256; // 0x100
+    field public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0; // 0x0
+    field public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1; // 0x1
     field public static final int USAGE_ALARM = 4; // 0x4
     field public static final int USAGE_ASSISTANCE_ACCESSIBILITY = 11; // 0xb
     field public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = 12; // 0xc
@@ -20236,7 +20254,9 @@
     method public android.media.AudioAttributes.Builder setContentType(int);
     method public android.media.AudioAttributes.Builder setFlags(int);
     method @NonNull public android.media.AudioAttributes.Builder setHapticChannelsMuted(boolean);
+    method @NonNull public android.media.AudioAttributes.Builder setIsContentSpatialized(boolean);
     method public android.media.AudioAttributes.Builder setLegacyStreamType(int);
+    method @NonNull public android.media.AudioAttributes.Builder setSpatializationBehavior(int);
     method public android.media.AudioAttributes.Builder setUsage(int);
   }
 
@@ -20353,24 +20373,45 @@
     field public static final int CHANNEL_IN_Y_AXIS = 4096; // 0x1000
     field public static final int CHANNEL_IN_Z_AXIS = 8192; // 0x2000
     field public static final int CHANNEL_OUT_5POINT1 = 252; // 0xfc
+    field public static final int CHANNEL_OUT_5POINT1POINT2 = 3145980; // 0x3000fc
+    field public static final int CHANNEL_OUT_5POINT1POINT4 = 737532; // 0xb40fc
     field @Deprecated public static final int CHANNEL_OUT_7POINT1 = 1020; // 0x3fc
+    field public static final int CHANNEL_OUT_7POINT1POINT2 = 3152124; // 0x3018fc
+    field public static final int CHANNEL_OUT_7POINT1POINT4 = 743676; // 0xb58fc
     field public static final int CHANNEL_OUT_7POINT1_SURROUND = 6396; // 0x18fc
+    field public static final int CHANNEL_OUT_9POINT1POINT4 = 202070268; // 0xc0b58fc
+    field public static final int CHANNEL_OUT_9POINT1POINT6 = 205215996; // 0xc3b58fc
     field public static final int CHANNEL_OUT_BACK_CENTER = 1024; // 0x400
     field public static final int CHANNEL_OUT_BACK_LEFT = 64; // 0x40
     field public static final int CHANNEL_OUT_BACK_RIGHT = 128; // 0x80
+    field public static final int CHANNEL_OUT_BOTTOM_FRONT_CENTER = 8388608; // 0x800000
+    field public static final int CHANNEL_OUT_BOTTOM_FRONT_LEFT = 4194304; // 0x400000
+    field public static final int CHANNEL_OUT_BOTTOM_FRONT_RIGHT = 16777216; // 0x1000000
     field public static final int CHANNEL_OUT_DEFAULT = 1; // 0x1
     field public static final int CHANNEL_OUT_FRONT_CENTER = 16; // 0x10
     field public static final int CHANNEL_OUT_FRONT_LEFT = 4; // 0x4
     field public static final int CHANNEL_OUT_FRONT_LEFT_OF_CENTER = 256; // 0x100
     field public static final int CHANNEL_OUT_FRONT_RIGHT = 8; // 0x8
     field public static final int CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 512; // 0x200
+    field public static final int CHANNEL_OUT_FRONT_WIDE_LEFT = 67108864; // 0x4000000
+    field public static final int CHANNEL_OUT_FRONT_WIDE_RIGHT = 134217728; // 0x8000000
     field public static final int CHANNEL_OUT_LOW_FREQUENCY = 32; // 0x20
+    field public static final int CHANNEL_OUT_LOW_FREQUENCY_2 = 33554432; // 0x2000000
     field public static final int CHANNEL_OUT_MONO = 4; // 0x4
     field public static final int CHANNEL_OUT_QUAD = 204; // 0xcc
     field public static final int CHANNEL_OUT_SIDE_LEFT = 2048; // 0x800
     field public static final int CHANNEL_OUT_SIDE_RIGHT = 4096; // 0x1000
     field public static final int CHANNEL_OUT_STEREO = 12; // 0xc
     field public static final int CHANNEL_OUT_SURROUND = 1052; // 0x41c
+    field public static final int CHANNEL_OUT_TOP_BACK_CENTER = 262144; // 0x40000
+    field public static final int CHANNEL_OUT_TOP_BACK_LEFT = 131072; // 0x20000
+    field public static final int CHANNEL_OUT_TOP_BACK_RIGHT = 524288; // 0x80000
+    field public static final int CHANNEL_OUT_TOP_CENTER = 8192; // 0x2000
+    field public static final int CHANNEL_OUT_TOP_FRONT_CENTER = 32768; // 0x8000
+    field public static final int CHANNEL_OUT_TOP_FRONT_LEFT = 16384; // 0x4000
+    field public static final int CHANNEL_OUT_TOP_FRONT_RIGHT = 65536; // 0x10000
+    field public static final int CHANNEL_OUT_TOP_SIDE_LEFT = 1048576; // 0x100000
+    field public static final int CHANNEL_OUT_TOP_SIDE_RIGHT = 2097152; // 0x200000
     field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioFormat> CREATOR;
     field public static final int ENCODING_AAC_ELD = 15; // 0xf
     field public static final int ENCODING_AAC_HE_V1 = 11; // 0xb
@@ -20440,6 +20481,7 @@
     method public String getProperty(String);
     method public int getRingerMode();
     method @Deprecated public int getRouting(int);
+    method @NonNull public android.media.Spatializer getSpatializer();
     method public int getStreamMaxVolume(int);
     method public int getStreamMinVolume(int);
     method public int getStreamVolume(int);
@@ -22557,6 +22599,7 @@
     field public static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
     field public static final String KEY_MAX_HEIGHT = "max-height";
     field public static final String KEY_MAX_INPUT_SIZE = "max-input-size";
+    field public static final String KEY_MAX_OUTPUT_CHANNEL_COUNT = "max-output-channel_count";
     field public static final String KEY_MAX_PTS_GAP_TO_ENCODER = "max-pts-gap-to-encoder";
     field public static final String KEY_MAX_WIDTH = "max-width";
     field public static final String KEY_MIME = "mime";
@@ -23836,6 +23879,23 @@
     method public void onLoadComplete(android.media.SoundPool, int, int);
   }
 
+  public class Spatializer {
+    method public void addOnSpatializerStateChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnSpatializerStateChangedListener);
+    method public boolean canBeSpatialized(@NonNull android.media.AudioAttributes, @NonNull android.media.AudioFormat);
+    method public int getImmersiveAudioLevel();
+    method public boolean isAvailable();
+    method public boolean isEnabled();
+    method public void removeOnSpatializerStateChangedListener(@NonNull android.media.Spatializer.OnSpatializerStateChangedListener);
+    field public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; // 0x1
+    field public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0; // 0x0
+    field public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1; // 0xffffffff
+  }
+
+  public static interface Spatializer.OnSpatializerStateChangedListener {
+    method public void onSpatializerAvailableChanged(@NonNull android.media.Spatializer, boolean);
+    method public void onSpatializerEnabledChanged(@NonNull android.media.Spatializer, boolean);
+  }
+
   public final class SubtitleData {
     ctor public SubtitleData(int, long, long, @NonNull byte[]);
     method @NonNull public byte[] getData();
@@ -31092,8 +31152,8 @@
     ctor public Environment();
     method public static java.io.File getDataDirectory();
     method public static java.io.File getDownloadCacheDirectory();
-    method @Deprecated public static java.io.File getExternalStorageDirectory();
-    method @Deprecated public static java.io.File getExternalStoragePublicDirectory(String);
+    method public static java.io.File getExternalStorageDirectory();
+    method public static java.io.File getExternalStoragePublicDirectory(String);
     method public static String getExternalStorageState();
     method public static String getExternalStorageState(java.io.File);
     method @NonNull public static java.io.File getRootDirectory();
@@ -35210,6 +35270,7 @@
     field public static final String ACTION_SEARCH_SETTINGS = "android.search.action.SEARCH_SETTINGS";
     field public static final String ACTION_SECURITY_SETTINGS = "android.settings.SECURITY_SETTINGS";
     field public static final String ACTION_SETTINGS = "android.settings.SETTINGS";
+    field public static final String ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY = "android.settings.SETTINGS_EMBED_DEEP_LINK_ACTIVITY";
     field public static final String ACTION_SHOW_REGULATORY_INFO = "android.settings.SHOW_REGULATORY_INFO";
     field public static final String ACTION_SHOW_WORK_POLICY_INFO = "android.settings.SHOW_WORK_POLICY_INFO";
     field public static final String ACTION_SOUND_SETTINGS = "android.settings.SOUND_SETTINGS";
@@ -35250,6 +35311,8 @@
     field public static final String EXTRA_EASY_CONNECT_ERROR_CODE = "android.provider.extra.EASY_CONNECT_ERROR_CODE";
     field public static final String EXTRA_INPUT_METHOD_ID = "input_method_id";
     field public static final String EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME = "android.provider.extra.NOTIFICATION_LISTENER_COMPONENT_NAME";
+    field public static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY = "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY";
+    field public static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI = "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI";
     field public static final String EXTRA_SUB_ID = "android.provider.extra.SUB_ID";
     field public static final String EXTRA_WIFI_NETWORK_LIST = "android.provider.extra.WIFI_NETWORK_LIST";
     field public static final String EXTRA_WIFI_NETWORK_RESULT_LIST = "android.provider.extra.WIFI_NETWORK_RESULT_LIST";
@@ -38975,6 +39038,13 @@
 
 package android.service.voice {
 
+  public final class VisibleActivityInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.service.voice.VoiceInteractionSession.ActivityId getActivityId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.voice.VisibleActivityInfo> CREATOR;
+  }
+
   public class VoiceInteractionService extends android.app.Service {
     ctor public VoiceInteractionService();
     method public int getDisabledShowContext();
@@ -39036,6 +39106,7 @@
     method public void onTaskStarted(android.content.Intent, int);
     method public void onTrimMemory(int);
     method public final void performDirectAction(@NonNull android.app.DirectAction, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.os.Bundle>);
+    method public final void registerVisibleActivityCallback(@NonNull java.util.concurrent.Executor, @NonNull android.service.voice.VoiceInteractionSession.VisibleActivityCallback);
     method public final void requestDirectActions(@NonNull android.service.voice.VoiceInteractionSession.ActivityId, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.DirectAction>>);
     method public void setContentView(android.view.View);
     method public void setDisabledShowContext(int);
@@ -39045,6 +39116,7 @@
     method public void show(android.os.Bundle, int);
     method public void startAssistantActivity(android.content.Intent);
     method public void startVoiceActivity(android.content.Intent);
+    method public final void unregisterVisibleActivityCallback(@NonNull android.service.voice.VoiceInteractionSession.VisibleActivityCallback);
     field public static final int SHOW_SOURCE_ACTIVITY = 16; // 0x10
     field public static final int SHOW_SOURCE_APPLICATION = 8; // 0x8
     field public static final int SHOW_SOURCE_ASSIST_GESTURE = 4; // 0x4
@@ -39118,6 +39190,11 @@
     method public boolean isActive();
   }
 
+  public static interface VoiceInteractionSession.VisibleActivityCallback {
+    method public default void onInvisible(@NonNull android.service.voice.VoiceInteractionSession.ActivityId);
+    method public default void onVisible(@NonNull android.service.voice.VisibleActivityInfo);
+  }
+
   public abstract class VoiceInteractionSessionService extends android.app.Service {
     ctor public VoiceInteractionSessionService();
     method public android.os.IBinder onBind(android.content.Intent);
@@ -46868,8 +46945,15 @@
   }
 
   @UiThread public interface AttachedSurfaceControl {
+    method public default void addOnBufferTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnBufferTransformHintChangedListener);
     method public boolean applyTransactionOnDraw(@NonNull android.view.SurfaceControl.Transaction);
     method @Nullable public android.view.SurfaceControl.Transaction buildReparentTransaction(@NonNull android.view.SurfaceControl);
+    method public default int getBufferTransformHint();
+    method public default void removeOnBufferTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnBufferTransformHintChangedListener);
+  }
+
+  @UiThread public static interface AttachedSurfaceControl.OnBufferTransformHintChangedListener {
+    method public void onBufferTransformHintChanged(int);
   }
 
   public final class Choreographer {
@@ -48359,6 +48443,12 @@
     method public void readFromParcel(android.os.Parcel);
     method public void release();
     method public void writeToParcel(android.os.Parcel, int);
+    field public static final int BUFFER_TRANSFORM_IDENTITY = 0; // 0x0
+    field public static final int BUFFER_TRANSFORM_MIRROR_HORIZONTAL = 1; // 0x1
+    field public static final int BUFFER_TRANSFORM_MIRROR_VERTICAL = 2; // 0x2
+    field public static final int BUFFER_TRANSFORM_ROTATE_180 = 3; // 0x3
+    field public static final int BUFFER_TRANSFORM_ROTATE_270 = 7; // 0x7
+    field public static final int BUFFER_TRANSFORM_ROTATE_90 = 4; // 0x4
     field @NonNull public static final android.os.Parcelable.Creator<android.view.SurfaceControl> CREATOR;
   }
 
@@ -49221,6 +49311,7 @@
     field public static final int AUTOFILL_TYPE_NONE = 0; // 0x0
     field public static final int AUTOFILL_TYPE_TEXT = 1; // 0x1
     field public static final int AUTOFILL_TYPE_TOGGLE = 2; // 0x2
+    field public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1024; // 0x400
     field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100
     field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40
     field public static final int DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION = 128; // 0x80
@@ -50647,6 +50738,9 @@
     method public void setPackageName(CharSequence);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 256; // 0x100
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
     field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
     field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
     field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -50945,6 +51039,9 @@
     field public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_COPY;
     field public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_CUT;
     field public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_DISMISS;
+    field @NonNull public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_DRAG_CANCEL;
+    field @NonNull public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_DRAG_DROP;
+    field @NonNull public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_DRAG_START;
     field public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_EXPAND;
     field public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_FOCUS;
     field public static final android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction ACTION_HIDE_TOOLTIP;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 6f825ae..b2a90e0 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -26,6 +26,7 @@
     field public static final String ADJUST_RUNTIME_PERMISSIONS_POLICY = "android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY";
     field public static final String ALLOCATE_AGGRESSIVE = "android.permission.ALLOCATE_AGGRESSIVE";
     field public static final String ALLOW_ANY_CODEC_FOR_PLAYBACK = "android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK";
+    field public static final String ALLOW_PLACE_IN_MULTI_PANE_SETTINGS = "android.permission.ALLOW_PLACE_IN_MULTI_PANE_SETTINGS";
     field public static final String AMBIENT_WALLPAPER = "android.permission.AMBIENT_WALLPAPER";
     field public static final String APPROVE_INCIDENT_REPORTS = "android.permission.APPROVE_INCIDENT_REPORTS";
     field public static final String ASSOCIATE_COMPANION_DEVICES = "android.permission.ASSOCIATE_COMPANION_DEVICES";
@@ -3302,11 +3303,13 @@
   public final class DisplayManager {
     method @RequiresPermission(android.Manifest.permission.ACCESS_AMBIENT_LIGHT_STATS) public java.util.List<android.hardware.display.AmbientBrightnessDayStats> getAmbientBrightnessStats();
     method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public android.hardware.display.BrightnessConfiguration getBrightnessConfiguration();
+    method @Nullable @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public android.hardware.display.BrightnessConfiguration getBrightnessConfigurationForDisplay(@NonNull String);
     method @RequiresPermission(android.Manifest.permission.BRIGHTNESS_SLIDER_USAGE) public java.util.List<android.hardware.display.BrightnessChangeEvent> getBrightnessEvents();
     method @Nullable @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public android.hardware.display.BrightnessConfiguration getDefaultBrightnessConfiguration();
     method public android.util.Pair<float[],float[]> getMinimumBrightnessCurve();
     method public android.graphics.Point getStableDisplaySize();
     method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfiguration(android.hardware.display.BrightnessConfiguration);
+    method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfigurationForDisplay(@NonNull android.hardware.display.BrightnessConfiguration, @NonNull String);
     method @Deprecated @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_SATURATION) public void setSaturationLevel(float);
   }
 
@@ -5410,6 +5413,46 @@
     field public static final android.media.RouteDiscoveryPreference EMPTY;
   }
 
+  public class Spatializer {
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addOnHeadTrackingModeChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadTrackingModeChangedListener);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void clearOnHeadToSoundstagePoseUpdatedListener();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void clearOnSpatializerOutputChangedListener();
+    method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List<android.media.AudioDeviceAttributes> getCompatibleAudioDevices();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getDesiredHeadTrackingMode();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void getEffectParameter(int, @NonNull byte[]);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getHeadTrackingMode();
+    method @IntRange(from=0) @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getOutput();
+    method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List<java.lang.Integer> getSupportedHeadTrackingModes();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void recenterHeadTracker();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void removeCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void removeOnHeadTrackingModeChangedListener(@NonNull android.media.Spatializer.OnHeadTrackingModeChangedListener);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setDesiredHeadTrackingMode(int);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setEffectParameter(int, @NonNull byte[]);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setEnabled(boolean);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setGlobalTransform(@NonNull float[]);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnHeadToSoundstagePoseUpdatedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadToSoundstagePoseUpdatedListener);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnSpatializerOutputChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnSpatializerOutputChangedListener);
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_DISABLED = -1; // 0xffffffff
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_OTHER = 0; // 0x0
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2; // 0x2
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1; // 0x1
+    field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2; // 0xfffffffe
+  }
+
+  public static interface Spatializer.OnHeadToSoundstagePoseUpdatedListener {
+    method public void onHeadToSoundstagePoseUpdated(@NonNull android.media.Spatializer, @NonNull float[]);
+  }
+
+  public static interface Spatializer.OnHeadTrackingModeChangedListener {
+    method public void onDesiredHeadTrackingModeChanged(@NonNull android.media.Spatializer, int);
+    method public void onHeadTrackingModeChanged(@NonNull android.media.Spatializer, int);
+  }
+
+  public static interface Spatializer.OnSpatializerOutputChangedListener {
+    method public void onSpatializerOutputChanged(@NonNull android.media.Spatializer, @IntRange(from=0) int);
+  }
+
 }
 
 package android.media.audiofx {
@@ -14467,6 +14510,7 @@
     method public int getFlags();
     method @Nullable public android.view.contentcapture.ContentCaptureSessionId getParentSessionId();
     method public int getTaskId();
+    method @Nullable public android.os.IBinder getWindowToken();
     field public static final int FLAG_DISABLED_BY_APP = 1; // 0x1
     field public static final int FLAG_DISABLED_BY_FLAG_SECURE = 2; // 0x2
     field public static final int FLAG_RECONNECTED = 4; // 0x4
@@ -14474,6 +14518,7 @@
 
   public final class ContentCaptureEvent implements android.os.Parcelable {
     method public int describeContents();
+    method @Nullable public android.graphics.Rect getBounds();
     method @Nullable public android.view.contentcapture.ContentCaptureContext getContentCaptureContext();
     method public long getEventTime();
     method @Nullable public android.view.autofill.AutofillId getId();
@@ -14493,6 +14538,7 @@
     field public static final int TYPE_VIEW_TEXT_CHANGED = 3; // 0x3
     field public static final int TYPE_VIEW_TREE_APPEARED = 5; // 0x5
     field public static final int TYPE_VIEW_TREE_APPEARING = 4; // 0x4
+    field public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10; // 0xa
   }
 
   public final class ContentCaptureManager {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 6ccdf91..d765f66 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -431,6 +431,7 @@
 
   public class DevicePolicyManager {
     method public int checkProvisioningPreCondition(@Nullable String, @NonNull String);
+    method @RequiresPermission("android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS") public void clearOrganizationId();
     method @RequiresPermission(android.Manifest.permission.CLEAR_FREEZE_PERIOD) public void clearSystemUpdatePolicyFreezePeriodRecord();
     method @Nullable public android.os.UserHandle createAndProvisionManagedProfile(@NonNull android.app.admin.ManagedProfileProvisioningParams) throws android.app.admin.ProvisioningException;
     method @RequiresPermission(android.Manifest.permission.FORCE_DEVICE_POLICY_MANAGER_LOGS) public long forceNetworkLogs();
@@ -453,6 +454,7 @@
     method @RequiresPermission("android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS") public void resetDefaultCrossProfileIntentFilters(int);
     method @RequiresPermission(allOf={"android.permission.MANAGE_DEVICE_ADMINS", android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int);
     method @RequiresPermission("android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS") public boolean setDeviceOwner(@NonNull android.content.ComponentName, @Nullable String, int);
+    method @RequiresPermission("android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS") public boolean setDeviceOwnerOnly(@NonNull android.content.ComponentName, @Nullable String, int);
     method @RequiresPermission("android.permission.MANAGE_DEVICE_ADMINS") public void setNextOperationSafety(int, int);
     field public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED = "android.app.action.DATA_SHARING_RESTRICTION_APPLIED";
     field public static final String ACTION_DEVICE_POLICY_CONSTANTS_CHANGED = "android.app.action.DEVICE_POLICY_CONSTANTS_CHANGED";
@@ -1139,10 +1141,10 @@
 package android.hardware.devicestate {
 
   public final class DeviceStateManager {
-    method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void cancelRequest(@NonNull android.hardware.devicestate.DeviceStateRequest);
+    method @RequiresPermission(value=android.Manifest.permission.CONTROL_DEVICE_STATE, conditional=true) public void cancelRequest(@NonNull android.hardware.devicestate.DeviceStateRequest);
     method @NonNull public int[] getSupportedStates();
     method public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateCallback);
-    method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void requestState(@NonNull android.hardware.devicestate.DeviceStateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.devicestate.DeviceStateRequest.Callback);
+    method @RequiresPermission(value=android.Manifest.permission.CONTROL_DEVICE_STATE, conditional=true) public void requestState(@NonNull android.hardware.devicestate.DeviceStateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.devicestate.DeviceStateRequest.Callback);
     method public void unregisterCallback(@NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateCallback);
     field public static final int MAXIMUM_DEVICE_STATE = 255; // 0xff
     field public static final int MINIMUM_DEVICE_STATE = 0; // 0x0
@@ -1287,6 +1289,12 @@
 
 package android.inputmethodservice {
 
+  public abstract class AbstractInputMethodService extends android.window.WindowProviderService implements android.view.KeyEvent.Callback {
+    method public final int getInitialDisplayId();
+    method @Nullable public final android.os.Bundle getWindowContextOptions();
+    method public final int getWindowType();
+  }
+
   @UiContext public class InputMethodService extends android.inputmethodservice.AbstractInputMethodService {
     field public static final long FINISH_INPUT_NO_FALLBACK_CONNECTION = 156215187L; // 0x94fa793L
   }
@@ -2127,6 +2135,7 @@
   public final class DeviceConfig {
     field public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
     field public static final String NAMESPACE_ANDROID = "android";
+    field public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
     field public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
     field public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
     field public static final String NAMESPACE_JOB_SCHEDULER = "jobscheduler";
@@ -2389,6 +2398,10 @@
     method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public void triggerHardwareRecognitionEventForTest(int, int, boolean, int, int, int, boolean, @NonNull android.media.AudioFormat, @Nullable byte[]);
   }
 
+  public final class VisibleActivityInfo implements android.os.Parcelable {
+    ctor public VisibleActivityInfo(int, @NonNull android.os.IBinder);
+  }
+
 }
 
 package android.service.watchdog {
@@ -3184,13 +3197,6 @@
     method @Nullable public android.view.View getBrandingView();
   }
 
-  public final class StartingWindowInfo implements android.os.Parcelable {
-    ctor public StartingWindowInfo();
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.window.StartingWindowInfo> CREATOR;
-  }
-
   public final class TaskAppearedInfo implements android.os.Parcelable {
     ctor public TaskAppearedInfo(@NonNull android.app.ActivityManager.RunningTaskInfo, @NonNull android.view.SurfaceControl);
     method public int describeContents();
@@ -3200,9 +3206,63 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.window.TaskAppearedInfo> CREATOR;
   }
 
+  public final class TaskFragmentAppearedInfo implements android.os.Parcelable {
+    method @NonNull public android.view.SurfaceControl getLeash();
+    method @NonNull public android.window.TaskFragmentInfo getTaskFragmentInfo();
+    field @NonNull public static final android.os.Parcelable.Creator<android.window.TaskFragmentAppearedInfo> CREATOR;
+  }
+
+  public final class TaskFragmentCreationParams implements android.os.Parcelable {
+    method @NonNull public android.os.IBinder getFragmentToken();
+    method @NonNull public android.graphics.Rect getInitialBounds();
+    method @NonNull public android.window.TaskFragmentOrganizerToken getOrganizer();
+    method @NonNull public android.os.IBinder getOwnerToken();
+    method public int getWindowingMode();
+    field @NonNull public static final android.os.Parcelable.Creator<android.window.TaskFragmentCreationParams> CREATOR;
+  }
+
+  public static final class TaskFragmentCreationParams.Builder {
+    ctor public TaskFragmentCreationParams.Builder(@NonNull android.window.TaskFragmentOrganizerToken, @NonNull android.os.IBinder, @NonNull android.os.IBinder);
+    method @NonNull public android.window.TaskFragmentCreationParams build();
+    method @NonNull public android.window.TaskFragmentCreationParams.Builder setInitialBounds(@NonNull android.graphics.Rect);
+    method @NonNull public android.window.TaskFragmentCreationParams.Builder setWindowingMode(int);
+  }
+
+  public final class TaskFragmentInfo implements android.os.Parcelable {
+    method public boolean equalsForTaskFragmentOrganizer(@Nullable android.window.TaskFragmentInfo);
+    method @NonNull public java.util.List<android.os.IBinder> getActivities();
+    method @NonNull public android.content.res.Configuration getConfiguration();
+    method @NonNull public android.os.IBinder getFragmentToken();
+    method @NonNull public android.graphics.Point getPositionInParent();
+    method public int getRunningActivityCount();
+    method @NonNull public android.window.WindowContainerToken getToken();
+    method public int getWindowingMode();
+    method public boolean hasRunningActivity();
+    method public boolean isEmpty();
+    method public boolean isTaskClearedForReuse();
+    method public boolean isVisible();
+    field @NonNull public static final android.os.Parcelable.Creator<android.window.TaskFragmentInfo> CREATOR;
+  }
+
+  public class TaskFragmentOrganizer extends android.window.WindowOrganizer {
+    ctor public TaskFragmentOrganizer(@NonNull java.util.concurrent.Executor);
+    method @NonNull public java.util.concurrent.Executor getExecutor();
+    method @NonNull public android.window.TaskFragmentOrganizerToken getOrganizerToken();
+    method public void onTaskFragmentAppeared(@NonNull android.window.TaskFragmentAppearedInfo);
+    method public void onTaskFragmentError(@NonNull android.os.IBinder, @NonNull Throwable);
+    method public void onTaskFragmentInfoChanged(@NonNull android.window.TaskFragmentInfo);
+    method public void onTaskFragmentParentInfoChanged(@NonNull android.os.IBinder, @NonNull android.content.res.Configuration);
+    method public void onTaskFragmentVanished(@NonNull android.window.TaskFragmentInfo);
+    method @CallSuper public void registerOrganizer();
+    method @CallSuper public void unregisterOrganizer();
+  }
+
+  public final class TaskFragmentOrganizerToken implements android.os.Parcelable {
+    field @NonNull public static final android.os.Parcelable.Creator<android.window.TaskFragmentOrganizerToken> CREATOR;
+  }
+
   public class TaskOrganizer extends android.window.WindowOrganizer {
     ctor public TaskOrganizer();
-    method @BinderThread public void addStartingWindow(@NonNull android.window.StartingWindowInfo, @NonNull android.os.IBinder);
     method @BinderThread public void copySplashScreenView(int);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void createRootTask(int, int, @Nullable android.os.IBinder);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull android.window.WindowContainerToken);
@@ -3215,7 +3275,6 @@
     method @BinderThread public void onTaskInfoChanged(@NonNull android.app.ActivityManager.RunningTaskInfo);
     method @BinderThread public void onTaskVanished(@NonNull android.app.ActivityManager.RunningTaskInfo);
     method @CallSuper @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public java.util.List<android.window.TaskAppearedInfo> registerOrganizer();
-    method @BinderThread public void removeStartingWindow(int, @Nullable android.view.SurfaceControl, @Nullable android.graphics.Rect, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void setInterceptBackPressedOnTaskRoot(@NonNull android.window.WindowContainerToken, boolean);
     method @CallSuper @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void unregisterOrganizer();
   }
@@ -3228,26 +3287,42 @@
 
   public final class WindowContainerTransaction implements android.os.Parcelable {
     ctor public WindowContainerTransaction();
+    method @NonNull public android.window.WindowContainerTransaction createTaskFragment(@NonNull android.window.TaskFragmentCreationParams);
+    method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.window.WindowContainerToken);
     method public int describeContents();
     method @NonNull public android.window.WindowContainerTransaction reorder(@NonNull android.window.WindowContainerToken, boolean);
     method @NonNull public android.window.WindowContainerTransaction reparent(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, boolean);
+    method @NonNull public android.window.WindowContainerTransaction reparentActivityToTaskFragment(@NonNull android.os.IBinder, @NonNull android.os.IBinder);
+    method @NonNull public android.window.WindowContainerTransaction reparentChildren(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken);
     method @NonNull public android.window.WindowContainerTransaction reparentTasks(@Nullable android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, @Nullable int[], @Nullable int[], boolean);
     method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
     method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int);
     method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
+    method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @Nullable android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams);
     method @NonNull public android.window.WindowContainerTransaction setAppBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
     method @NonNull public android.window.WindowContainerTransaction setBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
     method @NonNull public android.window.WindowContainerTransaction setBoundsChangeTransaction(@NonNull android.window.WindowContainerToken, @NonNull android.view.SurfaceControl.Transaction);
+    method @NonNull public android.window.WindowContainerTransaction setErrorCallbackToken(@NonNull android.os.IBinder);
     method @NonNull public android.window.WindowContainerTransaction setFocusable(@NonNull android.window.WindowContainerToken, boolean);
     method @NonNull public android.window.WindowContainerTransaction setHidden(@NonNull android.window.WindowContainerToken, boolean);
     method @NonNull public android.window.WindowContainerTransaction setLaunchRoot(@NonNull android.window.WindowContainerToken, @Nullable int[], @Nullable int[]);
     method @NonNull public android.window.WindowContainerTransaction setScreenSizeDp(@NonNull android.window.WindowContainerToken, int, int);
     method @NonNull public android.window.WindowContainerTransaction setSmallestScreenWidthDp(@NonNull android.window.WindowContainerToken, int);
     method @NonNull public android.window.WindowContainerTransaction setWindowingMode(@NonNull android.window.WindowContainerToken, int);
+    method @NonNull public android.window.WindowContainerTransaction startActivityInTaskFragment(@NonNull android.os.IBinder, @NonNull android.os.IBinder, @NonNull android.content.Intent, @Nullable android.os.Bundle);
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.window.WindowContainerTransaction> CREATOR;
   }
 
+  public static class WindowContainerTransaction.TaskFragmentAdjacentParams {
+    ctor public WindowContainerTransaction.TaskFragmentAdjacentParams();
+    ctor public WindowContainerTransaction.TaskFragmentAdjacentParams(@NonNull android.os.Bundle);
+    method public void setShouldDelayPrimaryLastActivityRemoval(boolean);
+    method public void setShouldDelaySecondaryLastActivityRemoval(boolean);
+    method public boolean shouldDelayPrimaryLastActivityRemoval();
+    method public boolean shouldDelaySecondaryLastActivityRemoval();
+  }
+
   public abstract class WindowContainerTransactionCallback {
     ctor public WindowContainerTransactionCallback();
     method public abstract void onTransactionReady(int, @NonNull android.view.SurfaceControl.Transaction);
@@ -3255,14 +3330,15 @@
 
   public class WindowOrganizer {
     ctor public WindowOrganizer();
-    method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public int applySyncTransaction(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.WindowContainerTransactionCallback);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void applyTransaction(@NonNull android.window.WindowContainerTransaction);
+    method @RequiresPermission(value=android.Manifest.permission.MANAGE_ACTIVITY_TASKS, conditional=true) public int applySyncTransaction(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.WindowContainerTransactionCallback);
+    method @RequiresPermission(value=android.Manifest.permission.MANAGE_ACTIVITY_TASKS, conditional=true) public void applyTransaction(@NonNull android.window.WindowContainerTransaction);
   }
 
   @UiContext public abstract class WindowProviderService extends android.app.Service {
     ctor public WindowProviderService();
     method public final void attachToWindowToken(@NonNull android.os.IBinder);
-    method @Nullable public android.os.Bundle getWindowContextOptions();
+    method @NonNull public int getInitialDisplayId();
+    method @CallSuper @Nullable public android.os.Bundle getWindowContextOptions();
     method public abstract int getWindowType();
   }
 
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index b31d8f7..765a2ba 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -16,6 +16,8 @@
 
 package android.accessibilityservice;
 
+import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
+
 import android.accessibilityservice.GestureDescription.MotionEventGenerator;
 import android.annotation.CallbackExecutor;
 import android.annotation.ColorInt;
@@ -27,6 +29,7 @@
 import android.app.Service;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Bitmap;
@@ -36,6 +39,7 @@
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -961,30 +965,31 @@
         }
     }
 
+    @NonNull
     @Override
     public Context createDisplayContext(Display display) {
-        final Context context = super.createDisplayContext(display);
-        final int displayId = display.getDisplayId();
-        setDefaultTokenInternal(context, displayId);
-        return context;
+        return new AccessibilityContext(super.createDisplayContext(display), mConnectionId);
     }
 
-    private void setDefaultTokenInternal(Context context, int displayId) {
-        final WindowManagerImpl wm = (WindowManagerImpl) context.getSystemService(WINDOW_SERVICE);
-        final IAccessibilityServiceConnection connection =
-                AccessibilityInteractionClient.getInstance(this).getConnection(mConnectionId);
-        IBinder token = null;
-        if (connection != null) {
-            synchronized (mLock) {
-                try {
-                    token = connection.getOverlayWindowToken(displayId);
-                } catch (RemoteException re) {
-                    Log.w(LOG_TAG, "Failed to get window token", re);
-                    re.rethrowFromSystemServer();
-                }
-            }
-            wm.setDefaultToken(token);
+    @NonNull
+    @Override
+    public Context createWindowContext(int type, @Nullable Bundle options) {
+        final Context context = super.createWindowContext(type, options);
+        if (type != TYPE_ACCESSIBILITY_OVERLAY) {
+            return context;
         }
+        return new AccessibilityContext(context, mConnectionId);
+    }
+
+    @NonNull
+    @Override
+    public Context createWindowContext(@NonNull Display display, int type,
+            @Nullable Bundle options) {
+        final Context context = super.createWindowContext(display, type, options);
+        if (type != TYPE_ACCESSIBILITY_OVERLAY) {
+            return context;
+        }
+        return new AccessibilityContext(context, mConnectionId);
     }
 
     /**
@@ -2069,6 +2074,10 @@
         if (WINDOW_SERVICE.equals(name)) {
             if (mWindowManager == null) {
                 mWindowManager = (WindowManager) getBaseContext().getSystemService(name);
+                final WindowManagerImpl wm = (WindowManagerImpl) mWindowManager;
+                // Set e default token obtained from the connection to ensure client could use
+                // accessibility overlay.
+                wm.setDefaultToken(mWindowToken);
             }
             return mWindowManager;
         }
@@ -2177,8 +2186,10 @@
 
                 // The client may have already obtained the window manager, so
                 // update the default token on whatever manager we gave them.
-                final WindowManagerImpl wm = (WindowManagerImpl) getSystemService(WINDOW_SERVICE);
-                wm.setDefaultToken(windowToken);
+                if (mWindowManager != null) {
+                    final WindowManagerImpl wm = (WindowManagerImpl) mWindowManager;
+                    wm.setDefaultToken(mWindowToken);
+                }
             }
 
             @Override
@@ -2675,4 +2686,58 @@
             }
         }
     }
+
+    private static class AccessibilityContext extends ContextWrapper {
+        private final int mConnectionId;
+
+        private AccessibilityContext(Context base, int connectionId) {
+            super(base);
+            mConnectionId = connectionId;
+            setDefaultTokenInternal(this, getDisplayId());
+        }
+
+        @NonNull
+        @Override
+        public Context createDisplayContext(Display display) {
+            return new AccessibilityContext(super.createDisplayContext(display), mConnectionId);
+        }
+
+        @NonNull
+        @Override
+        public Context createWindowContext(int type, @Nullable Bundle options) {
+            final Context context = super.createWindowContext(type, options);
+            if (type != TYPE_ACCESSIBILITY_OVERLAY) {
+                return context;
+            }
+            return new AccessibilityContext(context, mConnectionId);
+        }
+
+        @NonNull
+        @Override
+        public Context createWindowContext(@NonNull Display display, int type,
+                @Nullable Bundle options) {
+            final Context context = super.createWindowContext(display, type, options);
+            if (type != TYPE_ACCESSIBILITY_OVERLAY) {
+                return context;
+            }
+            return new AccessibilityContext(context, mConnectionId);
+        }
+
+        private void setDefaultTokenInternal(Context context, int displayId) {
+            final WindowManagerImpl wm = (WindowManagerImpl) context.getSystemService(
+                    WINDOW_SERVICE);
+            final IAccessibilityServiceConnection connection =
+                    AccessibilityInteractionClient.getConnection(mConnectionId);
+            IBinder token = null;
+            if (connection != null) {
+                try {
+                    token = connection.getOverlayWindowToken(displayId);
+                } catch (RemoteException re) {
+                    Log.w(LOG_TAG, "Failed to get window token", re);
+                    re.rethrowFromSystemServer();
+                }
+                wm.setDefaultToken(token);
+            }
+        }
+    }
 }
diff --git a/core/java/android/accessibilityservice/AccessibilityTrace.java b/core/java/android/accessibilityservice/AccessibilityTrace.java
new file mode 100644
index 0000000..f28015a
--- /dev/null
+++ b/core/java/android/accessibilityservice/AccessibilityTrace.java
@@ -0,0 +1,216 @@
+/**
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.accessibilityservice;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Interface to log accessibility trace.
+ *
+ * @hide
+ */
+public interface AccessibilityTrace {
+    String NAME_ACCESSIBILITY_SERVICE_CONNECTION = "IAccessibilityServiceConnection";
+    String NAME_ACCESSIBILITY_SERVICE_CLIENT = "IAccessibilityServiceClient";
+    String NAME_ACCESSIBILITY_MANAGER = "IAccessibilityManager";
+    String NAME_ACCESSIBILITY_MANAGER_CLIENT = "IAccessibilityManagerClient";
+    String NAME_ACCESSIBILITY_INTERACTION_CONNECTION = "IAccessibilityInteractionConnection";
+    String NAME_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK =
+            "IAccessibilityInteractionConnectionCallback";
+    String NAME_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK = "IRemoteMagnificationAnimationCallback";
+    String NAME_WINDOW_MAGNIFICATION_CONNECTION = "IWindowMagnificationConnection";
+    String NAME_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK = "IWindowMagnificationConnectionCallback";
+    String NAME_WINDOW_MANAGER_INTERNAL = "WindowManagerInternal";
+    String NAME_WINDOWS_FOR_ACCESSIBILITY_CALLBACK = "WindowsForAccessibilityCallback";
+    String NAME_MAGNIFICATION_CALLBACK = "MagnificationCallbacks";
+    String NAME_INPUT_FILTER = "InputFilter";
+    String NAME_GESTURE = "Gesture";
+    String NAME_ACCESSIBILITY_SERVICE = "AccessibilityService";
+    String NAME_PACKAGE_BROADCAST_RECEIVER = "PMBroadcastReceiver";
+    String NAME_USER_BROADCAST_RECEIVER = "UserBroadcastReceiver";
+    String NAME_FINGERPRINT = "FingerprintGesture";
+    String NAME_ACCESSIBILITY_INTERACTION_CLIENT = "AccessibilityInteractionClient";
+
+    String NAME_ALL_LOGGINGS = "AllLoggings";
+    String NAME_NONE = "None";
+
+    long FLAGS_ACCESSIBILITY_SERVICE_CONNECTION = 0x0000000000000001L;
+    long FLAGS_ACCESSIBILITY_SERVICE_CLIENT = 0x0000000000000002L;
+    long FLAGS_ACCESSIBILITY_MANAGER = 0x0000000000000004L;
+    long FLAGS_ACCESSIBILITY_MANAGER_CLIENT = 0x0000000000000008L;
+    long FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION = 0x0000000000000010L;
+    long FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK = 0x0000000000000020L;
+    long FLAGS_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK = 0x0000000000000040L;
+    long FLAGS_WINDOW_MAGNIFICATION_CONNECTION = 0x0000000000000080L;
+    long FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK = 0x0000000000000100L;
+    long FLAGS_WINDOW_MANAGER_INTERNAL = 0x0000000000000200L;
+    long FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK = 0x0000000000000400L;
+    long FLAGS_MAGNIFICATION_CALLBACK = 0x0000000000000800L;
+    long FLAGS_INPUT_FILTER = 0x0000000000001000L;
+    long FLAGS_GESTURE = 0x0000000000002000L;
+    long FLAGS_ACCESSIBILITY_SERVICE = 0x0000000000004000L;
+    long FLAGS_PACKAGE_BROADCAST_RECEIVER = 0x0000000000008000L;
+    long FLAGS_USER_BROADCAST_RECEIVER = 0x0000000000010000L;
+    long FLAGS_FINGERPRINT = 0x0000000000020000L;
+    long FLAGS_ACCESSIBILITY_INTERACTION_CLIENT = 0x0000000000040000L;
+
+    long FLAGS_LOGGING_NONE = 0x0000000000000000L;
+    long FLAGS_LOGGING_ALL = 0xFFFFFFFFFFFFFFFFL;
+
+    long FLAGS_ACCESSIBILITY_MANAGER_CLIENT_STATES = FLAGS_ACCESSIBILITY_INTERACTION_CLIENT
+            | FLAGS_ACCESSIBILITY_SERVICE
+            | FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION
+            | FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK;
+
+    Map<String, Long> sNamesToFlags = Map.ofEntries(
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_SERVICE_CONNECTION, FLAGS_ACCESSIBILITY_SERVICE_CONNECTION),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_SERVICE_CLIENT, FLAGS_ACCESSIBILITY_SERVICE_CLIENT),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_MANAGER, FLAGS_ACCESSIBILITY_MANAGER),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_MANAGER_CLIENT, FLAGS_ACCESSIBILITY_MANAGER_CLIENT),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_INTERACTION_CONNECTION,
+                    FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK,
+                    FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK,
+                    FLAGS_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_WINDOW_MAGNIFICATION_CONNECTION, FLAGS_WINDOW_MAGNIFICATION_CONNECTION),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK,
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_WINDOW_MANAGER_INTERNAL, FLAGS_WINDOW_MANAGER_INTERNAL),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
+                    FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_MAGNIFICATION_CALLBACK, FLAGS_MAGNIFICATION_CALLBACK),
+            new AbstractMap.SimpleEntry<String, Long>(NAME_INPUT_FILTER, FLAGS_INPUT_FILTER),
+            new AbstractMap.SimpleEntry<String, Long>(NAME_GESTURE, FLAGS_GESTURE),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_SERVICE, FLAGS_ACCESSIBILITY_SERVICE),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_PACKAGE_BROADCAST_RECEIVER, FLAGS_PACKAGE_BROADCAST_RECEIVER),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_USER_BROADCAST_RECEIVER, FLAGS_USER_BROADCAST_RECEIVER),
+            new AbstractMap.SimpleEntry<String, Long>(NAME_FINGERPRINT, FLAGS_FINGERPRINT),
+            new AbstractMap.SimpleEntry<String, Long>(
+                    NAME_ACCESSIBILITY_INTERACTION_CLIENT, FLAGS_ACCESSIBILITY_INTERACTION_CLIENT),
+            new AbstractMap.SimpleEntry<String, Long>(NAME_NONE, FLAGS_LOGGING_NONE),
+            new AbstractMap.SimpleEntry<String, Long>(NAME_ALL_LOGGINGS, FLAGS_LOGGING_ALL));
+
+    /**
+     * Get the flags of the logging types by the given names.
+     * The names list contains logging type names in lower case.
+     */
+    static long getLoggingFlagsFromNames(List<String> names) {
+        long types = FLAGS_LOGGING_NONE;
+        for (String name : names) {
+            long flag = sNamesToFlags.get(name);
+            types |= flag;
+        }
+        return types;
+    }
+
+    /**
+     * Get the list of the names of logging types by the given flags.
+     */
+    static List<String> getNamesOfLoggingTypes(long flags) {
+        List<String> list = new ArrayList<String>();
+
+        for (Map.Entry<String, Long> entry : sNamesToFlags.entrySet()) {
+            if ((entry.getValue() & flags) != FLAGS_LOGGING_NONE) {
+                list.add(entry.getKey());
+            }
+        }
+
+        return list;
+    }
+
+    /**
+     * Whether the trace is enabled for any logging type.
+     */
+    boolean isA11yTracingEnabled();
+
+    /**
+     * Whether the trace is enabled for any of the given logging type.
+     */
+    boolean isA11yTracingEnabledForTypes(long typeIdFlags);
+
+    /**
+     * Get trace state to be sent to AccessibilityManager.
+     */
+    int getTraceStateForAccessibilityManagerClientState();
+
+    /**
+     * Start tracing for the given logging types.
+     */
+    void startTrace(long flagss);
+
+    /**
+     * Stop tracing.
+     */
+    void stopTrace();
+
+    /**
+     * Log one trace entry.
+     * @param where A string to identify this log entry, which can be used to search through the
+     *        tracing file.
+     * @param loggingFlags Flags to identify which logging types this entry belongs to. This
+     *        can be used to filter the log entries when generating tracing file.
+     */
+    void logTrace(String where, long loggingFlags);
+
+    /**
+     * Log one trace entry.
+     * @param where A string to identify this log entry, which can be used to filter/search
+     *        through the tracing file.
+     * @param loggingFlags Flags to identify which logging types this entry belongs to. This
+     *        can be used to filter the log entries when generating tracing file.
+     * @param callingParams The parameters for the method to be logged.
+     */
+    void logTrace(String where, long loggingFlags, String callingParams);
+
+    /**
+     * Log one trace entry. Accessibility services using AccessibilityInteractionClient to
+     * make screen content related requests use this API to log entry when receive callback.
+     * @param timestamp The timestamp when a callback is received.
+     * @param where A string to identify this log entry, which can be used to filter/search
+     *        through the tracing file.
+     * @param loggingFlags Flags to identify which logging types this entry belongs to. This
+     *        can be used to filter the log entries when generating tracing file.
+     * @param callingParams The parameters for the callback.
+     * @param processId The process id of the calling component.
+     * @param threadId The threadId of the calling component.
+     * @param callingUid The calling uid of the callback.
+     * @param callStack The call stack of the callback.
+     * @param ignoreStackElements ignore these call stack element
+     */
+    void logTrace(long timestamp, String where, long loggingFlags, String callingParams,
+            int processId, long threadId, int callingUid, StackTraceElement[] callStack,
+            Set<String> ignoreStackElements);
+}
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 923b6f4..1e76bbf 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -118,6 +118,6 @@
 
     void setFocusAppearance(int strokeWidth, int color);
 
-    oneway void logTrace(long timestamp, String where, String callingParams, int processId,
-        long threadId, int callingUid, in Bundle serializedCallingStackInBundle);
+    oneway void logTrace(long timestamp, String where, long loggingTypes, String callingParams,
+        int processId, long threadId, int callingUid, in Bundle serializedCallingStackInBundle);
 }
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index db5dcc5..f453ba1 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -77,7 +77,6 @@
 import android.os.Looper;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
-import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager.ServiceNotFoundException;
@@ -141,7 +140,6 @@
 import android.widget.Toast;
 import android.widget.Toolbar;
 import android.window.SplashScreen;
-import android.window.SplashScreenView;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -970,7 +968,6 @@
     private UiTranslationController mUiTranslationController;
 
     private SplashScreen mSplashScreen;
-    private SplashScreenView mSplashScreenView;
 
     private final WindowControllerCallback mWindowControllerCallback =
             new WindowControllerCallback() {
@@ -1539,6 +1536,17 @@
         getApplication().dispatchActivityPostDestroyed(this);
     }
 
+    private void dispatchActivityConfigurationChanged() {
+        getApplication().dispatchActivityConfigurationChanged(this);
+        Object[] callbacks = collectActivityLifecycleCallbacks();
+        if (callbacks != null) {
+            for (int i = 0; i < callbacks.length; i++) {
+                ((Application.ActivityLifecycleCallbacks) callbacks[i])
+                        .onActivityConfigurationChanged(this);
+            }
+        }
+    }
+
     private Object[] collectActivityLifecycleCallbacks() {
         Object[] callbacks = null;
         synchronized (mActivityLifecycleCallbacks) {
@@ -1630,16 +1638,6 @@
         }
     }
 
-    /** @hide */
-    public void setSplashScreenView(SplashScreenView v) {
-        mSplashScreenView = v;
-    }
-
-    /** @hide */
-    SplashScreenView getSplashScreenView() {
-        return mSplashScreenView;
-    }
-
     /**
      * Same as {@link #onCreate(android.os.Bundle)} but called for those activities created with
      * the attribute {@link android.R.attr#persistableMode} set to
@@ -1923,10 +1921,14 @@
     }
 
     /**
-     * Called after {@link #onRestoreInstanceState}, {@link #onRestart}, or
-     * {@link #onPause}, for your activity to start interacting with the user. This is an indicator
-     * that the activity became active and ready to receive input. It is on top of an activity stack
-     * and visible to user.
+     * Called after {@link #onRestoreInstanceState}, {@link #onRestart}, or {@link #onPause}. This
+     * is usually a hint for your activity to start interacting with the user, which is a good
+     * indicator that the activity became active and ready to receive input. This sometimes could
+     * also be a transit state toward another resting state. For instance, an activity may be
+     * relaunched to {@link #onPause} due to configuration changes and the activity was visible,
+     * but wasn’t the top-most activity of an activity task. {@link #onResume} is guaranteed to be
+     * called before {@link #onPause} in this case which honors the activity lifecycle policy and
+     * the activity eventually rests in {@link #onPause}.
      *
      * <p>On platform versions prior to {@link android.os.Build.VERSION_CODES#Q} this is also a good
      * place to try to open exclusive-access devices or to get access to singleton resources.
@@ -2492,12 +2494,11 @@
      *
      * <p>To get the voice interactor you need to call {@link #getVoiceInteractor()}
      * which would return non <code>null</code> only if there is an ongoing voice
-     * interaction session. You an also detect when the voice interactor is no
+     * interaction session. You can also detect when the voice interactor is no
      * longer valid because the voice interaction session that is backing is finished
      * by calling {@link VoiceInteractor#registerOnDestroyedCallback(Executor, Runnable)}.
      *
-     * <p>This method will be called only after {@link #onStart()} is being called and
-     * before {@link #onStop()} is being called.
+     * <p>This method will be called only after {@link #onStart()} and before {@link #onStop()}.
      *
      * <p>You should pass to the callback the currently supported direct actions which
      * cannot be <code>null</code> or contain <code>null</code> elements.
@@ -3028,6 +3029,8 @@
             // view changes from above.
             mActionBar.onConfigurationChanged(newConfig);
         }
+
+        dispatchActivityConfigurationChanged();
     }
 
     /**
@@ -8788,9 +8791,7 @@
      * the activity is visible after the screen is turned on when the lockscreen is up. In addition,
      * if this flag is set and the activity calls {@link
      * KeyguardManager#requestDismissKeyguard(Activity, KeyguardManager.KeyguardDismissCallback)}
-     * the screen will turn on. If the screen is off and device is not secured, this flag can turn
-     * screen on and dismiss keyguard to make this activity visible and resume, which can be used to
-     * replace {@link PowerManager#ACQUIRE_CAUSES_WAKEUP}
+     * the screen will turn on.
      *
      * @param turnScreenOn {@code true} to turn on the screen; {@code false} otherwise.
      *
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index bd43868..db7ab1a 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -205,6 +206,19 @@
         }
     }
 
+    /**
+     * Returns the non-finishing activity token below in the same task if it belongs to the same
+     * process.
+     */
+    @Nullable
+    public IBinder getActivityTokenBelow(IBinder activityToken) {
+        try {
+            return getActivityClientController().getActivityTokenBelow(activityToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     ComponentName getCallingActivity(IBinder token) {
         try {
             return getActivityClientController().getCallingActivity(token);
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 4e8480c..f8c8aa3 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -647,4 +647,28 @@
      */
     @Nullable
     public abstract List<Integer> getIsolatedProcesses(int uid);
+
+    /** @see ActivityManagerService#sendIntentSender */
+    public abstract int sendIntentSender(IIntentSender target, IBinder allowlistToken, int code,
+            Intent intent, String resolvedType,
+            IIntentReceiver finishedReceiver, String requiredPermission, Bundle options);
+
+    /**
+     * Sets the provider to communicate between voice interaction manager service and
+     * ActivityManagerService.
+     */
+    public abstract void setVoiceInteractionManagerProvider(
+            @Nullable VoiceInteractionManagerProvider provider);
+
+    /**
+     * Provides the interface to communicate between voice interaction manager service and
+     * ActivityManagerService.
+     */
+    public interface VoiceInteractionManagerProvider {
+        /**
+         * Notifies the service when a high-level activity event has been changed, for example,
+         * an activity was resumed or stopped.
+         */
+        void notifyActivityEventChanged();
+    }
 }
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 8e1f263..d1def7e 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -55,7 +55,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 import android.window.SplashScreen;
 import android.window.WindowContainerToken;
 
@@ -215,6 +215,14 @@
             "android.activity.launchRootTaskToken";
 
     /**
+     * The {@link com.android.server.wm.TaskFragment} token the activity should be launched into.
+     * @see #setLaunchTaskFragmentToken(IBinder)
+     * @hide
+     */
+    public static final String KEY_LAUNCH_TASK_FRAGMENT_TOKEN =
+            "android.activity.launchTaskFragmentToken";
+
+    /**
      * The windowing mode the activity should be launched into.
      * @hide
      */
@@ -396,6 +404,7 @@
     private int mCallerDisplayId = INVALID_DISPLAY;
     private WindowContainerToken mLaunchTaskDisplayArea;
     private WindowContainerToken mLaunchRootTask;
+    private IBinder mLaunchTaskFragmentToken;
     @WindowConfiguration.WindowingMode
     private int mLaunchWindowingMode = WINDOWING_MODE_UNDEFINED;
     @WindowConfiguration.ActivityType
@@ -417,7 +426,7 @@
     private IAppTransitionAnimationSpecsFuture mSpecsFuture;
     private RemoteAnimationAdapter mRemoteAnimationAdapter;
     private IBinder mLaunchCookie;
-    private IRemoteTransition mRemoteTransition;
+    private RemoteTransition mRemoteTransition;
     private boolean mOverrideTaskTransition;
     private String mSplashScreenThemeResName;
     @SplashScreen.SplashScreenStyle
@@ -1045,7 +1054,7 @@
      */
     @RequiresPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
     public static ActivityOptions makeRemoteAnimation(RemoteAnimationAdapter remoteAnimationAdapter,
-            IRemoteTransition remoteTransition) {
+            RemoteTransition remoteTransition) {
         final ActivityOptions opts = new ActivityOptions();
         opts.mRemoteAnimationAdapter = remoteAnimationAdapter;
         opts.mAnimationType = ANIM_REMOTE_ANIMATION;
@@ -1055,11 +1064,11 @@
 
     /**
      * Create an {@link ActivityOptions} instance that lets the application control the entire
-     * transition using a {@link IRemoteTransition}.
+     * transition using a {@link RemoteTransition}.
      * @hide
      */
     @RequiresPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
-    public static ActivityOptions makeRemoteTransition(IRemoteTransition remoteTransition) {
+    public static ActivityOptions makeRemoteTransition(RemoteTransition remoteTransition) {
         final ActivityOptions opts = new ActivityOptions();
         opts.mRemoteTransition = remoteTransition;
         return opts;
@@ -1138,6 +1147,7 @@
         mCallerDisplayId = opts.getInt(KEY_CALLER_DISPLAY_ID, INVALID_DISPLAY);
         mLaunchTaskDisplayArea = opts.getParcelable(KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN);
         mLaunchRootTask = opts.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN);
+        mLaunchTaskFragmentToken = opts.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
         mLaunchWindowingMode = opts.getInt(KEY_LAUNCH_WINDOWING_MODE, WINDOWING_MODE_UNDEFINED);
         mLaunchActivityType = opts.getInt(KEY_LAUNCH_ACTIVITY_TYPE, ACTIVITY_TYPE_UNDEFINED);
         mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1);
@@ -1171,8 +1181,7 @@
         }
         mRemoteAnimationAdapter = opts.getParcelable(KEY_REMOTE_ANIMATION_ADAPTER);
         mLaunchCookie = opts.getBinder(KEY_LAUNCH_COOKIE);
-        mRemoteTransition = IRemoteTransition.Stub.asInterface(opts.getBinder(
-                KEY_REMOTE_TRANSITION));
+        mRemoteTransition = opts.getParcelable(KEY_REMOTE_TRANSITION);
         mOverrideTaskTransition = opts.getBoolean(KEY_OVERRIDE_TASK_TRANSITION);
         mSplashScreenThemeResName = opts.getString(KEY_SPLASH_SCREEN_THEME);
         mRemoveWithTaskOrganizer = opts.getBoolean(KEY_REMOVE_WITH_TASK_ORGANIZER);
@@ -1338,7 +1347,7 @@
     }
 
     /** @hide */
-    public IRemoteTransition getRemoteTransition() {
+    public RemoteTransition getRemoteTransition() {
         return mRemoteTransition;
     }
 
@@ -1473,6 +1482,17 @@
     }
 
     /** @hide */
+    public IBinder getLaunchTaskFragmentToken() {
+        return mLaunchTaskFragmentToken;
+    }
+
+    /** @hide */
+    public ActivityOptions setLaunchTaskFragmentToken(IBinder taskFragmentToken) {
+        mLaunchTaskFragmentToken = taskFragmentToken;
+        return this;
+    }
+
+    /** @hide */
     public int getLaunchWindowingMode() {
         return mLaunchWindowingMode;
     }
@@ -1882,6 +1902,9 @@
         if (mLaunchRootTask != null) {
             b.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, mLaunchRootTask);
         }
+        if (mLaunchTaskFragmentToken != null) {
+            b.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, mLaunchTaskFragmentToken);
+        }
         if (mLaunchWindowingMode != WINDOWING_MODE_UNDEFINED) {
             b.putInt(KEY_LAUNCH_WINDOWING_MODE, mLaunchWindowingMode);
         }
@@ -1941,7 +1964,7 @@
             b.putBinder(KEY_LAUNCH_COOKIE, mLaunchCookie);
         }
         if (mRemoteTransition != null) {
-            b.putBinder(KEY_REMOTE_TRANSITION, mRemoteTransition.asBinder());
+            b.putParcelable(KEY_REMOTE_TRANSITION, mRemoteTransition);
         }
         if (mOverrideTaskTransition) {
             b.putBoolean(KEY_OVERRIDE_TASK_TRANSITION, mOverrideTaskTransition);
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index 4a7fcd2..a836625 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -476,6 +476,19 @@
     }
 
     /**
+     * Detaches the navigation bar from the app it was attached to during a transition.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
+    public void detachNavigationBarFromApp(@NonNull IBinder transition) {
+        try {
+            getService().detachNavigationBarFromApp(transition);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Information you can retrieve about a root task in the system.
      * @hide
      */
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3b0a5f3..a538e6e 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -18,7 +18,6 @@
 
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 import static android.app.ConfigurationController.createNewConfigAndUpdateIfNotNull;
-import static android.app.ConfigurationController.freeTextLayoutCachesIfNeeded;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE;
@@ -31,6 +30,10 @@
 import static android.content.ContentResolver.DEPRECATE_DATA_COLUMNS;
 import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.window.ConfigurationHelper.diffPublicWithSizeBuckets;
+import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
+import static android.window.ConfigurationHelper.isDifferentDisplay;
+import static android.window.ConfigurationHelper.shouldUpdateResources;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 
@@ -88,10 +91,8 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.HardwareRenderer;
-import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.hardware.display.DisplayManagerGlobal;
-import android.inputmethodservice.InputMethodService;
 import android.media.MediaFrameworkInitializer;
 import android.media.MediaFrameworkPlatformInitializer;
 import android.media.MediaServiceManager;
@@ -165,6 +166,7 @@
 import android.view.Display;
 import android.view.DisplayAdjustments;
 import android.view.DisplayAdjustments.FixedRotationAdjustments;
+import android.view.SurfaceControl;
 import android.view.ThreadedRenderer;
 import android.view.View;
 import android.view.ViewDebug;
@@ -183,6 +185,7 @@
 import android.window.SizeConfigurationBuckets;
 import android.window.SplashScreen;
 import android.window.SplashScreenView;
+import android.window.WindowProviderService;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -233,7 +236,6 @@
 import java.util.Objects;
 import java.util.TimeZone;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
 /**
@@ -3557,6 +3559,13 @@
                     + ", comp=" + r.intent.getComponent().toShortString()
                     + ", dir=" + r.packageInfo.getAppDir());
 
+            // updatePendingActivityConfiguration() reads from mActivities to update
+            // ActivityClientRecord which runs in a different thread. Protect modifications to
+            // mActivities to avoid race.
+            synchronized (mResourcesManager) {
+                mActivities.put(r.token, r);
+            }
+
             if (activity != null) {
                 CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                 Configuration config =
@@ -3602,6 +3611,11 @@
                 }
                 activity.mLaunchedFromBubble = r.mLaunchedFromBubble;
                 activity.mCalled = false;
+                // Assigning the activity to the record before calling onCreate() allows
+                // ActivityThread#getActivity() lookup for the callbacks triggered from
+                // ActivityLifecycleCallbacks#onActivityCreated() or
+                // ActivityLifecycleCallback#onActivityPostCreated().
+                r.activity = activity;
                 if (r.isPersistable()) {
                     mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                 } else {
@@ -3612,19 +3626,11 @@
                         "Activity " + r.intent.getComponent().toShortString() +
                         " did not call through to super.onCreate()");
                 }
-                r.activity = activity;
                 mLastReportedWindowingMode.put(activity.getActivityToken(),
                         config.windowConfiguration.getWindowingMode());
             }
             r.setState(ON_CREATE);
 
-            // updatePendingActivityConfiguration() reads from mActivities to update
-            // ActivityClientRecord which runs in a different thread. Protect modifications to
-            // mActivities to avoid race.
-            synchronized (mResourcesManager) {
-                mActivities.put(r.token, r);
-            }
-
         } catch (SuperNotCalledException e) {
             throw e;
 
@@ -4067,10 +4073,11 @@
 
     @Override
     public void handleAttachSplashScreenView(@NonNull ActivityClientRecord r,
-            @Nullable SplashScreenView.SplashScreenViewParcelable parcelable) {
+            @Nullable SplashScreenView.SplashScreenViewParcelable parcelable,
+            @NonNull SurfaceControl startingWindowLeash) {
         final DecorView decorView = (DecorView) r.window.peekDecorView();
         if (parcelable != null && decorView != null) {
-            createSplashScreen(r, decorView, parcelable);
+            createSplashScreen(r, decorView, parcelable, startingWindowLeash);
         } else {
             // shouldn't happen!
             Slog.e(TAG, "handleAttachSplashScreenView failed, unable to attach");
@@ -4078,61 +4085,50 @@
     }
 
     private void createSplashScreen(ActivityClientRecord r, DecorView decorView,
-            SplashScreenView.SplashScreenViewParcelable parcelable) {
+            SplashScreenView.SplashScreenViewParcelable parcelable,
+            @NonNull SurfaceControl startingWindowLeash) {
         final SplashScreenView.Builder builder = new SplashScreenView.Builder(r.activity);
         final SplashScreenView view = builder.createFromParcel(parcelable).build();
         decorView.addView(view);
         view.attachHostActivityAndSetSystemUIColors(r.activity, r.window);
         view.requestLayout();
-        // Ensure splash screen view is shown before remove the splash screen window.
-        final ViewRootImpl impl = decorView.getViewRootImpl();
-        final boolean hardwareEnabled = impl != null && impl.isHardwareEnabled();
-        final AtomicBoolean notified = new AtomicBoolean();
-        if (hardwareEnabled) {
-            final Runnable frameCommit = new Runnable() {
-                        @Override
-                        public void run() {
-                            view.post(() -> {
-                                if (!notified.get()) {
-                                    view.getViewTreeObserver().unregisterFrameCommitCallback(this);
-                                    ActivityClient.getInstance().reportSplashScreenAttached(
-                                            r.token);
-                                    notified.set(true);
-                                }
-                            });
-                        }
-                    };
-            view.getViewTreeObserver().registerFrameCommitCallback(frameCommit);
-        } else {
-            final ViewTreeObserver.OnDrawListener onDrawListener =
-                    new ViewTreeObserver.OnDrawListener() {
-                        @Override
-                        public void onDraw() {
-                            view.post(() -> {
-                                if (!notified.get()) {
-                                    view.getViewTreeObserver().removeOnDrawListener(this);
-                                    ActivityClient.getInstance().reportSplashScreenAttached(
-                                            r.token);
-                                    notified.set(true);
-                                }
-                            });
-                        }
-                    };
-            view.getViewTreeObserver().addOnDrawListener(onDrawListener);
+
+        view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
+            @Override
+            public void onDraw() {
+                // Transfer the splash screen view from shell to client.
+                // Call syncTransferSplashscreenViewTransaction at the first onDraw so we can ensure
+                // the client view is ready to show and we can use applyTransactionOnDraw to make
+                // all transitions happen at the same frame.
+                syncTransferSplashscreenViewTransaction(
+                        view, r.token, decorView, startingWindowLeash);
+                view.postOnAnimation(() -> view.getViewTreeObserver().removeOnDrawListener(this));
+            }
+        });
+    }
+
+    private void reportSplashscreenViewShown(IBinder token, SplashScreenView view) {
+        ActivityClient.getInstance().reportSplashScreenAttached(token);
+        synchronized (this) {
+            if (mSplashScreenGlobal != null) {
+                mSplashScreenGlobal.handOverSplashScreenView(token, view);
+            }
         }
     }
 
-    @Override
-    public void handOverSplashScreenView(@NonNull ActivityClientRecord r) {
-        final SplashScreenView v = r.activity.getSplashScreenView();
-        if (v == null) {
-            return;
-        }
-        synchronized (this) {
-            if (mSplashScreenGlobal != null) {
-                mSplashScreenGlobal.handOverSplashScreenView(r.token, v);
-            }
-        }
+    private void syncTransferSplashscreenViewTransaction(SplashScreenView view, IBinder token,
+            View decorView, @NonNull SurfaceControl startingWindowLeash) {
+        // Ensure splash screen view is shown before remove the splash screen window.
+        // Once the copied splash screen view is onDrawn on decor view, use applyTransactionOnDraw
+        // to ensure the transfer of surface view and hide starting window are happen at the same
+        // frame.
+        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+        transaction.hide(startingWindowLeash);
+
+        decorView.getViewRootImpl().applyTransactionOnDraw(transaction);
+        view.syncTransferSurfaceOnDraw();
+        // Tell server we can remove the starting window
+        decorView.postOnAnimation(() -> reportSplashscreenViewShown(token, view));
     }
 
     /**
@@ -4906,8 +4902,7 @@
                 Slog.w(TAG, "Activity top position already set to onTop=" + onTop);
                 return;
             }
-            // TODO(b/197484331): Remove this short-term workaround while fixing the binder failure.
-            Slog.e(TAG, "Activity top position already set to onTop=" + onTop);
+            throw new IllegalStateException("Activity top position already set to onTop=" + onTop);
         }
 
         r.isTopResumedActivity = onTop;
@@ -5435,6 +5430,12 @@
                     // behave properly when activity is relaunching.
                     r.window.clearContentView();
                 } else {
+                    final ViewRootImpl viewRoot = v.getViewRootImpl();
+                    if (viewRoot != null) {
+                        // Clear the callback to avoid the destroyed activity from receiving
+                        // configuration changes that are no longer effective.
+                        viewRoot.setActivityConfigCallback(null);
+                    }
                     wm.removeViewImmediate(v);
                 }
             }
@@ -5758,7 +5759,7 @@
     }
 
     @Override
-    public ArrayList<ComponentCallbacks2> collectComponentCallbacks(boolean includeActivities) {
+    public ArrayList<ComponentCallbacks2> collectComponentCallbacks(boolean includeUiContexts) {
         ArrayList<ComponentCallbacks2> callbacks
                 = new ArrayList<ComponentCallbacks2>();
 
@@ -5767,7 +5768,7 @@
             for (int i=0; i<NAPP; i++) {
                 callbacks.add(mAllApplications.get(i));
             }
-            if (includeActivities) {
+            if (includeUiContexts) {
                 for (int i = mActivities.size() - 1; i >= 0; i--) {
                     final Activity a = mActivities.valueAt(i).activity;
                     if (a != null && !a.mFinished) {
@@ -5777,11 +5778,12 @@
             }
             final int NSVC = mServices.size();
             for (int i=0; i<NSVC; i++) {
-                final ComponentCallbacks2 serviceComp = mServices.valueAt(i);
-                if (serviceComp instanceof InputMethodService) {
-                    mHasImeComponent = true;
+                final Service service = mServices.valueAt(i);
+                // If {@code includeUiContext} is set to false, WindowProviderService should not be
+                // collected because WindowProviderService is a UI Context.
+                if (includeUiContexts || !(service instanceof WindowProviderService)) {
+                    callbacks.add(service);
                 }
-                callbacks.add(serviceComp);
             }
         }
         synchronized (mProviderMap) {
@@ -5836,35 +5838,25 @@
         // change callback, see also PinnedStackTests#testConfigurationChangeOrderDuringTransition
         handleWindowingModeChangeIfNeeded(activity, newConfig);
 
-        final boolean movedToDifferentDisplay = isDifferentDisplay(activity, displayId);
-        boolean shouldReportChange = false;
-        if (activity.mCurrentConfig == null) {
-            shouldReportChange = true;
-        } else {
-            // If the new config is the same as the config this Activity is already running with and
-            // the override config also didn't change, then don't bother calling
-            // onConfigurationChanged.
-            // TODO(b/173090263): Use diff instead after the improvement of AssetManager and
-            // ResourcesImpl constructions.
-            int diff = activity.mCurrentConfig.diffPublicOnly(newConfig);
-            final ActivityClientRecord cr = getActivityClient(activityToken);
-            diff = SizeConfigurationBuckets.filterDiff(diff, activity.mCurrentConfig, newConfig,
-                    cr != null ? cr.mSizeConfigurations : null);
-
-            if (diff == 0) {
-                if (!shouldUpdateWindowMetricsBounds(activity.mCurrentConfig, newConfig)
-                        && !movedToDifferentDisplay
-                        && mResourcesManager.isSameResourcesOverrideConfig(
-                                activityToken, amOverrideConfig)) {
-                    // Nothing significant, don't proceed with updating and reporting.
-                    return null;
-                }
-            } else if ((~activity.mActivityInfo.getRealConfigChanged() & diff) == 0) {
+        final boolean movedToDifferentDisplay = isDifferentDisplay(activity.getDisplayId(),
+                displayId);
+        final ActivityClientRecord r = mActivities.get(activityToken);
+        final int diff = diffPublicWithSizeBuckets(activity.mCurrentConfig,
+                newConfig, r != null ? r.mSizeConfigurations : null);
+        final boolean hasPublicConfigChange = diff != 0;
+        // TODO(b/173090263): Use diff instead after the improvement of AssetManager and
+        // ResourcesImpl constructions.
+        final boolean shouldUpdateResources = hasPublicConfigChange
+                || shouldUpdateResources(activityToken, activity.mCurrentConfig, newConfig,
+                amOverrideConfig, movedToDifferentDisplay, hasPublicConfigChange);
+        final boolean shouldReportChange = hasPublicConfigChange
                 // If this activity doesn't handle any of the config changes, then don't bother
                 // calling onConfigurationChanged. Otherwise, report to the activity for the
                 // changes.
-                shouldReportChange = true;
-            }
+                && (~activity.mActivityInfo.getRealConfigChanged() & diff) == 0;
+        // Nothing significant, don't proceed with updating and reporting.
+        if (!shouldUpdateResources) {
+            return null;
         }
 
         // Propagate the configuration change to ResourcesManager and Activity.
@@ -5915,26 +5907,6 @@
         return configToReport;
     }
 
-    // TODO(b/173090263): Remove this method after the improvement of AssetManager and ResourcesImpl
-    // constructions.
-    /**
-     * Returns {@code true} if the metrics reported by {@link android.view.WindowMetrics} APIs
-     * should be updated.
-     *
-     * @see WindowManager#getCurrentWindowMetrics()
-     * @see WindowManager#getMaximumWindowMetrics()
-     */
-    private static boolean shouldUpdateWindowMetricsBounds(@NonNull Configuration currentConfig,
-            @NonNull Configuration newConfig) {
-        final Rect currentBounds = currentConfig.windowConfiguration.getBounds();
-        final Rect newBounds = newConfig.windowConfiguration.getBounds();
-
-        final Rect currentMaxBounds = currentConfig.windowConfiguration.getMaxBounds();
-        final Rect newMaxBounds = newConfig.windowConfiguration.getMaxBounds();
-
-        return !currentBounds.equals(newBounds) || !currentMaxBounds.equals(newMaxBounds);
-    }
-
     public final void applyConfigurationToResources(Configuration config) {
         synchronized (mResourcesManager) {
             mResourcesManager.applyConfigurationToResources(config, null);
@@ -6078,7 +6050,8 @@
             // display.
             displayId = r.activity.getDisplayId();
         }
-        final boolean movedToDifferentDisplay = isDifferentDisplay(r.activity, displayId);
+        final boolean movedToDifferentDisplay = isDifferentDisplay(
+                r.activity.getDisplayId(), displayId);
         if (r.overrideConfig != null && !r.overrideConfig.isOtherSeqNewer(overrideConfig)
                 && !movedToDifferentDisplay) {
             if (DEBUG_CONFIGURATION) {
@@ -6114,14 +6087,6 @@
         mSomeActivitiesChanged = true;
     }
 
-    /**
-     * Checks if the display id of activity is different from the given one. Note that
-     * {@link Display#INVALID_DISPLAY} means no difference.
-     */
-    private static boolean isDifferentDisplay(@NonNull Activity activity, int displayId) {
-        return displayId != INVALID_DISPLAY && displayId != activity.getDisplayId();
-    }
-
     final void handleProfilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) {
         if (start) {
             try {
@@ -6301,7 +6266,7 @@
 
     final void handleLowMemory() {
         final ArrayList<ComponentCallbacks2> callbacks =
-                collectComponentCallbacks(true /* includeActivities */);
+                collectComponentCallbacks(true /* includeUiContexts */);
 
         final int N = callbacks.size();
         for (int i=0; i<N; i++) {
@@ -6334,7 +6299,7 @@
         }
 
         final ArrayList<ComponentCallbacks2> callbacks =
-                collectComponentCallbacks(true /* includeActivities */);
+                collectComponentCallbacks(true /* includeUiContexts */);
 
         final int N = callbacks.size();
         for (int i = 0; i < N; i++) {
@@ -7568,12 +7533,6 @@
 
         ViewRootImpl.ConfigChangedCallback configChangedCallback = (Configuration globalConfig) -> {
             synchronized (mResourcesManager) {
-                // TODO (b/135719017): Temporary log for debugging IME service.
-                if (Build.IS_DEBUGGABLE && mHasImeComponent) {
-                    Log.d(TAG, "ViewRootImpl.ConfigChangedCallback for IME, "
-                            + "config=" + globalConfig);
-                }
-
                 // We need to apply this change to the resources immediately, because upon returning
                 // the view hierarchy will be informed about it.
                 if (mResourcesManager.applyConfigurationToResources(globalConfig,
@@ -7928,11 +7887,6 @@
         return mDensityCompatMode;
     }
 
-    @Override
-    public boolean hasImeComponent() {
-        return mHasImeComponent;
-    }
-
     // ------------------ Regular JNI ------------------------
     private native void nPurgePendingResources();
     private native void nDumpGraphicsInfo(FileDescriptor fd);
diff --git a/core/java/android/app/ActivityThreadInternal.java b/core/java/android/app/ActivityThreadInternal.java
index d91933c..bc698f6 100644
--- a/core/java/android/app/ActivityThreadInternal.java
+++ b/core/java/android/app/ActivityThreadInternal.java
@@ -32,11 +32,9 @@
 
     boolean isInDensityCompatMode();
 
-    boolean hasImeComponent();
-
     boolean isCachedProcessState();
 
     Application getApplication();
 
-    ArrayList<ComponentCallbacks2> collectComponentCallbacks(boolean includeActivities);
+    ArrayList<ComponentCallbacks2> collectComponentCallbacks(boolean includeUiContexts);
 }
diff --git a/core/java/android/app/ActivityTransitionCoordinator.java b/core/java/android/app/ActivityTransitionCoordinator.java
index 4b87a64..f5b3b40 100644
--- a/core/java/android/app/ActivityTransitionCoordinator.java
+++ b/core/java/android/app/ActivityTransitionCoordinator.java
@@ -871,6 +871,7 @@
                 if (view.isAttachedToWindow()) {
                     tempMatrix.reset();
                     mSharedElementParentMatrices.get(i).invert(tempMatrix);
+                    decor.transformMatrixToLocal(tempMatrix);
                     GhostView.addGhost(view, decor, tempMatrix);
                     ViewGroup parent = (ViewGroup) view.getParent();
                     if (moveWithParent && !isInTransitionGroup(parent, decor)) {
diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java
index 618eda8..a1eab65 100644
--- a/core/java/android/app/Application.java
+++ b/core/java/android/app/Application.java
@@ -205,6 +205,13 @@
          */
         default void onActivityPostDestroyed(@NonNull Activity activity) {
         }
+
+        /**
+         * Called when the Activity configuration was changed.
+         * @hide
+         */
+        default void onActivityConfigurationChanged(@NonNull Activity activity) {
+        }
     }
 
     /**
@@ -554,6 +561,16 @@
         }
     }
 
+    /* package */ void dispatchActivityConfigurationChanged(@NonNull Activity activity) {
+        Object[] callbacks = collectActivityLifecycleCallbacks();
+        if (callbacks != null) {
+            for (int i = 0; i < callbacks.length; i++) {
+                ((ActivityLifecycleCallbacks) callbacks[i]).onActivityConfigurationChanged(
+                        activity);
+            }
+        }
+    }
+
     @UnsupportedAppUsage
     private Object[] collectActivityLifecycleCallbacks() {
         Object[] callbacks = null;
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index 115101c..c743f65 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -28,6 +28,7 @@
 import android.os.IBinder;
 import android.util.MergedConfiguration;
 import android.view.DisplayAdjustments.FixedRotationAdjustments;
+import android.view.SurfaceControl;
 import android.window.SplashScreenView.SplashScreenViewParcelable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -165,10 +166,8 @@
 
     /** Attach a splash screen window view to the top of the activity */
     public abstract void handleAttachSplashScreenView(@NonNull ActivityClientRecord r,
-            @NonNull SplashScreenViewParcelable parcelable);
-
-    /** Hand over the splash screen window view to the activity */
-    public abstract void handOverSplashScreenView(@NonNull ActivityClientRecord r);
+            @NonNull SplashScreenViewParcelable parcelable,
+            @NonNull SurfaceControl startingWindowLeash);
 
     /** Perform activity launch. */
     public abstract Activity handleLaunchActivity(@NonNull ActivityClientRecord r,
diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java
index f79e078..8637e31 100644
--- a/core/java/android/app/ConfigurationController.java
+++ b/core/java/android/app/ConfigurationController.java
@@ -17,24 +17,20 @@
 package android.app;
 
 import static android.app.ActivityThread.DEBUG_CONFIGURATION;
+import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentCallbacks2;
 import android.content.Context;
-import android.content.pm.ActivityInfo;
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.HardwareRenderer;
-import android.inputmethodservice.InputMethodService;
-import android.os.Build;
 import android.os.LocaleList;
 import android.os.Trace;
 import android.util.DisplayMetrics;
-import android.util.Log;
 import android.util.Slog;
 import android.view.ContextThemeWrapper;
 import android.view.WindowManagerGlobal;
@@ -169,12 +165,7 @@
                 mPendingConfiguration = null;
             }
 
-            final boolean hasIme = mActivityThread.hasImeComponent();
             if (config == null) {
-                // TODO (b/135719017): Temporary log for debugging IME service.
-                if (Build.IS_DEBUGGABLE && hasIme) {
-                    Log.w(TAG, "handleConfigurationChanged for IME app but config is null");
-                }
                 return;
             }
 
@@ -205,12 +196,6 @@
                 mConfiguration = new Configuration();
             }
             if (!mConfiguration.isOtherSeqNewer(config) && compat == null) {
-                // TODO (b/135719017): Temporary log for debugging IME service.
-                if (Build.IS_DEBUGGABLE && hasIme) {
-                    Log.w(TAG, "handleConfigurationChanged for IME app but config seq is obsolete "
-                            + ", config=" + config
-                            + ", mConfiguration=" + mConfiguration);
-                }
                 return;
             }
 
@@ -228,7 +213,7 @@
         }
 
         final ArrayList<ComponentCallbacks2> callbacks =
-                mActivityThread.collectComponentCallbacks(false /* includeActivities */);
+                mActivityThread.collectComponentCallbacks(false /* includeUiContexts */);
 
         freeTextLayoutCachesIfNeeded(configDiff);
 
@@ -238,13 +223,6 @@
                 ComponentCallbacks2 cb = callbacks.get(i);
                 if (!equivalent) {
                     performConfigurationChanged(cb, config);
-                } else {
-                    // TODO (b/135719017): Temporary log for debugging IME service.
-                    if (Build.IS_DEBUGGABLE && cb instanceof InputMethodService) {
-                        Log.w(TAG, "performConfigurationChanged didn't callback to IME "
-                                + ", configDiff=" + configDiff
-                                + ", mConfiguration=" + mConfiguration);
-                    }
                 }
             }
         }
@@ -326,16 +304,4 @@
         return newConfig;
     }
 
-    /** Ask test layout engine to free its caches if there is a locale change. */
-    static void freeTextLayoutCachesIfNeeded(int configDiff) {
-        if (configDiff != 0) {
-            boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0);
-            if (hasLocaleConfigChange) {
-                Canvas.freeTextLayoutCaches();
-                if (DEBUG_CONFIGURATION) {
-                    Slog.v(TAG, "Cleared TextLayout Caches");
-                }
-            }
-        }
-    }
 }
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 9272e45..adb5830 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -3205,12 +3205,6 @@
     @UnsupportedAppUsage
     final void setOuterContext(@NonNull Context context) {
         mOuterContext = context;
-        // TODO(b/149463653): check if we still need this method after migrating IMS to
-        //  WindowContext.
-        if (mOuterContext.isUiContext() && mContextType <= CONTEXT_TYPE_DISPLAY_CONTEXT) {
-            mContextType = CONTEXT_TYPE_WINDOW_CONTEXT;
-            mIsConfigurationBasedContext = true;
-        }
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/app/DirectAction.java b/core/java/android/app/DirectAction.java
index b0ed490..ac3868b 100644
--- a/core/java/android/app/DirectAction.java
+++ b/core/java/android/app/DirectAction.java
@@ -22,14 +22,13 @@
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.view.accessibility.AccessibilityNodeInfo;
 
 import com.android.internal.util.Preconditions;
 
 import java.util.Objects;
 
 /**
- * Represents a abstract action that can be perform on this app. This are requested from
+ * Represents an abstract action that can be perform on this app. This are requested from
  * outside the app's UI (eg by SystemUI or assistant). The semantics of these actions are
  * not specified by the OS. This allows open-ended and scalable approach for defining how
  * an app interacts with components that expose alternative interaction models to the user
diff --git a/core/java/android/app/DisabledWallpaperManager.java b/core/java/android/app/DisabledWallpaperManager.java
index 1f323c3..ae3a9e6 100644
--- a/core/java/android/app/DisabledWallpaperManager.java
+++ b/core/java/android/app/DisabledWallpaperManager.java
@@ -74,6 +74,11 @@
         return false;
     }
 
+    private static int unsupportedInt() {
+        if (DEBUG) Log.w(TAG, "unsupported method called; returning -1", new Exception());
+        return -1;
+    }
+
     @Override
     public Drawable getDrawable() {
         return unsupported();
@@ -189,12 +194,12 @@
 
     @Override
     public int getWallpaperId(int which) {
-        return unsupported();
+        return unsupportedInt();
     }
 
     @Override
     public int getWallpaperIdForUser(int which, int userId) {
-        return unsupported();
+        return unsupportedInt();
     }
 
     @Override
@@ -209,7 +214,8 @@
 
     @Override
     public int setResource(int resid, int which) throws IOException {
-        return unsupported();
+        unsupported();
+        return 0;
     }
 
     @Override
@@ -220,19 +226,22 @@
     @Override
     public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup)
             throws IOException {
-        return unsupported();
+        unsupported();
+        return 0;
     }
 
     @Override
     public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, int which)
             throws IOException {
-        return unsupported();
+        unsupported();
+        return 0;
     }
 
     @Override
     public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, int which,
             int userId) throws IOException {
-        return unsupported();
+        unsupported();
+        return 0;
     }
 
     @Override
@@ -243,13 +252,15 @@
     @Override
     public int setStream(InputStream bitmapData, Rect visibleCropHint, boolean allowBackup)
             throws IOException {
-        return unsupported();
+        unsupported();
+        return 0;
     }
 
     @Override
     public int setStream(InputStream bitmapData, Rect visibleCropHint, boolean allowBackup,
             int which) throws IOException {
-        return unsupported();
+        unsupported();
+        return 0;
     }
 
     @Override
@@ -259,12 +270,12 @@
 
     @Override
     public int getDesiredMinimumWidth() {
-        return unsupported();
+        return unsupportedInt();
     }
 
     @Override
     public int getDesiredMinimumHeight() {
-        return unsupported();
+        return unsupportedInt();
     }
 
     @Override
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index c664969..aba6eb9 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -70,6 +70,7 @@
     boolean willActivityBeVisible(in IBinder token);
     int getDisplayId(in IBinder activityToken);
     int getTaskForActivity(in IBinder token, in boolean onlyRoot);
+    IBinder getActivityTokenBelow(IBinder token);
     ComponentName getCallingActivity(in IBinder token);
     String getCallingPackage(in IBinder token);
     int getLaunchedFromUid(in IBinder token);
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index 74d51a0..2be7803 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -330,4 +330,18 @@
      * When the Picture-in-picture state has changed.
      */
     void onPictureInPictureStateChanged(in PictureInPictureUiState pipState);
+
+    /**
+     * Re-attach navbar to the display during a recents transition.
+     * TODO(188595497): Remove this once navbar attachment is in shell.
+     */
+    void detachNavigationBarFromApp(in IBinder transition);
+
+    /**
+     * Marks a process as a delegate for the currently playing remote transition animation. This
+     * must be called from a process that is already a remote transition player or delegate. Any
+     * marked delegates are cleaned-up automatically at the end of the transition.
+     * @param caller is the IApplicationThread representing the calling process.
+     */
+    void setRunningRemoteTransitionDelegate(in IApplicationThread caller);
 }
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index b2184fe..fd6fa57 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -743,6 +743,18 @@
         }
 
         /**
+         * This overload is used for notifying the {@link android.window.TaskFragmentOrganizer}
+         * implementation internally about started activities.
+         *
+         * @see #onStartActivity(Intent)
+         * @hide
+         */
+        public ActivityResult onStartActivity(@NonNull Context who, @NonNull Intent intent,
+                @NonNull Bundle options) {
+            return onStartActivity(intent);
+        }
+
+        /**
          * Used for intercepting any started activity.
          *
          * <p> A non-null return value here will be considered a hit for this monitor.
@@ -1722,7 +1734,10 @@
                     final ActivityMonitor am = mActivityMonitors.get(i);
                     ActivityResult result = null;
                     if (am.ignoreMatchingSpecificIntents()) {
-                        result = am.onStartActivity(intent);
+                        if (options == null) {
+                            options = ActivityOptions.makeBasic().toBundle();
+                        }
+                        result = am.onStartActivity(who, intent, options);
                     }
                     if (result != null) {
                         am.mHits++;
@@ -1790,7 +1805,10 @@
                     final ActivityMonitor am = mActivityMonitors.get(i);
                     ActivityResult result = null;
                     if (am.ignoreMatchingSpecificIntents()) {
-                        result = am.onStartActivity(intents[0]);
+                        if (options == null) {
+                            options = ActivityOptions.makeBasic().toBundle();
+                        }
+                        result = am.onStartActivity(who, intents[0], options);
                     }
                     if (result != null) {
                         am.mHits++;
@@ -1861,7 +1879,10 @@
                     final ActivityMonitor am = mActivityMonitors.get(i);
                     ActivityResult result = null;
                     if (am.ignoreMatchingSpecificIntents()) {
-                        result = am.onStartActivity(intent);
+                        if (options == null) {
+                            options = ActivityOptions.makeBasic().toBundle();
+                        }
+                        result = am.onStartActivity(who, intent, options);
                     }
                     if (result != null) {
                         am.mHits++;
@@ -1928,7 +1949,10 @@
                     final ActivityMonitor am = mActivityMonitors.get(i);
                     ActivityResult result = null;
                     if (am.ignoreMatchingSpecificIntents()) {
-                        result = am.onStartActivity(intent);
+                        if (options == null) {
+                            options = ActivityOptions.makeBasic().toBundle();
+                        }
+                        result = am.onStartActivity(who, intent, options);
                     }
                     if (result != null) {
                         am.mHits++;
@@ -1974,7 +1998,10 @@
                     final ActivityMonitor am = mActivityMonitors.get(i);
                     ActivityResult result = null;
                     if (am.ignoreMatchingSpecificIntents()) {
-                        result = am.onStartActivity(intent);
+                        if (options == null) {
+                            options = ActivityOptions.makeBasic().toBundle();
+                        }
+                        result = am.onStartActivity(who, intent, options);
                     }
                     if (result != null) {
                         am.mHits++;
@@ -2021,7 +2048,10 @@
                     final ActivityMonitor am = mActivityMonitors.get(i);
                     ActivityResult result = null;
                     if (am.ignoreMatchingSpecificIntents()) {
-                        result = am.onStartActivity(intent);
+                        if (options == null) {
+                            options = ActivityOptions.makeBasic().toBundle();
+                        }
+                        result = am.onStartActivity(who, intent, options);
                     }
                     if (result != null) {
                         am.mHits++;
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 198c33e..bf3778d 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -693,7 +693,7 @@
      * @return true if activity resources override config matches the provided one or they are both
      *         null, false otherwise.
      */
-    boolean isSameResourcesOverrideConfig(@Nullable IBinder activityToken,
+    public boolean isSameResourcesOverrideConfig(@Nullable IBinder activityToken,
             @Nullable Configuration overrideConfig) {
         synchronized (mLock) {
             final ActivityResources activityResources
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 77bcef3..be702c2 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -46,7 +46,7 @@
  */
 @SystemService(Context.STATUS_BAR_SERVICE)
 public class StatusBarManager {
-
+    // LINT.IfChange
     /** @hide */
     public static final int DISABLE_EXPAND = View.STATUS_BAR_DISABLE_EXPAND;
     /** @hide */
@@ -144,6 +144,7 @@
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Disable2Flags {}
+    // LINT.ThenChange(frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt)
 
     /**
      * Default disable flags for setup
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 85758a9..bd9b6e9 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -17,6 +17,7 @@
 package android.app;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -119,6 +120,12 @@
     public int displayId;
 
     /**
+     * The feature id of {@link com.android.server.wm.TaskDisplayArea} this task is associated with.
+     * @hide
+     */
+    public int displayAreaFeatureId = FEATURE_UNDEFINED;
+
+    /**
      * The recent activity values for the highest activity in the stack to have set the values.
      * {@link Activity#setTaskDescription(android.app.ActivityManager.TaskDescription)}.
      */
@@ -243,6 +250,12 @@
      */
     public boolean isVisible;
 
+    /**
+     * Whether this task is sleeping due to sleeping display.
+     * @hide
+     */
+    public boolean isSleeping;
+
     TaskInfo() {
         // Do nothing
     }
@@ -252,6 +265,13 @@
     }
 
     /**
+     * Whether this task is visible.
+     */
+    public boolean isVisible() {
+        return isVisible;
+    }
+
+    /**
      * @param isLowResolution
      * @return
      * @hide
@@ -329,11 +349,10 @@
     }
 
     /**
-      * Returns {@code true} if parameters that are important for task organizers have changed
-      * and {@link com.android.server.wm.TaskOrginizerController} needs to notify listeners
-      * about that.
-      * @hide
-      */
+     * Returns {@code true} if the parameters that are important for task organizers are equal
+     * between this {@link TaskInfo} and {@param that}.
+     * @hide
+     */
     public boolean equalsForTaskOrganizer(@Nullable TaskInfo that) {
         if (that == null) {
             return false;
@@ -341,13 +360,15 @@
         return topActivityType == that.topActivityType
                 && isResizeable == that.isResizeable
                 && supportsMultiWindow == that.supportsMultiWindow
+                && displayAreaFeatureId == that.displayAreaFeatureId
                 && Objects.equals(positionInParent, that.positionInParent)
                 && Objects.equals(pictureInPictureParams, that.pictureInPictureParams)
                 && Objects.equals(displayCutoutInsets, that.displayCutoutInsets)
                 && getWindowingMode() == that.getWindowingMode()
                 && Objects.equals(taskDescription, that.taskDescription)
                 && isFocused == that.isFocused
-                && isVisible == that.isVisible;
+                && isVisible == that.isVisible
+                && isSleeping == that.isSleeping;
     }
 
     /**
@@ -402,8 +423,10 @@
         parentTaskId = source.readInt();
         isFocused = source.readBoolean();
         isVisible = source.readBoolean();
+        isSleeping = source.readBoolean();
         topActivityInSizeCompat = source.readBoolean();
         mTopActivityLocusId = source.readTypedObject(LocusId.CREATOR);
+        displayAreaFeatureId = source.readInt();
     }
 
     /**
@@ -440,8 +463,10 @@
         dest.writeInt(parentTaskId);
         dest.writeBoolean(isFocused);
         dest.writeBoolean(isVisible);
+        dest.writeBoolean(isSleeping);
         dest.writeBoolean(topActivityInSizeCompat);
         dest.writeTypedObject(mTopActivityLocusId, flags);
+        dest.writeInt(displayAreaFeatureId);
     }
 
     @Override
@@ -468,8 +493,10 @@
                 + " parentTaskId=" + parentTaskId
                 + " isFocused=" + isFocused
                 + " isVisible=" + isVisible
+                + " isSleeping=" + isSleeping
                 + " topActivityInSizeCompat=" + topActivityInSizeCompat
-                + " locusId= " + mTopActivityLocusId
+                + " locusId=" + mTopActivityLocusId
+                + " displayAreaFeatureId=" + displayAreaFeatureId
                 + "}";
     }
 }
diff --git a/core/java/android/app/WallpaperInfo.java b/core/java/android/app/WallpaperInfo.java
index e9b0175..99d4064 100644
--- a/core/java/android/app/WallpaperInfo.java
+++ b/core/java/android/app/WallpaperInfo.java
@@ -81,6 +81,7 @@
     final int mContextDescriptionResource;
     final boolean mShowMetadataInPreview;
     final boolean mSupportsAmbientMode;
+    final boolean mShouldUseDefaultUnfoldTransition;
     final String mSettingsSliceUri;
     final boolean mSupportMultipleDisplays;
 
@@ -145,6 +146,9 @@
             mSupportsAmbientMode = sa.getBoolean(
                     com.android.internal.R.styleable.Wallpaper_supportsAmbientMode,
                     false);
+            mShouldUseDefaultUnfoldTransition = sa.getBoolean(
+                    com.android.internal.R.styleable
+                            .Wallpaper_shouldUseDefaultUnfoldTransition, true);
             mSettingsSliceUri = sa.getString(
                     com.android.internal.R.styleable.Wallpaper_settingsSliceUri);
             mSupportMultipleDisplays = sa.getBoolean(
@@ -171,6 +175,7 @@
         mSupportsAmbientMode = source.readInt() != 0;
         mSettingsSliceUri = source.readString();
         mSupportMultipleDisplays = source.readInt() != 0;
+        mShouldUseDefaultUnfoldTransition = source.readInt() != 0;
         mService = ResolveInfo.CREATOR.createFromParcel(source);
     }
     
@@ -393,6 +398,28 @@
         return mSupportMultipleDisplays;
     }
 
+    /**
+     * Returns whether this wallpaper should receive default zooming updates when the device
+     * changes its state (e.g. when folding or unfolding a foldable device).
+     * If set to false the wallpaper will not receive zoom events when changing the device state,
+     * so it can implement its own transition instead.
+     * <p>
+     * This corresponds to the value {@link
+     * android.R.styleable#Wallpaper_shouldUseDefaultUnfoldTransition} in the
+     * XML description of the wallpaper.
+     * <p>
+     * The default value is {@code true}.
+     *
+     * @see android.R.styleable#Wallpaper_shouldUseDefaultUnfoldTransition
+     * @return {@code true} if wallpaper should receive default device state change
+     * transition updates
+     *
+     * @attr ref android.R.styleable#Wallpaper_shouldUseDefaultUnfoldTransition
+     */
+    public boolean shouldUseDefaultUnfoldTransition() {
+        return mShouldUseDefaultUnfoldTransition;
+    }
+
     public void dump(Printer pw, String prefix) {
         pw.println(prefix + "Service:");
         mService.dump(pw, prefix + "  ");
@@ -423,6 +450,7 @@
         dest.writeInt(mSupportsAmbientMode ? 1 : 0);
         dest.writeString(mSettingsSliceUri);
         dest.writeInt(mSupportMultipleDisplays ? 1 : 0);
+        dest.writeInt(mShouldUseDefaultUnfoldTransition ? 1 : 0);
         mService.writeToParcel(dest, flags);
     }
 
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 6cfa39c..b570ae6 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -221,6 +221,20 @@
     public static final String COMMAND_REAPPLY = "android.wallpaper.reapply";
 
     /**
+     * Command for {@link #sendWallpaperCommand}: reported when the live wallpaper needs to be
+     * frozen.
+     * @hide
+     */
+    public static final String COMMAND_FREEZE = "android.wallpaper.freeze";
+
+    /**
+     * Command for {@link #sendWallpaperCommand}: reported when the live wallapper doesn't need
+     * to be frozen anymore.
+     * @hide
+     */
+    public static final String COMMAND_UNFREEZE = "android.wallpaper.unfreeze";
+
+    /**
      * Extra passed back from setWallpaper() giving the new wallpaper's assigned ID.
      * @hide
      */
@@ -548,7 +562,7 @@
             }
             if (returnDefault) {
                 Bitmap defaultWallpaper = mDefaultWallpaper;
-                if (defaultWallpaper == null) {
+                if (defaultWallpaper == null || defaultWallpaper.isRecycled()) {
                     defaultWallpaper = getDefaultWallpaper(context, which);
                     synchronized (this) {
                         mDefaultWallpaper = defaultWallpaper;
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 0fe80c4..5b01a7d 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -7726,27 +7726,64 @@
     }
 
     /**
-     * @hide
-     * Sets the given package as the device owner. The package must already be installed. There
-     * must not already be a device owner.
-     * Only apps with the MANAGE_PROFILE_AND_DEVICE_OWNERS permission and the shell uid can call
-     * this method.
-     * Calling this after the setup phase of the primary user has completed is allowed only if
-     * the caller is the shell uid, and there are no additional users and no accounts.
+     * Sets the given package as the device owner.
+     *
+     * <p>Preconditions:
+     * <ul>
+     *   <li>The package must already be installed.
+     *   <li>There must not already be a device owner.
+     *   <li>Only apps with the {@code MANAGE_PROFILE_AND_DEVICE_OWNERS} permission or the
+     *       {@link Process#SHELL_UID Shell UID} can call this method.
+     * </ul>
+     *
+     * <p>Calling this after the setup phase of the device owner user has completed is allowed only
+     * if the caller is the {@link Process#SHELL_UID Shell UID}, and there are no additional users
+     * (except when the device runs on headless system user mode, in which case it could have exact
+     * one extra user, which is the current user - the device owner will be set in the
+     * {@link UserHandle#SYSTEM system} user and a profile owner will be set in the current user)
+     * and no accounts.
+     *
      * @param who the component name to be registered as device owner.
      * @param ownerName the human readable name of the institution that owns this device.
      * @param userId ID of the user on which the device owner runs.
+     *
      * @return whether the package was successfully registered as the device owner.
-     * @throws IllegalArgumentException if the package name is null or invalid
+     *
+     * @throws IllegalArgumentException if the package name is {@code null} or invalid.
      * @throws IllegalStateException If the preconditions mentioned are not met.
+     *
+     * @hide
      */
     @TestApi
     @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS)
-    public boolean setDeviceOwner(
+    public boolean setDeviceOwner(@NonNull ComponentName who, @Nullable String ownerName,
+            @UserIdInt int userId) {
+        if (mService != null) {
+            try {
+                return mService.setDeviceOwner(who, ownerName, userId,
+                        /* setProfileOwnerOnCurrentUserIfNecessary= */ true);
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Same as {@link #setDeviceOwner(ComponentName, String, int)}, but without setting the profile
+     * owner on current user when running on headless system user mode - should be used only by
+     * testing infra.
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS)
+    public boolean setDeviceOwnerOnly(
             @NonNull ComponentName who, @Nullable String ownerName, @UserIdInt int userId) {
         if (mService != null) {
             try {
-                return mService.setDeviceOwner(who, ownerName, userId);
+                return mService.setDeviceOwner(who, ownerName, userId,
+                        /* setProfileOwnerOnCurrentUserIfNecessary= */ false);
             } catch (RemoteException re) {
                 throw re.rethrowFromSystemServer();
             }
@@ -13761,6 +13798,23 @@
     }
 
     /**
+     * Clears organization ID set by the DPC and resets the precomputed enrollment specific ID.
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS)
+    public void clearOrganizationId() {
+        if (mService == null) {
+            return;
+        }
+        try {
+            mService.clearOrganizationIdForUser(myUserId());
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Creates and provisions a managed profile and sets the
      * {@link ManagedProfileProvisioningParams#getProfileAdminComponentName()} as the profile
      * owner.
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index d287437..7804ab0 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -159,7 +159,7 @@
     void reportKeyguardDismissed(int userHandle);
     void reportKeyguardSecured(int userHandle);
 
-    boolean setDeviceOwner(in ComponentName who, String ownerName, int userId);
+    boolean setDeviceOwner(in ComponentName who, String ownerName, int userId, boolean setProfileOwnerOnCurrentUserIfNecessary);
     ComponentName getDeviceOwnerComponent(boolean callingUserOnly);
     boolean hasDeviceOwner();
     String getDeviceOwnerName();
@@ -378,6 +378,7 @@
 
     void setOrganizationColor(in ComponentName admin, in int color);
     void setOrganizationColorForUser(in int color, in int userId);
+    void clearOrganizationIdForUser(int userHandle);
     int getOrganizationColor(in ComponentName admin);
     int getOrganizationColorForUser(int userHandle);
 
diff --git a/core/java/android/app/compat/PackageOverride.java b/core/java/android/app/compat/PackageOverride.java
index fad6cd3..ebc2945 100644
--- a/core/java/android/app/compat/PackageOverride.java
+++ b/core/java/android/app/compat/PackageOverride.java
@@ -24,6 +24,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 
 /**
  * An app compat override applied to a given package and change id pairing.
@@ -139,6 +140,22 @@
 
     /** @hide */
     @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PackageOverride that = (PackageOverride) o;
+        return mMinVersionCode == that.mMinVersionCode && mMaxVersionCode == that.mMaxVersionCode
+                && mEnabled == that.mEnabled;
+    }
+
+    /** @hide */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMinVersionCode, mMaxVersionCode, mEnabled);
+    }
+
+    /** @hide */
+    @Override
     public String toString() {
         if (mMinVersionCode == Long.MIN_VALUE && mMaxVersionCode == Long.MAX_VALUE) {
             return Boolean.toString(mEnabled);
diff --git a/core/java/android/app/servertransaction/ActivityResultItem.java b/core/java/android/app/servertransaction/ActivityResultItem.java
index e059f17..27d104b 100644
--- a/core/java/android/app/servertransaction/ActivityResultItem.java
+++ b/core/java/android/app/servertransaction/ActivityResultItem.java
@@ -16,6 +16,8 @@
 
 package android.app.servertransaction;
 
+import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME;
+import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED;
 import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
 
 import android.annotation.NonNull;
@@ -23,6 +25,9 @@
 import android.app.ActivityThread.ActivityClientRecord;
 import android.app.ClientTransactionHandler;
 import android.app.ResultInfo;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Parcel;
@@ -41,11 +46,19 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private List<ResultInfo> mResultInfoList;
 
-    /* TODO(b/78294732)
+    /**
+     * Correct the lifecycle of activity result after {@link android.os.Build.VERSION_CODES#S} to
+     * guarantee that an activity gets activity result just before resume.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S)
+    public static final long CALL_ACTIVITY_RESULT_BEFORE_RESUME = 78294732L;
+
     @Override
     public int getPostExecutionState() {
-        return ON_RESUME;
-    }*/
+        return CompatChanges.isChangeEnabled(CALL_ACTIVITY_RESULT_BEFORE_RESUME)
+                ? ON_RESUME : UNDEFINED;
+    }
 
     @Override
     public void execute(ClientTransactionHandler client, ActivityClientRecord r,
diff --git a/core/java/android/app/servertransaction/ActivityTransactionItem.java b/core/java/android/app/servertransaction/ActivityTransactionItem.java
index a539812..186f25d 100644
--- a/core/java/android/app/servertransaction/ActivityTransactionItem.java
+++ b/core/java/android/app/servertransaction/ActivityTransactionItem.java
@@ -59,36 +59,37 @@
     }
 
     /**
-     * Get the {@link ActivityClientRecord} instance that corresponds to the provided token.
+     * Gets the {@link ActivityClientRecord} instance that corresponds to the provided token.
      * @param client Target client handler.
      * @param token Target activity token.
-     * @param includeLaunching Indicate to also find the {@link ActivityClientRecord} in launching
-     *                         activity list. It should be noted that there is no activity in
+     * @param includeLaunching Indicate to find the {@link ActivityClientRecord} in launching
+     *                         activity list.
+     *                         <p>Note that there is no {@link android.app.Activity} instance in
      *                         {@link ActivityClientRecord} from the launching activity list.
      * @return The {@link ActivityClientRecord} instance that corresponds to the provided token.
      */
     @NonNull ActivityClientRecord getActivityClientRecord(
             @NonNull ClientTransactionHandler client, IBinder token, boolean includeLaunching) {
-        ActivityClientRecord r = client.getActivityClient(token);
-        if (r != null) {
-            if (client.getActivity(token) == null) {
+        ActivityClientRecord r = null;
+        // Check launching Activity first to prevent race condition that activity instance has not
+        // yet set to ActivityClientRecord.
+        if (includeLaunching) {
+            r = client.getLaunchingActivity(token);
+        }
+        // Then if we don't want to find launching Activity or the ActivityClientRecord doesn't
+        // exist in launching Activity list. The ActivityClientRecord should have been initialized
+        // and put in the Activity list.
+        if (r == null) {
+            r = client.getActivityClient(token);
+            if (r != null && client.getActivity(token) == null) {
                 throw new IllegalArgumentException("Activity must not be null to execute "
                         + "transaction item");
             }
-            return r;
-        }
-        // The activity may not be launched yet. Fallback to check launching activity.
-        if (includeLaunching) {
-            r = client.getLaunchingActivity(token);
         }
         if (r == null) {
             throw new IllegalArgumentException("Activity client record must not be null to execute "
                     + "transaction item");
         }
-
-        // We don't need to check the activity of launching activity client records because they
-        // have not been launched yet.
-
         return r;
     }
 }
diff --git a/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java b/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java
index 5374984..767fd28 100644
--- a/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java
+++ b/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java
@@ -16,17 +16,14 @@
 
 package android.app.servertransaction;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
 import android.app.ClientTransactionHandler;
 import android.os.Parcel;
+import android.view.SurfaceControl;
 import android.window.SplashScreenView.SplashScreenViewParcelable;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
 /**
  * Transfer a splash screen view to an Activity.
  * @hide
@@ -34,31 +31,13 @@
 public class TransferSplashScreenViewStateItem extends ActivityTransactionItem {
 
     private SplashScreenViewParcelable mSplashScreenViewParcelable;
-    private @TransferRequest int mRequest;
-
-    @IntDef(value = {
-            ATTACH_TO,
-            HANDOVER_TO
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface TransferRequest {}
-    // request client to attach the view on it.
-    public static final int ATTACH_TO = 0;
-    // tell client that you can handle the splash screen view.
-    public static final int HANDOVER_TO = 1;
+    private SurfaceControl mStartingWindowLeash;
 
     @Override
     public void execute(@NonNull ClientTransactionHandler client,
             @NonNull ActivityThread.ActivityClientRecord r,
             PendingTransactionActions pendingActions) {
-        switch (mRequest) {
-            case ATTACH_TO:
-                client.handleAttachSplashScreenView(r, mSplashScreenViewParcelable);
-                break;
-            case HANDOVER_TO:
-                client.handOverSplashScreenView(r);
-                break;
-        }
+        client.handleAttachSplashScreenView(r, mSplashScreenViewParcelable, mStartingWindowLeash);
     }
 
     @Override
@@ -68,26 +47,27 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeInt(mRequest);
         dest.writeTypedObject(mSplashScreenViewParcelable, flags);
+        dest.writeTypedObject(mStartingWindowLeash, flags);
     }
 
     private TransferSplashScreenViewStateItem() {}
     private TransferSplashScreenViewStateItem(Parcel in) {
-        mRequest = in.readInt();
         mSplashScreenViewParcelable = in.readTypedObject(SplashScreenViewParcelable.CREATOR);
+        mStartingWindowLeash = in.readTypedObject(SurfaceControl.CREATOR);
     }
 
     /** Obtain an instance initialized with provided params. */
-    public static TransferSplashScreenViewStateItem obtain(@TransferRequest int state,
-            @Nullable SplashScreenViewParcelable parcelable) {
+    public static TransferSplashScreenViewStateItem obtain(
+            @Nullable SplashScreenViewParcelable parcelable,
+            @Nullable SurfaceControl startingWindowLeash) {
         TransferSplashScreenViewStateItem instance =
                 ObjectPool.obtain(TransferSplashScreenViewStateItem.class);
         if (instance == null) {
             instance = new TransferSplashScreenViewStateItem();
         }
-        instance.mRequest = state;
         instance.mSplashScreenViewParcelable = parcelable;
+        instance.mStartingWindowLeash = startingWindowLeash;
 
         return instance;
     }
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index 8aa2785..dd147cc 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -552,6 +552,10 @@
                 inflateAsync(rvToApply);
                 return;
             }
+            // Prepare a local reference to the remote Context so we're ready to
+            // inflate any requested LayoutParams.
+            mRemoteContext = getRemoteContext();
+
             int layoutId = rvToApply.getLayoutId();
             if (rvToApply.canRecycleView(mView)) {
                 try {
@@ -727,6 +731,9 @@
         } catch (NameNotFoundException e) {
             Log.e(TAG, "Package name " +  mInfo.providerInfo.packageName + " not found");
             return mContext;
+        } catch (NullPointerException e) {
+            Log.e(TAG, "Error trying to create the remote context.", e);
+            return mContext;
         }
     }
 
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index 06ce053..6458acb 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -3052,9 +3052,6 @@
                 return true;
             }
             return false;
-        } else if (profile == BluetoothProfile.LE_AUDIO) {
-            BluetoothLeAudio leAudio = new BluetoothLeAudio(context, listener, this);
-            return true;
         } else if (profile == BluetoothProfile.VOLUME_CONTROL) {
             BluetoothVolumeControl vcs = new BluetoothVolumeControl(context, listener, this);
             return true;
@@ -3151,10 +3148,6 @@
                 BluetoothHearingAid hearingAid = (BluetoothHearingAid) proxy;
                 hearingAid.close();
                 break;
-            case BluetoothProfile.LE_AUDIO:
-                BluetoothLeAudio leAudio = (BluetoothLeAudio) proxy;
-                leAudio.close();
-                break;
             case BluetoothProfile.VOLUME_CONTROL:
                 BluetoothVolumeControl vcs = (BluetoothVolumeControl) proxy;
                 vcs.close();
diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java
index f49362e..2ecd71b 100644
--- a/core/java/android/content/ClipDescription.java
+++ b/core/java/android/content/ClipDescription.java
@@ -124,6 +124,16 @@
      */
     public static final String EXTRA_ACTIVITY_OPTIONS = "android.intent.extra.ACTIVITY_OPTIONS";
 
+    /**
+     * An instance id used for logging.
+     * <p>
+     * Type: {@link com.android.internal.logging.InstanceId}
+     * </p>
+     * @hide
+     */
+    public static final String EXTRA_LOGGING_INSTANCE_ID =
+            "android.intent.extra.LOGGING_INSTANCE_ID";
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value =
diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java
index dd2080b..2bac066 100644
--- a/core/java/android/content/pm/PackageItemInfo.java
+++ b/core/java/android/content/pm/PackageItemInfo.java
@@ -207,7 +207,9 @@
             return loadSafeLabel(pm, DEFAULT_MAX_LABEL_SIZE_PX, SAFE_STRING_FLAG_TRIM
                     | SAFE_STRING_FLAG_FIRST_LINE);
         } else {
-            return loadUnsafeLabel(pm);
+            // Trims the label string to the MAX_SAFE_LABEL_LENGTH. This is to prevent that the
+            // system is overwhelmed by an enormous string returned by the application.
+            return TextUtils.trimToSize(loadUnsafeLabel(pm), MAX_SAFE_LABEL_LENGTH);
         }
     }
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 2ed00b5..d4fa2d5 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3699,6 +3699,17 @@
     public static final String FEATURE_KEYSTORE_APP_ATTEST_KEY =
             "android.hardware.keystore.app_attest_key";
 
+    /**
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device
+     * is opted-in to receive per-app compatibility overrides that are applied in
+     * {@link com.android.server.compat.overrides.AppCompatOverridesService}.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_APP_COMPAT_OVERRIDES =
+            "android.software.app_compat_overrides";
+
     /** @hide */
     public static final boolean APP_ENUMERATION_ENABLED_BY_DEFAULT = true;
 
diff --git a/core/java/android/hardware/biometrics/BiometricOverlayConstants.java b/core/java/android/hardware/biometrics/BiometricOverlayConstants.java
new file mode 100644
index 0000000..603b06d
--- /dev/null
+++ b/core/java/android/hardware/biometrics/BiometricOverlayConstants.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.biometrics;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Common constants for biometric overlays.
+ * @hide
+ */
+public interface BiometricOverlayConstants {
+    /** Unknown usage. */
+    int REASON_UNKNOWN = 0;
+    /** User is about to enroll. */
+    int REASON_ENROLL_FIND_SENSOR = 1;
+    /** User is enrolling. */
+    int REASON_ENROLL_ENROLLING = 2;
+    /** Usage from BiometricPrompt. */
+    int REASON_AUTH_BP = 3;
+    /** Usage from Keyguard. */
+    int REASON_AUTH_KEYGUARD = 4;
+    /** Non-specific usage (from FingerprintManager). */
+    int REASON_AUTH_OTHER = 5;
+
+    @IntDef({REASON_UNKNOWN,
+            REASON_ENROLL_FIND_SENSOR,
+            REASON_ENROLL_ENROLLING,
+            REASON_AUTH_BP,
+            REASON_AUTH_KEYGUARD,
+            REASON_AUTH_OTHER})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ShowReason {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/core/java/android/hardware/biometrics/SensorLocationInternal.aidl
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to core/java/android/hardware/biometrics/SensorLocationInternal.aidl
index b391bd9..0981904 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/core/java/android/hardware/biometrics/SensorLocationInternal.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,14 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.hardware.biometrics;
 
-package com.android.systemui.statusbar.policy;
-
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
-}
+// @hide
+parcelable SensorLocationInternal;
diff --git a/core/java/android/hardware/biometrics/SensorLocationInternal.java b/core/java/android/hardware/biometrics/SensorLocationInternal.java
new file mode 100644
index 0000000..fb25a2f
--- /dev/null
+++ b/core/java/android/hardware/biometrics/SensorLocationInternal.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.biometrics;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The location of a sensor relative to a physical display.
+ *
+ * Note that the location may change depending on other attributes of the device, such as
+ * fold status, which are not yet included in this class.
+ * @hide
+ */
+public class SensorLocationInternal implements Parcelable {
+
+    /** Default value to use when the sensor's location is unknown or undefined. */
+    public static final SensorLocationInternal DEFAULT = new SensorLocationInternal("", 0, 0, 0);
+
+    /**
+     * The stable display id.
+     */
+    @NonNull
+    public final String displayId;
+
+    /**
+     * The location of the center of the sensor if applicable. For example, sensors of type
+     * {@link FingerprintSensorProperties#TYPE_UDFPS_OPTICAL} would report this value as the
+     * distance in pixels, measured from the left edge of the screen.
+     */
+    public final int sensorLocationX;
+
+    /**
+     * The location of the center of the sensor if applicable. For example, sensors of type
+     * {@link FingerprintSensorProperties#TYPE_UDFPS_OPTICAL} would report this value as the
+     * distance in pixels, measured from the top edge of the screen.
+     *
+     */
+    public final int sensorLocationY;
+
+    /**
+     * The radius of the sensor if applicable. For example, sensors of type
+     * {@link FingerprintSensorProperties#TYPE_UDFPS_OPTICAL} would report this value as the radius
+     * of the sensor, in pixels.
+     */
+    public final int sensorRadius;
+
+    public SensorLocationInternal(@Nullable String displayId,
+            int sensorLocationX, int sensorLocationY, int sensorRadius) {
+        this.displayId = displayId != null ? displayId : "";
+        this.sensorLocationX = sensorLocationX;
+        this.sensorLocationY = sensorLocationY;
+        this.sensorRadius = sensorRadius;
+    }
+
+    protected SensorLocationInternal(Parcel in) {
+        displayId = in.readString16NoHelper();
+        sensorLocationX = in.readInt();
+        sensorLocationY = in.readInt();
+        sensorRadius = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(displayId);
+        dest.writeInt(sensorLocationX);
+        dest.writeInt(sensorLocationY);
+        dest.writeInt(sensorRadius);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<SensorLocationInternal> CREATOR =
+            new Creator<SensorLocationInternal>() {
+        @Override
+        public SensorLocationInternal createFromParcel(Parcel in) {
+            return new SensorLocationInternal(in);
+        }
+
+        @Override
+        public SensorLocationInternal[] newArray(int size) {
+            return new SensorLocationInternal[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return "[id: " + displayId
+                + ", x: " + sensorLocationX
+                + ", y: " + sensorLocationY
+                + ", r: " + sensorRadius + "]";
+    }
+}
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index e0138c5..9f77a7e 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -22,15 +22,18 @@
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
+import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
 import android.hardware.camera2.params.RecommendedStreamConfigurationMap;
 import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.utils.TypeReference;
 import android.os.Build;
+import android.util.Log;
 import android.util.Rational;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -202,8 +205,25 @@
     private List<CaptureResult.Key<?>> mAvailableResultKeys;
     private ArrayList<RecommendedStreamConfigurationMap> mRecommendedConfigurations;
 
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private boolean mFoldedDeviceState;
+
+    private final CameraManager.DeviceStateListener mFoldStateListener =
+            new CameraManager.DeviceStateListener() {
+                @Override
+                public final void onDeviceStateChanged(boolean folded) {
+                    synchronized (mLock) {
+                        mFoldedDeviceState = folded;
+                    }
+                }};
+
+    private static final String TAG = "CameraCharacteristics";
+
     /**
      * Takes ownership of the passed-in properties object
+     *
+     * @param properties Camera properties.
      * @hide
      */
     public CameraCharacteristics(CameraMetadataNative properties) {
@@ -220,6 +240,42 @@
     }
 
     /**
+     * Return the device state listener for this Camera characteristics instance
+     */
+    CameraManager.DeviceStateListener getDeviceStateListener() { return mFoldStateListener; }
+
+    /**
+     * Overrides the property value
+     *
+     * <p>Check whether a given property value needs to be overridden in some specific
+     * case.</p>
+     *
+     * @param key The characteristics field to override.
+     * @return The value of overridden property, or {@code null} if the property doesn't need an
+     * override.
+     */
+    @Nullable
+    private <T> T overrideProperty(Key<T> key) {
+        if (CameraCharacteristics.SENSOR_ORIENTATION.equals(key) && (mFoldStateListener != null) &&
+                (mProperties.get(CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATIONS) != null)) {
+            DeviceStateSensorOrientationMap deviceStateSensorOrientationMap =
+                    mProperties.get(CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP);
+            synchronized (mLock) {
+                Integer ret = deviceStateSensorOrientationMap.getSensorOrientation(
+                        mFoldedDeviceState ? DeviceStateSensorOrientationMap.FOLDED :
+                                DeviceStateSensorOrientationMap.NORMAL);
+                if (ret >= 0) {
+                    return (T) ret;
+                } else {
+                    Log.w(TAG, "No valid device state to orientation mapping! Using default!");
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
      * Get a camera characteristics field value.
      *
      * <p>The field definitions can be
@@ -235,7 +291,8 @@
      */
     @Nullable
     public <T> T get(Key<T> key) {
-        return mProperties.get(key);
+        T propertyOverride = overrideProperty(key);
+        return (propertyOverride != null) ? propertyOverride : mProperties.get(key);
     }
 
     /**
@@ -3993,11 +4050,26 @@
      * upright on the device screen in its native orientation.</p>
      * <p>Also defines the direction of rolling shutter readout, which is from top to bottom in
      * the sensor's coordinate system.</p>
+     * <p>Starting with Android API level 32, camera clients that query the orientation via
+     * {@link android.hardware.camera2.CameraCharacteristics#get } on foldable devices which
+     * include logical cameras can receive a value that can dynamically change depending on the
+     * device/fold state.
+     * Clients are advised to not cache or store the orientation value of such logical sensors.
+     * In case repeated queries to CameraCharacteristics are not preferred, then clients can
+     * also access the entire mapping from device state to sensor orientation in
+     * {@link android.hardware.camera2.params.DeviceStateSensorOrientationMap }.
+     * Do note that a dynamically changing sensor orientation value in camera characteristics
+     * will not be the best way to establish the orientation per frame. Clients that want to
+     * know the sensor orientation of a particular captured frame should query the
+     * {@link CaptureResult#LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID android.logicalMultiCamera.activePhysicalId} from the corresponding capture result and
+     * check the respective physical camera orientation.</p>
      * <p><b>Units</b>: Degrees of clockwise rotation; always a multiple of
      * 90</p>
      * <p><b>Range of valid values:</b><br>
      * 0, 90, 180, 270</p>
      * <p>This key is available on all devices.</p>
+     *
+     * @see CaptureResult#LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID
      */
     @PublicKey
     @NonNull
@@ -4307,6 +4379,46 @@
             new Key<String>("android.info.version", String.class);
 
     /**
+     * <p>This lists the mapping between a device folding state and
+     * specific camera sensor orientation for logical cameras on a foldable device.</p>
+     * <p>Logical cameras on foldable devices can support sensors with different orientation
+     * values. The orientation value may need to change depending on the specific folding
+     * state. Information about the mapping between the device folding state and the
+     * sensor orientation can be obtained in
+     * {@link android.hardware.camera2.params.DeviceStateSensorOrientationMap }.
+     * Device state orientation maps are optional and maybe present on devices that support
+     * {@link CaptureRequest#SCALER_ROTATE_AND_CROP android.scaler.rotateAndCrop}.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_ROTATE_AND_CROP
+     */
+    @PublicKey
+    @NonNull
+    @SyntheticKey
+    public static final Key<android.hardware.camera2.params.DeviceStateSensorOrientationMap> INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP =
+            new Key<android.hardware.camera2.params.DeviceStateSensorOrientationMap>("android.info.deviceStateSensorOrientationMap", android.hardware.camera2.params.DeviceStateSensorOrientationMap.class);
+
+    /**
+     * <p>HAL must populate the array with
+     * (hardware::camera::provider::V2_5::DeviceState, sensorOrientation) pairs for each
+     * supported device state bitwise combination.</p>
+     * <p><b>Units</b>: (device fold state, sensor orientation) x n</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @hide
+     */
+    public static final Key<long[]> INFO_DEVICE_STATE_ORIENTATIONS =
+            new Key<long[]>("android.info.deviceStateOrientations", long[].class);
+
+    /**
      * <p>The maximum number of frames that can occur after a request
      * (different than the previous) has been submitted, and before the
      * result's state becomes synchronized.</p>
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 5833b3d..b7c5644 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -35,10 +35,13 @@
 import android.hardware.camera2.params.StreamConfiguration;
 import android.hardware.camera2.utils.CameraIdAndSessionConfiguration;
 import android.hardware.camera2.utils.ConcurrentCameraIdCombination;
+import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.DisplayManager;
 import android.os.Binder;
 import android.os.DeadObjectException;
 import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -50,6 +53,10 @@
 import android.util.Size;
 import android.view.Display;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -97,6 +104,90 @@
         synchronized(mLock) {
             mContext = context;
         }
+
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mFoldStateListener = new FoldStateListener(context);
+        try {
+            context.getSystemService(DeviceStateManager.class)
+                    .registerCallback(new HandlerExecutor(mHandler), mFoldStateListener);
+        } catch (IllegalStateException e) {
+            Log.v(TAG, "Failed to register device state listener!");
+            Log.v(TAG, "Device state dependent characteristics updates will not be functional!");
+            mHandlerThread.quitSafely();
+            mHandler = null;
+            mFoldStateListener = null;
+        }
+    }
+
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private FoldStateListener mFoldStateListener;
+    @GuardedBy("mLock")
+    private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>();
+    private boolean mFoldedDeviceState;
+
+    /**
+     * @hide
+     */
+    public interface DeviceStateListener {
+        void onDeviceStateChanged(boolean folded);
+    }
+
+    private final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+        private final int[] mFoldedDeviceStates;
+
+        public FoldStateListener(Context context) {
+            mFoldedDeviceStates = context.getResources().getIntArray(
+                    com.android.internal.R.array.config_foldedDeviceStates);
+        }
+
+        private void handleStateChange(int state) {
+            boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
+            synchronized (mLock) {
+                mFoldedDeviceState = folded;
+                ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
+                for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
+                    DeviceStateListener callback = listener.get();
+                    if (callback != null) {
+                        callback.onDeviceStateChanged(folded);
+                    } else {
+                        invalidListeners.add(listener);
+                    }
+                }
+                if (!invalidListeners.isEmpty()) {
+                    mDeviceStateListeners.removeAll(invalidListeners);
+                }
+            }
+        }
+
+        @Override
+        public final void onBaseStateChanged(int state) {
+            handleStateChange(state);
+        }
+
+        @Override
+        public final void onStateChanged(int state) {
+            handleStateChange(state);
+        }
+    }
+
+    /**
+     * Register a {@link CameraCharacteristics} device state listener
+     *
+     * @param chars Camera characteristics that need to receive device state updates
+     *
+     * @hide
+     */
+    public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) {
+        synchronized (mLock) {
+            DeviceStateListener listener = chars.getDeviceStateListener();
+            listener.onDeviceStateChanged(mFoldedDeviceState);
+            if (mFoldStateListener != null) {
+                mDeviceStateListeners.add(new WeakReference<>(listener));
+            }
+        }
     }
 
     /**
@@ -504,6 +595,7 @@
                         "Camera service is currently unavailable", e);
             }
         }
+        registerDeviceStateListener(characteristics);
         return characteristics;
     }
 
@@ -1327,8 +1419,7 @@
         private ICameraService mCameraService;
 
         // Singleton, don't allow construction
-        private CameraManagerGlobal() {
-        }
+        private CameraManagerGlobal() { }
 
         public static final boolean sCameraServiceDisabled =
                 SystemProperties.getBoolean("config.disable_cameraservice", false);
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 196134b..e393a66 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -50,10 +50,10 @@
 import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfigurationDuration;
 import android.hardware.camera2.marshal.impl.MarshalQueryableString;
 import android.hardware.camera2.params.Capability;
+import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
 import android.hardware.camera2.params.Face;
 import android.hardware.camera2.params.HighSpeedVideoConfiguration;
 import android.hardware.camera2.params.LensShadingMap;
-import android.hardware.camera2.params.MeteringRectangle;
 import android.hardware.camera2.params.MandatoryStreamCombination;
 import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
 import android.hardware.camera2.params.OisSample;
@@ -754,7 +754,7 @@
                 });
         sGetCommandMap.put(
                 CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP.getNativeKey(),
-                        new GetCommand() {
+                new GetCommand() {
                     @Override
                     @SuppressWarnings("unchecked")
                     public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
@@ -762,6 +762,15 @@
                     }
                 });
         sGetCommandMap.put(
+                CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP.getNativeKey(),
+                        new GetCommand() {
+                    @Override
+                    @SuppressWarnings("unchecked")
+                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
+                        return (T) metadata.getDeviceStateOrientationMap();
+                    }
+                });
+        sGetCommandMap.put(
                 CaptureResult.STATISTICS_OIS_SAMPLES.getNativeKey(),
                         new GetCommand() {
                     @Override
@@ -994,6 +1003,18 @@
         return map;
     }
 
+    private DeviceStateSensorOrientationMap getDeviceStateOrientationMap() {
+        long[] mapArray = getBase(CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATIONS);
+
+        // Do not warn if map is null while s is not. This is valid.
+        if (mapArray == null) {
+            return null;
+        }
+
+        DeviceStateSensorOrientationMap map = new DeviceStateSensorOrientationMap(mapArray);
+        return map;
+    }
+
     private Location getGpsLocation() {
         String processingMethod = get(CaptureResult.JPEG_GPS_PROCESSING_METHOD);
         double[] coords = get(CaptureResult.JPEG_GPS_COORDINATES);
diff --git a/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
new file mode 100644
index 0000000..200409e
--- /dev/null
+++ b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.camera2.params;
+
+import android.annotation.LongDef;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.utils.HashCodeHelpers;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * Immutable class that maps the device fold state to sensor orientation.
+ *
+ * <p>Some {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA logical}
+ * cameras on foldables can include physical sensors with different sensor orientation
+ * values. As a result, the values of the logical camera device can potentially change depending
+ * on the device fold state.</p>
+ *
+ * <p>The device fold state to sensor orientation map will contain information about the
+ * respective logical camera sensor orientation given a device state. Clients
+ * can query the mapping for all possible supported folded states.
+ *
+ * @see CameraCharacteristics#SENSOR_ORIENTATION
+ */
+public final class DeviceStateSensorOrientationMap {
+    /**
+     *  Needs to be kept in sync with the HIDL/AIDL DeviceState
+     */
+
+    /**
+     * The device is in its normal physical configuration. This is the default if the
+     * device does not support multiple different states.
+     */
+    public static final long NORMAL = 0;
+
+    /**
+     * The device is folded.  If not set, the device is unfolded or does not
+     * support folding.
+     *
+     * The exact point when this status change happens during the folding
+     * operation is device-specific.
+     */
+    public static final long FOLDED = 1 << 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(prefix = {"DEVICE_STATE"}, value =
+            {NORMAL,
+             FOLDED })
+    public @interface DeviceState {};
+
+    private final HashMap<Long, Integer> mDeviceStateOrientationMap = new HashMap<>();
+
+    /**
+     * Create a new immutable DeviceStateOrientationMap instance.
+     *
+     * <p>This constructor takes over the array; do not write to the array afterwards.</p>
+     *
+     * @param elements
+     *          An array of elements describing the map
+     *
+     * @throws IllegalArgumentException
+     *            if the {@code elements} array length is invalid, not divisible by 2 or contains
+     *            invalid element values
+     * @throws NullPointerException
+     *            if {@code elements} is {@code null}
+     *
+     * @hide
+     */
+    public DeviceStateSensorOrientationMap(final long[] elements) {
+        mElements = Objects.requireNonNull(elements, "elements must not be null");
+        if ((elements.length % 2) != 0) {
+            throw new IllegalArgumentException("Device state sensor orientation map length " +
+                    elements.length + " is not even!");
+        }
+
+        for (int i = 0; i < elements.length; i += 2) {
+            if ((elements[i+1] % 90) != 0) {
+                throw new IllegalArgumentException("Sensor orientation not divisible by 90: " +
+                        elements[i+1]);
+            }
+
+            mDeviceStateOrientationMap.put(elements[i], Math.toIntExact(elements[i + 1]));
+        }
+    }
+
+    /**
+     * Return the logical camera sensor orientation given a specific device fold state.
+     *
+     * @param deviceState Device fold state
+     *
+     * @return Valid {@link android.hardware.camera2.CameraCharacteristics#SENSOR_ORIENTATION} for
+     *         any supported device fold state
+     *
+     * @throws IllegalArgumentException if the given device state is invalid
+     */
+    public int getSensorOrientation(@DeviceState long deviceState) {
+        if (!mDeviceStateOrientationMap.containsKey(deviceState)) {
+            throw new IllegalArgumentException("Invalid device state: " + deviceState);
+        }
+
+        return mDeviceStateOrientationMap.get(deviceState);
+    }
+
+    /**
+     * Check if this DeviceStateSensorOrientationMap is equal to another
+     * DeviceStateSensorOrientationMap.
+     *
+     * <p>Two device state orientation maps are equal if and only if all of their elements are
+     * {@link Object#equals equal}.</p>
+     *
+     * @return {@code true} if the objects were equal, {@code false} otherwise
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof DeviceStateSensorOrientationMap) {
+            final DeviceStateSensorOrientationMap other = (DeviceStateSensorOrientationMap) obj;
+            return Arrays.equals(mElements, other.mElements);
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return HashCodeHelpers.hashCodeGeneric(mElements);
+    }
+
+    private final long[] mElements;
+}
diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java
index 52dad3e..95892aa 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManager.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManager.java
@@ -75,23 +75,24 @@
     /**
      * Submits a {@link DeviceStateRequest request} to modify the device state.
      * <p>
-     * By default, the request is kept active until a call to
-     * {@link #cancelRequest(DeviceStateRequest)} or until one of the following occurs:
+     * By default, the request is kept active until one of the following occurs:
      * <ul>
+     *     <li>The system deems the request can no longer be honored, for example if the requested
+     *     state becomes unsupported.
+     *     <li>A call to {@link #cancelRequest(DeviceStateRequest)}.
      *     <li>Another processes submits a request succeeding this request in which case the request
      *     will be suspended until the interrupting request is canceled.
-     *     <li>The requested state has become unsupported.
-     *     <li>The process submitting the request dies.
      * </ul>
      * However, this behavior can be changed by setting flags on the {@link DeviceStateRequest}.
      *
      * @throws IllegalArgumentException if the requested state is unsupported.
-     * @throws SecurityException if the {@link android.Manifest.permission#CONTROL_DEVICE_STATE}
-     * permission is not held.
+     * @throws SecurityException if the caller is neither the current top-focused activity nor if
+     * the {@link android.Manifest.permission#CONTROL_DEVICE_STATE} permission is held.
      *
      * @see DeviceStateRequest
      */
-    @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)
+    @RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE,
+            conditional = true)
     public void requestState(@NonNull DeviceStateRequest request,
             @Nullable @CallbackExecutor Executor executor,
             @Nullable DeviceStateRequest.Callback callback) {
@@ -105,10 +106,11 @@
      * This method is noop if the {@code request} has not been submitted with a call to
      * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}.
      *
-     * @throws SecurityException if the {@link android.Manifest.permission#CONTROL_DEVICE_STATE}
-     * permission is not held.
+     * @throws SecurityException if the caller is neither the current top-focused activity nor if
+     * the {@link android.Manifest.permission#CONTROL_DEVICE_STATE} permission is held.
      */
-    @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)
+    @RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE,
+            conditional = true)
     public void cancelRequest(@NonNull DeviceStateRequest request) {
         mGlobal.cancelRequest(request);
     }
diff --git a/core/java/android/hardware/devicestate/DeviceStateManagerInternal.java b/core/java/android/hardware/devicestate/DeviceStateManagerInternal.java
new file mode 100644
index 0000000..4c91c16
--- /dev/null
+++ b/core/java/android/hardware/devicestate/DeviceStateManagerInternal.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.devicestate;
+
+/**
+ * Device state manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class DeviceStateManagerInternal {
+
+    /** Returns the list of currently supported device state identifiers. */
+    public abstract int[] getSupportedStateIdentifiers();
+}
diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
index a1f7aa1..2b52e96 100644
--- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java
+++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
@@ -24,6 +24,7 @@
 import android.text.TextUtils;
 
 import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
 
 /**
  * AmbientDisplayConfiguration encapsulates reading access to the configuration of ambient display.
@@ -32,7 +33,7 @@
  */
 @TestApi
 public class AmbientDisplayConfiguration {
-
+    private static final String TAG = "AmbientDisplayConfig";
     private final Context mContext;
     private final boolean mAlwaysOnByDefault;
 
@@ -87,7 +88,12 @@
 
     /** {@hide} */
     public boolean tapSensorAvailable() {
-        return !TextUtils.isEmpty(tapSensorType());
+        for (String tapType : tapSensorTypeMapping()) {
+            if (!TextUtils.isEmpty(tapType)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /** {@hide} */
@@ -140,9 +146,18 @@
         return mContext.getResources().getString(R.string.config_dozeDoubleTapSensorType);
     }
 
-    /** {@hide} */
-    public String tapSensorType() {
-        return mContext.getResources().getString(R.string.config_dozeTapSensorType);
+    /** {@hide}
+     * May support multiple postures.
+     */
+    public String[] tapSensorTypeMapping() {
+        String[] postureMapping =
+                mContext.getResources().getStringArray(R.array.config_dozeTapSensorPostureMapping);
+        if (ArrayUtils.isEmpty(postureMapping)) {
+            return new String[] {
+                    mContext.getResources().getString(R.string.config_dozeTapSensorType)
+            };
+        }
+        return postureMapping;
     }
 
     /** {@hide} */
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index fc8337a..d8f2039 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -864,7 +864,8 @@
         if (surface != null) {
             builder.setSurface(surface);
         }
-        return createVirtualDisplay(null /* projection */, builder.build(), callback, handler);
+        return createVirtualDisplay(null /* projection */, builder.build(), callback, handler,
+                null /* windowContext */);
     }
 
     // TODO : Remove this hidden API after remove all callers. (Refer to MultiDisplayService)
@@ -882,15 +883,17 @@
         if (surface != null) {
             builder.setSurface(surface);
         }
-        return createVirtualDisplay(projection, builder.build(), callback, handler);
+        return createVirtualDisplay(projection, builder.build(), callback, handler,
+                null /* windowContext */);
     }
 
     /** @hide */
     public VirtualDisplay createVirtualDisplay(@Nullable MediaProjection projection,
             @NonNull VirtualDisplayConfig virtualDisplayConfig,
-            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
+            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler,
+            @Nullable Context windowContext) {
         return mGlobal.createVirtualDisplay(mContext, projection, virtualDisplayConfig, callback,
-                handler);
+                handler, windowContext);
     }
 
     /**
@@ -940,6 +943,34 @@
     }
 
     /**
+     * Sets the brightness configuration for the specified display.
+     * If the specified display doesn't exist, then this will return and do nothing.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
+    public void setBrightnessConfigurationForDisplay(@NonNull BrightnessConfiguration c,
+            @NonNull String uniqueId) {
+        mGlobal.setBrightnessConfigurationForDisplay(c, uniqueId, mContext.getUserId(),
+                mContext.getPackageName());
+    }
+
+    /**
+     * Gets the brightness configuration for the specified display and default user.
+     * Returns the default configuration if unset or display is invalid.
+     *
+     * @hide
+     */
+    @Nullable
+    @SystemApi
+    @RequiresPermission(Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
+    public BrightnessConfiguration getBrightnessConfigurationForDisplay(
+            @NonNull String uniqueId) {
+        return mGlobal.getBrightnessConfigurationForDisplay(uniqueId, mContext.getUserId());
+    }
+
+    /**
      * Sets the global display brightness configuration for a specific user.
      *
      * Note this requires the INTERACT_ACROSS_USERS permission if setting the configuration for a
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 6c39365..1fec3c9f 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -583,7 +583,7 @@
 
     public VirtualDisplay createVirtualDisplay(@NonNull Context context, MediaProjection projection,
             @NonNull VirtualDisplayConfig virtualDisplayConfig, VirtualDisplay.Callback callback,
-            Handler handler) {
+            Handler handler, @Nullable Context windowContext) {
         VirtualDisplayCallback callbackWrapper = new VirtualDisplayCallback(callback, handler);
         IMediaProjection projectionToken = projection != null ? projection.getProjection() : null;
         int displayId;
@@ -609,7 +609,7 @@
             return null;
         }
         return new VirtualDisplay(this, display, callbackWrapper,
-                virtualDisplayConfig.getSurface());
+                virtualDisplayConfig.getSurface(), windowContext);
     }
 
     public void setVirtualDisplaySurface(IVirtualDisplayCallback token, Surface surface) {
@@ -710,6 +710,34 @@
     }
 
     /**
+     * Sets the brightness configuration for a given display.
+     *
+     * @hide
+     */
+    public void setBrightnessConfigurationForDisplay(BrightnessConfiguration c,
+            String uniqueDisplayId, int userId, String packageName) {
+        try {
+            mDm.setBrightnessConfigurationForDisplay(c, uniqueDisplayId, userId, packageName);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Gets the brightness configuration for a given display or null if one hasn't been set.
+     *
+     * @hide
+     */
+    public BrightnessConfiguration getBrightnessConfigurationForDisplay(String uniqueDisplayId,
+            int userId) {
+        try {
+            return mDm.getBrightnessConfigurationForDisplay(uniqueDisplayId, userId);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Gets the global brightness configuration for a given user or null if one hasn't been set.
      *
      * @hide
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index abcc33c..5bb51c1 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -21,6 +21,7 @@
 import android.graphics.Point;
 import android.hardware.SensorManager;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.PowerManager;
 import android.util.IntArray;
 import android.util.Slog;
@@ -34,6 +35,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Display manager local system service interface.
@@ -128,6 +130,14 @@
     public abstract DisplayInfo getDisplayInfo(int displayId);
 
     /**
+     * Returns a set of DisplayInfo, for the states that may be assumed by either the given display,
+     * or any other display within that display's group.
+     *
+     * @param displayId The logical display id to fetch DisplayInfo for.
+     */
+    public abstract Set<DisplayInfo> getPossibleDisplayInfo(int displayId);
+
+    /**
      * Returns the position of the display's projection.
      *
      * @param displayId The logical display id.
@@ -340,6 +350,28 @@
     public abstract List<RefreshRateLimitation> getRefreshRateLimitations(int displayId);
 
     /**
+     * Returns the window token of the level of the WindowManager hierarchy to mirror. Returns null
+     * if layer mirroring by SurfaceFlinger should not be performed for the given displayId.
+     * For now, only used for mirroring started from MediaProjection.
+     */
+    public abstract IBinder getWindowTokenClientToMirror(int displayId);
+
+    /**
+     * For the given displayId, updates the window token of the level of the WindowManager hierarchy
+     * to mirror. If windowToken is null, then SurfaceFlinger performs no layer mirroring to the
+     * given display.
+     * For now, only used for mirroring started from MediaProjection.
+     */
+    public abstract void setWindowTokenClientToMirror(int displayId, IBinder windowToken);
+
+    /**
+     * Returns the default size of the surface associated with the display, or null if the surface
+     * is not provided for layer mirroring by SurfaceFlinger.
+     * For now, only used for mirroring started from MediaProjection.
+     */
+    public abstract Point getDisplaySurfaceDefaultSize(int displayId);
+
+    /**
      * Describes the requested power state of the display.
      *
      * This object is intended to describe the general characteristics of the
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 2303353..1162146 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -118,6 +118,16 @@
     void setBrightnessConfigurationForUser(in BrightnessConfiguration c, int userId,
             String packageName);
 
+    // Sets the global brightness configuration for a given display. Requires
+    // CONFIGURE_DISPLAY_BRIGHTNESS.
+    void setBrightnessConfigurationForDisplay(in BrightnessConfiguration c, String uniqueDisplayId,
+            int userId, String packageName);
+
+    // Gets the brightness configuration for a given display. Requires
+    // CONFIGURE_DISPLAY_BRIGHTNESS.
+    BrightnessConfiguration getBrightnessConfigurationForDisplay(String uniqueDisplayId,
+            int userId);
+
     // Gets the global brightness configuration for a given user. Requires
     // CONFIGURE_DISPLAY_BRIGHTNESS, and INTERACT_ACROSS_USER if the user is not
     // the same as the calling user.
diff --git a/core/java/android/hardware/display/VirtualDisplay.java b/core/java/android/hardware/display/VirtualDisplay.java
index bf62c95..71cbd20 100644
--- a/core/java/android/hardware/display/VirtualDisplay.java
+++ b/core/java/android/hardware/display/VirtualDisplay.java
@@ -15,6 +15,8 @@
  */
 package android.hardware.display;
 
+import android.annotation.Nullable;
+import android.content.Context;
 import android.view.Display;
 import android.view.Surface;
 
@@ -36,13 +38,19 @@
     private final Display mDisplay;
     private IVirtualDisplayCallback mToken;
     private Surface mSurface;
+    /**
+     * Store the WindowContext in a field. If it is a local variable, and it is garbage collected
+     * during a MediaProjection session, the WindowContainer listener no longer exists.
+     */
+    @Nullable private final Context mWindowContext;
 
     VirtualDisplay(DisplayManagerGlobal global, Display display,
-            IVirtualDisplayCallback token, Surface surface) {
+            IVirtualDisplayCallback token, Surface surface, Context windowContext) {
         mGlobal = global;
         mDisplay = display;
         mToken = token;
         mSurface = surface;
+        mWindowContext = windowContext;
     }
 
     /**
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index 71688c7c..0e86f43 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.media.projection.MediaProjection;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.Surface;
@@ -91,9 +92,16 @@
      */
     private int mDisplayIdToMirror = DEFAULT_DISPLAY;
 
+    /**
+     * The window token of the level of the WindowManager hierarchy to mirror, or null if mirroring
+     * should not be performed.
+     */
+    @Nullable
+    private IBinder mWindowTokenClientToMirror = null;
 
 
-    // Code below generated by codegen v1.0.20.
+
+    // Code below generated by codegen v1.0.23.
     //
     // DO NOT MODIFY!
     // CHECKSTYLE:OFF Generated code
@@ -115,7 +123,8 @@
             int flags,
             @Nullable Surface surface,
             @Nullable String uniqueId,
-            int displayIdToMirror) {
+            int displayIdToMirror,
+            @Nullable IBinder windowTokenClientToMirror) {
         this.mName = name;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mName);
@@ -135,6 +144,7 @@
         this.mSurface = surface;
         this.mUniqueId = uniqueId;
         this.mDisplayIdToMirror = displayIdToMirror;
+        this.mWindowTokenClientToMirror = windowTokenClientToMirror;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -212,6 +222,15 @@
         return mDisplayIdToMirror;
     }
 
+    /**
+     * The window token of the level of the WindowManager hierarchy to mirror, or null if mirroring
+     * should not be performed.
+     */
+    @DataClass.Generated.Member
+    public @Nullable IBinder getWindowTokenClientToMirror() {
+        return mWindowTokenClientToMirror;
+    }
+
     @Override
     @DataClass.Generated.Member
     public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -221,6 +240,7 @@
         int flg = 0;
         if (mSurface != null) flg |= 0x20;
         if (mUniqueId != null) flg |= 0x40;
+        if (mWindowTokenClientToMirror != null) flg |= 0x100;
         dest.writeInt(flg);
         dest.writeString(mName);
         dest.writeInt(mWidth);
@@ -230,6 +250,7 @@
         if (mSurface != null) dest.writeTypedObject(mSurface, flags);
         if (mUniqueId != null) dest.writeString(mUniqueId);
         dest.writeInt(mDisplayIdToMirror);
+        if (mWindowTokenClientToMirror != null) dest.writeStrongBinder(mWindowTokenClientToMirror);
     }
 
     @Override
@@ -252,6 +273,7 @@
         Surface surface = (flg & 0x20) == 0 ? null : (Surface) in.readTypedObject(Surface.CREATOR);
         String uniqueId = (flg & 0x40) == 0 ? null : in.readString();
         int displayIdToMirror = in.readInt();
+        IBinder windowTokenClientToMirror = (flg & 0x100) == 0 ? null : (IBinder) in.readStrongBinder();
 
         this.mName = name;
         com.android.internal.util.AnnotationValidations.validate(
@@ -272,6 +294,7 @@
         this.mSurface = surface;
         this.mUniqueId = uniqueId;
         this.mDisplayIdToMirror = displayIdToMirror;
+        this.mWindowTokenClientToMirror = windowTokenClientToMirror;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -305,6 +328,7 @@
         private @Nullable Surface mSurface;
         private @Nullable String mUniqueId;
         private int mDisplayIdToMirror;
+        private @Nullable IBinder mWindowTokenClientToMirror;
 
         private long mBuilderFieldsSet = 0L;
 
@@ -439,10 +463,22 @@
             return this;
         }
 
+        /**
+         * The window token of the level of the WindowManager hierarchy to mirror, or null if mirroring
+         * should not be performed.
+         */
+        @DataClass.Generated.Member
+        public @NonNull Builder setWindowTokenClientToMirror(@NonNull IBinder value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x100;
+            mWindowTokenClientToMirror = value;
+            return this;
+        }
+
         /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull VirtualDisplayConfig build() {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x100; // Mark builder used
+            mBuilderFieldsSet |= 0x200; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x10) == 0) {
                 mFlags = 0;
@@ -456,6 +492,9 @@
             if ((mBuilderFieldsSet & 0x80) == 0) {
                 mDisplayIdToMirror = DEFAULT_DISPLAY;
             }
+            if ((mBuilderFieldsSet & 0x100) == 0) {
+                mWindowTokenClientToMirror = null;
+            }
             VirtualDisplayConfig o = new VirtualDisplayConfig(
                     mName,
                     mWidth,
@@ -464,12 +503,13 @@
                     mFlags,
                     mSurface,
                     mUniqueId,
-                    mDisplayIdToMirror);
+                    mDisplayIdToMirror,
+                    mWindowTokenClientToMirror);
             return o;
         }
 
         private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x100) != 0) {
+            if ((mBuilderFieldsSet & 0x200) != 0) {
                 throw new IllegalStateException(
                         "This Builder should not be reused. Use a new Builder instance instead");
             }
@@ -477,10 +517,10 @@
     }
 
     @DataClass.Generated(
-            time = 1604456298440L,
-            codegenVersion = "1.0.20",
+            time = 1620657851981L,
+            codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/hardware/display/VirtualDisplayConfig.java",
-            inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate  int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate  int mDisplayIdToMirror\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)")
+            inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate  int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate  int mDisplayIdToMirror\nprivate @android.annotation.Nullable android.os.IBinder mWindowTokenClientToMirror\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
index 45c6b29..4bf9a74 100644
--- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
@@ -21,7 +21,9 @@
 import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.hardware.biometrics.ComponentInfoInternal;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.biometrics.SensorPropertiesInternal;
 import android.os.Parcel;
@@ -38,34 +40,14 @@
      */
     public final @FingerprintSensorProperties.SensorType int sensorType;
 
-    /**
-     * The location of the center of the sensor if applicable. For example, sensors of type
-     * {@link FingerprintSensorProperties#TYPE_UDFPS_OPTICAL} would report this value as the
-     * distance in pixels, measured from the left edge of the screen.
-     */
-    public final int sensorLocationX;
-
-    /**
-     * The location of the center of the sensor if applicable. For example, sensors of type
-     * {@link FingerprintSensorProperties#TYPE_UDFPS_OPTICAL} would report this value as the
-     * distance in pixels, measured from the top edge of the screen.
-     *
-     */
-    public final int sensorLocationY;
-
-    /**
-     * The radius of the sensor if applicable. For example, sensors of type
-     * {@link FingerprintSensorProperties#TYPE_UDFPS_OPTICAL} would report this value as the radius
-     * of the sensor, in pixels.
-     */
-    public final int sensorRadius;
+    private final List<SensorLocationInternal> mSensorLocations;
 
     public FingerprintSensorPropertiesInternal(int sensorId,
             @SensorProperties.Strength int strength, int maxEnrollmentsPerUser,
             @NonNull List<ComponentInfoInternal> componentInfo,
             @FingerprintSensorProperties.SensorType int sensorType,
-            boolean resetLockoutRequiresHardwareAuthToken, int sensorLocationX, int sensorLocationY,
-            int sensorRadius) {
+            boolean resetLockoutRequiresHardwareAuthToken,
+            @NonNull List<SensorLocationInternal> sensorLocations) {
         // IBiometricsFingerprint@2.1 handles lockout in the framework, so the challenge is not
         // required as it can only be generated/attested/verified by TEE components.
         // IFingerprint@1.0 handles lockout below the HAL, but does not require a challenge. See
@@ -73,9 +55,7 @@
         super(sensorId, strength, maxEnrollmentsPerUser, componentInfo,
             resetLockoutRequiresHardwareAuthToken, false /* resetLockoutRequiresChallenge */);
         this.sensorType = sensorType;
-        this.sensorLocationX = sensorLocationX;
-        this.sensorLocationY = sensorLocationY;
-        this.sensorRadius = sensorRadius;
+        this.mSensorLocations = List.copyOf(sensorLocations);
     }
 
     /**
@@ -88,16 +68,15 @@
             boolean resetLockoutRequiresHardwareAuthToken) {
         // TODO(b/179175438): Value should be provided from the HAL
         this(sensorId, strength, maxEnrollmentsPerUser, componentInfo, sensorType,
-                resetLockoutRequiresHardwareAuthToken, 540 /* sensorLocationX */,
-                1636 /* sensorLocationY */, 130 /* sensorRadius */);
+                resetLockoutRequiresHardwareAuthToken, List.of(new SensorLocationInternal(
+                        "" /* displayId */,  540 /* sensorLocationX */, 1636 /* sensorLocationY */,
+                        130 /* sensorRadius */)));
     }
 
     protected FingerprintSensorPropertiesInternal(Parcel in) {
         super(in);
         sensorType = in.readInt();
-        sensorLocationX = in.readInt();
-        sensorLocationY = in.readInt();
-        sensorRadius = in.readInt();
+        mSensorLocations = in.createTypedArrayList(SensorLocationInternal.CREATOR);
     }
 
     public static final Creator<FingerprintSensorPropertiesInternal> CREATOR =
@@ -122,9 +101,7 @@
     public void writeToParcel(Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
         dest.writeInt(sensorType);
-        dest.writeInt(sensorLocationX);
-        dest.writeInt(sensorLocationY);
-        dest.writeInt(sensorRadius);
+        dest.writeTypedList(mSensorLocations);
     }
 
     public boolean isAnyUdfpsType() {
@@ -150,6 +127,44 @@
         }
     }
 
+    /**
+     * Get the default location.
+     *
+     * Use this method when the sensor's relationship to the displays on the device do not
+     * matter.
+     * @return
+     */
+    @NonNull
+    public SensorLocationInternal getLocation() {
+        final SensorLocationInternal location = getLocation("" /* displayId */);
+        return location != null ? location : SensorLocationInternal.DEFAULT;
+    }
+
+    /**
+     * Get the location of a sensor relative to a physical display layout.
+     *
+     * @param displayId stable display id
+     * @return location or null if none is specified
+     */
+    @Nullable
+    public SensorLocationInternal getLocation(String displayId) {
+        for (SensorLocationInternal location : mSensorLocations) {
+            if (location.displayId.equals(displayId)) {
+                return location;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets all locations relative to all supported display layouts.
+     * @return supported locations
+     */
+    @NonNull
+    public List<SensorLocationInternal> getAllLocations() {
+        return mSensorLocations;
+    }
+
     @Override
     public String toString() {
         return "ID: " + sensorId + ", Strength: " + sensorStrength + ", Type: " + sensorType;
diff --git a/core/java/android/hardware/fingerprint/ISidefpsController.aidl b/core/java/android/hardware/fingerprint/ISidefpsController.aidl
index 00f4004..684f18f 100644
--- a/core/java/android/hardware/fingerprint/ISidefpsController.aidl
+++ b/core/java/android/hardware/fingerprint/ISidefpsController.aidl
@@ -21,9 +21,9 @@
  */
 oneway interface ISidefpsController {
 
-    // Shows the overlay.
-    void show();
+    // Shows the overlay for the given sensor with a reason from BiometricOverlayConstants.
+    void show(int sensorId, int reason);
 
     // Hides the overlay.
-    void hide();
+    void hide(int sensorId);
 }
diff --git a/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl b/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
index f18360ff..648edda 100644
--- a/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
+++ b/core/java/android/hardware/fingerprint/IUdfpsOverlayController.aidl
@@ -22,14 +22,7 @@
  * @hide
  */
 oneway interface IUdfpsOverlayController {
-    const int REASON_UNKNOWN = 0;
-    const int REASON_ENROLL_FIND_SENSOR = 1;
-    const int REASON_ENROLL_ENROLLING = 2;
-    const int REASON_AUTH_BP = 3; // BiometricPrompt
-    const int REASON_AUTH_FPM_KEYGUARD = 4; // FingerprintManager usage from Keyguard
-    const int REASON_AUTH_FPM_OTHER = 5; // Other FingerprintManager usage
-
-    // Shows the overlay.
+    // Shows the overlay  for the given sensor with a reason from BiometricOverlayConstants.
     void showUdfpsOverlay(int sensorId, int reason, IUdfpsOverlayControllerCallback callback);
 
     // Hides the overlay.
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java
index 3cd13a2..f8f0970 100644
--- a/core/java/android/inputmethodservice/AbstractInputMethodService.java
+++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java
@@ -18,16 +18,23 @@
 
 import android.annotation.MainThread;
 import android.annotation.NonNull;
-import android.app.Service;
+import android.annotation.Nullable;
+import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.util.proto.ProtoOutputStream;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputContentInfo;
 import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodSession;
+import android.window.WindowProviderService;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -44,9 +51,22 @@
  * implement.  This base class takes care of reporting your InputMethod from
  * the service when clients bind to it, but provides no standard implementation
  * of the InputMethod interface itself.  Derived classes must implement that
- * interface.
+ * interface.</p>
+ *
+ * <p>After {@link android.os.Build.VERSION_CODES#S}, the maximum possible area to show the soft
+ * input may not be the entire screen. For example, some devices may support to show the soft input
+ * on only half of screen.</p>
+ *
+ * <p>In that case, moving the soft input from one half screen to another will trigger a
+ * {@link android.content.res.Resources} update to match the new {@link Configuration} and
+ * this {@link AbstractInputMethodService} may also receive a
+ * {@link #onConfigurationChanged(Configuration)} callback if there's notable configuration changes
+ * </p>
+ *
+ * @see android.content.ComponentCallbacks#onConfigurationChanged(Configuration)
+ * @see Context#isUiContext Context#isUiContext to see the concept of UI Context.
  */
-public abstract class AbstractInputMethodService extends Service
+public abstract class AbstractInputMethodService extends WindowProviderService
         implements KeyEvent.Callback {
     private InputMethod mInputMethod;
     
@@ -272,9 +292,33 @@
     public void notifyUserActionIfNecessary() {
     }
 
+    // TODO(b/149463653): remove it in T. We missed the API deadline in S.
     /** @hide */
     @Override
     public final boolean isUiContext() {
         return true;
     }
+
+    /** @hide */
+    @Override
+    public final int getWindowType() {
+        return WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+    }
+
+    /** @hide */
+    @Override
+    @Nullable
+    public final Bundle getWindowContextOptions() {
+        return super.getWindowContextOptions();
+    }
+
+    /** @hide */
+    @Override
+    public final int getInitialDisplayId() {
+        try {
+            return WindowManagerGlobal.getWindowManagerService().getImeDisplayId();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index 9198eb7..89612fe 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -170,8 +170,8 @@
             case DO_INITIALIZE_INTERNAL: {
                 SomeArgs args = (SomeArgs) msg.obj;
                 try {
-                    inputMethod.initializeInternal((IBinder) args.arg1, msg.arg1,
-                            (IInputMethodPrivilegedOperations) args.arg2, (int) args.arg3);
+                    inputMethod.initializeInternal((IBinder) args.arg1,
+                            (IInputMethodPrivilegedOperations) args.arg2, msg.arg1);
                 } finally {
                     args.recycle();
                 }
@@ -279,11 +279,10 @@
 
     @BinderThread
     @Override
-    public void initializeInternal(IBinder token, int displayId,
-            IInputMethodPrivilegedOperations privOps, int configChanges) {
+    public void initializeInternal(IBinder token, IInputMethodPrivilegedOperations privOps,
+            int configChanges) {
         mCaller.executeOrSendMessage(
-                mCaller.obtainMessageIOOO(DO_INITIALIZE_INTERNAL, displayId, token, privOps,
-                        configChanges));
+                mCaller.obtainMessageIOO(DO_INITIALIZE_INTERNAL, configChanges, token, privOps));
     }
 
     @BinderThread
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 74cb42d..9721279 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -589,7 +589,7 @@
          */
         @MainThread
         @Override
-        public final void initializeInternal(@NonNull IBinder token, int displayId,
+        public final void initializeInternal(@NonNull IBinder token,
                 IInputMethodPrivilegedOperations privilegedOperations, int configChanges) {
             if (InputMethodPrivilegedOperationsRegistry.isRegistered(token)) {
                 Log.w(TAG, "The token has already registered, ignore this initialization.");
@@ -599,7 +599,6 @@
             mConfigTracker.onInitialize(configChanges);
             mPrivOps.set(privilegedOperations);
             InputMethodPrivilegedOperationsRegistry.put(token, mPrivOps);
-            updateInputMethodDisplay(displayId);
             attachToken(token);
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         }
@@ -629,29 +628,13 @@
                 throw new IllegalStateException(
                         "attachToken() must be called at most once. token=" + token);
             }
+            attachToWindowToken(token);
             mToken = token;
             mWindow.setToken(token);
         }
 
         /**
          * {@inheritDoc}
-         * @hide
-         */
-        @MainThread
-        @Override
-        public void updateInputMethodDisplay(int displayId) {
-            if (getDisplayId() == displayId) {
-                return;
-            }
-            // Update display for adding IME window to the right display.
-            // TODO(b/111364446) Need to address context lifecycle issue if need to re-create
-            // for update resources & configuration correctly when show soft input
-            // in non-default display.
-            updateDisplay(displayId);
-        }
-
-        /**
-         * {@inheritDoc}
          *
          * <p>Calls {@link InputMethodService#onBindInput()} when done.</p>
          */
@@ -807,11 +790,6 @@
                 return;
             }
 
-            if (Trace.isEnabled()) {
-                Binder.enableTracing();
-            } else {
-                Binder.disableTracing();
-            }
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.showSoftInput");
             ImeTracing.getInstance().triggerServiceDump(
                     "InputMethodService.InputMethodImpl#showSoftInput", InputMethodService.this,
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index fb99118..79ec554 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -2303,6 +2303,38 @@
     public abstract Timer getScreenBrightnessTimer(int brightnessBin);
 
     /**
+     * Returns the number of physical displays on the device.
+     *
+     * {@hide}
+     */
+    public abstract int getDisplayCount();
+
+    /**
+     * Returns the time in microseconds that the screen has been on for a display while the
+     * device was running on battery.
+     *
+     * {@hide}
+     */
+    public abstract long getDisplayScreenOnTime(int display, long elapsedRealtimeUs);
+
+    /**
+     * Returns the time in microseconds that a display has been dozing while the device was
+     * running on battery.
+     *
+     * {@hide}
+     */
+    public abstract long getDisplayScreenDozeTime(int display, long elapsedRealtimeUs);
+
+    /**
+     * Returns the time in microseconds that a display has been on with the given brightness
+     * level while the device was running on battery.
+     *
+     * {@hide}
+     */
+    public abstract long getDisplayScreenBrightnessTime(int display, int brightnessBin,
+            long elapsedRealtimeUs);
+
+    /**
      * Returns the time in microseconds that power save mode has been enabled while the device was
      * running on battery.
      *
@@ -5038,6 +5070,71 @@
             pw.println(sb.toString());
         }
 
+        final int numDisplays = getDisplayCount();
+        if (numDisplays > 1) {
+            pw.println("");
+            pw.print(prefix);
+            sb.setLength(0);
+            sb.append(prefix);
+            sb.append("  MULTI-DISPLAY POWER SUMMARY START");
+            pw.println(sb.toString());
+
+            for (int display = 0; display < numDisplays; display++) {
+                sb.setLength(0);
+                sb.append(prefix);
+                sb.append("  Display ");
+                sb.append(display);
+                sb.append(" Statistics:");
+                pw.println(sb.toString());
+
+                final long displayScreenOnTime = getDisplayScreenOnTime(display, rawRealtime);
+                sb.setLength(0);
+                sb.append(prefix);
+                sb.append("    Screen on: ");
+                formatTimeMs(sb, displayScreenOnTime / 1000);
+                sb.append("(");
+                sb.append(formatRatioLocked(displayScreenOnTime, whichBatteryRealtime));
+                sb.append(") ");
+                pw.println(sb.toString());
+
+                sb.setLength(0);
+                sb.append("    Screen brightness levels:");
+                didOne = false;
+                for (int bin = 0; bin < NUM_SCREEN_BRIGHTNESS_BINS; bin++) {
+                    final long timeUs = getDisplayScreenBrightnessTime(display, bin, rawRealtime);
+                    if (timeUs == 0) {
+                        continue;
+                    }
+                    didOne = true;
+                    sb.append("\n      ");
+                    sb.append(prefix);
+                    sb.append(SCREEN_BRIGHTNESS_NAMES[bin]);
+                    sb.append(" ");
+                    formatTimeMs(sb, timeUs / 1000);
+                    sb.append("(");
+                    sb.append(formatRatioLocked(timeUs, displayScreenOnTime));
+                    sb.append(")");
+                }
+                if (!didOne) sb.append(" (no activity)");
+                pw.println(sb.toString());
+
+                final long displayScreenDozeTimeUs = getDisplayScreenDozeTime(display, rawRealtime);
+                sb.setLength(0);
+                sb.append(prefix);
+                sb.append("    Screen Doze: ");
+                formatTimeMs(sb, displayScreenDozeTimeUs / 1000);
+                sb.append("(");
+                sb.append(formatRatioLocked(displayScreenDozeTimeUs, whichBatteryRealtime));
+                sb.append(") ");
+                pw.println(sb.toString());
+            }
+            pw.print(prefix);
+            sb.setLength(0);
+            sb.append(prefix);
+            sb.append("  MULTI-DISPLAY POWER SUMMARY END");
+            pw.println(sb.toString());
+        }
+
         pw.println("");
         pw.print(prefix);
         sb.setLength(0);
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 2ed0bad..0257408 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -189,13 +189,11 @@
         }
 
         @UnsupportedAppUsage
-        @Deprecated
         public File getExternalStorageDirectory() {
             return getExternalDirs()[0];
         }
 
         @UnsupportedAppUsage
-        @Deprecated
         public File getExternalStoragePublicDirectory(String type) {
             return buildExternalStoragePublicDirs(type)[0];
         }
@@ -695,14 +693,13 @@
      * <p>
      * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java
      * monitor_storage}
+     * <p>
+     * Note that alternatives such as {@link Context#getExternalFilesDir(String)} or
+     * {@link MediaStore} offer better performance.
      *
      * @see #getExternalStorageState()
      * @see #isExternalStorageRemovable()
-     * @deprecated Alternatives such as {@link Context#getExternalFilesDir(String)},
-     *             {@link MediaStore}, or {@link Intent#ACTION_OPEN_DOCUMENT} offer better
-     *             performance.
      */
-    @Deprecated
     public static File getExternalStorageDirectory() {
         throwIfUserRequired();
         return sCurrentUser.getExternalDirs()[0];
@@ -999,6 +996,9 @@
      * </p>
      * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java
      * public_picture}
+     * <p>
+     * Note that alternatives such as {@link Context#getExternalFilesDir(String)} or
+     * {@link MediaStore} offer better performance.
      *
      * @param type The type of storage directory to return. Should be one of
      *            {@link #DIRECTORY_MUSIC}, {@link #DIRECTORY_PODCASTS},
@@ -1009,11 +1009,7 @@
      * @return Returns the File path for the directory. Note that this directory
      *         may not yet exist, so you must make sure it exists before using
      *         it such as with {@link File#mkdirs File.mkdirs()}.
-     * @deprecated Alternatives such as {@link Context#getExternalFilesDir(String)},
-     *             {@link MediaStore}, or {@link Intent#ACTION_OPEN_DOCUMENT} offer better
-     *             performance.
      */
-    @Deprecated
     public static File getExternalStoragePublicDirectory(String type) {
         throwIfUserRequired();
         return sCurrentUser.buildExternalStoragePublicDirs(type)[0];
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 3aa0bcb..4dae7c7 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -548,6 +548,7 @@
             WAKE_REASON_HDMI,
             WAKE_REASON_DISPLAY_GROUP_ADDED,
             WAKE_REASON_DISPLAY_GROUP_TURNED_ON,
+            WAKE_REASON_UNFOLD_DEVICE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface WakeReason{}
@@ -647,6 +648,12 @@
     public static final int WAKE_REASON_DISPLAY_GROUP_TURNED_ON = 11;
 
     /**
+     * Wake up reason code: Waking the device due to unfolding of a foldable device.
+     * @hide
+     */
+    public static final int WAKE_REASON_UNFOLD_DEVICE = 12;
+
+    /**
      * Convert the wake reason to a string for debugging purposes.
      * @hide
      */
@@ -664,6 +671,7 @@
             case WAKE_REASON_LID: return "WAKE_REASON_LID";
             case WAKE_REASON_DISPLAY_GROUP_ADDED: return "WAKE_REASON_DISPLAY_GROUP_ADDED";
             case WAKE_REASON_DISPLAY_GROUP_TURNED_ON: return "WAKE_REASON_DISPLAY_GROUP_TURNED_ON";
+            case WAKE_REASON_UNFOLD_DEVICE: return "WAKE_REASON_UNFOLD_DEVICE";
             default: return Integer.toString(wakeReason);
         }
     }
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 25e3a4f..8de9d23 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -613,6 +613,14 @@
     @TestApi
     public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
 
+    /**
+     * Namespace for App Compat Overrides related features.
+     *
+     * @hide
+     */
+    @TestApi
+    public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
+
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
     private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners =
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 00abc75..1b6a03c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -28,6 +28,7 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
+import android.app.Activity;
 import android.app.ActivityThread;
 import android.app.AppOpsManager;
 import android.app.Application;
@@ -8979,6 +8980,15 @@
         public static final String UNSAFE_VOLUME_MUSIC_ACTIVE_MS = "unsafe_volume_music_active_ms";
 
         /**
+         * Indicates whether the spatial audio feature was enabled for this user.
+         *
+         * Type : int (0 disabled, 1 enabled)
+         *
+         * @hide
+         */
+        public static final String SPATIAL_AUDIO_ENABLED = "spatial_audio_enabled";
+
+        /**
          * Indicates whether notification display on the lock screen is enabled.
          * <p>
          * Type: int (0 for false, 1 for true)
@@ -9979,15 +9989,18 @@
 
         /**
          * Controls the accessibility button mode. System will force-set the value to {@link
-         * #ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU} if {@link #NAVIGATION_MODE} is fully
-         * gestural.
+         * #ACCESSIBILITY_BUTTON_MODE_GESTURE} if {@link #NAVIGATION_MODE} is button; force-set the
+         * value to {@link ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR} if {@link #NAVIGATION_MODE} is
+         * gestural; otherwise, remain the option.
          * <ul>
          *    <li> 0 = button in navigation bar </li>
          *    <li> 1 = button floating on the display </li>
+         *    <li> 2 = button using gesture to trigger </li>
          * </ul>
          *
          * @see #ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR
          * @see #ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU
+         * @see #ACCESSIBILITY_BUTTON_MODE_GESTURE
          * @hide
          */
         public static final String ACCESSIBILITY_BUTTON_MODE =
@@ -10010,6 +10023,14 @@
         public static final int ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU = 0x1;
 
         /**
+         * Accessibility button mode value that specifying the accessibility service or feature to
+         * be toggled via the gesture.
+         *
+         * @hide
+         */
+        public static final int ACCESSIBILITY_BUTTON_MODE_GESTURE = 0x2;
+
+        /**
          * The size of the accessibility floating menu.
          * <ul>
          *     <li> 0 = small size
@@ -10123,6 +10144,61 @@
         @Readable
         public static final String GAME_DASHBOARD_ALWAYS_ON = "game_dashboard_always_on";
 
+
+        /**
+         * For this device state, no specific auto-rotation lock setting should be applied.
+         * If the user toggles the auto-rotate lock in this state, the setting will apply to the
+         * previously valid device state.
+         * @hide
+         */
+        public static final int DEVICE_STATE_ROTATION_LOCK_IGNORED = 0;
+        /**
+         * For this device state, the setting for auto-rotation is locked.
+         * @hide
+         */
+        public static final int DEVICE_STATE_ROTATION_LOCK_LOCKED = 1;
+        /**
+         * For this device state, the setting for auto-rotation is unlocked.
+         * @hide
+         */
+        public static final int DEVICE_STATE_ROTATION_LOCK_UNLOCKED = 2;
+
+        /**
+         * The different settings that can be used as values with
+         * {@link #DEVICE_STATE_ROTATION_LOCK}.
+         * @hide
+         */
+        @IntDef(prefix = {"DEVICE_STATE_ROTATION_LOCK_"}, value = {
+                DEVICE_STATE_ROTATION_LOCK_IGNORED,
+                DEVICE_STATE_ROTATION_LOCK_LOCKED,
+                DEVICE_STATE_ROTATION_LOCK_UNLOCKED,
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        @interface DeviceStateRotationLockSetting {
+        }
+
+        /**
+         * Rotation lock setting keyed on device state.
+         *
+         * This holds a serialized map using int keys that represent Device States and value of
+         * {@link DeviceStateRotationLockSetting} representing the rotation lock setting for that
+         * device state.
+         *
+         * Serialized as key0:value0:key1:value1:...:keyN:valueN.
+         *
+         * Example: "0:1:1:2:2:1"
+         * This example represents a map of:
+         * <ul>
+         *     <li>0 -> DEVICE_STATE_ROTATION_LOCK_LOCKED</li>
+         *     <li>1 -> DEVICE_STATE_ROTATION_LOCK_UNLOCKED</li>
+         *     <li>2 -> DEVICE_STATE_ROTATION_LOCK_IGNORED</li>
+         * </ul>
+         *
+         * @hide
+         */
+        public static final String DEVICE_STATE_ROTATION_LOCK =
+                "device_state_rotation_lock";
+
         /**
          * These entries are considered common between the personal and the managed profile,
          * since the managed profile doesn't get to change them.
@@ -16890,6 +16966,44 @@
             "android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION";
 
     /**
+     * Activity Action: For system or preinstalled apps to show their {@link Activity} embedded
+     * in Settings app on large screen devices.
+     * <p>
+     *     Input: {@link #EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI} must be included to
+     * specify the intent for the activity which will be embedded in Settings app.
+     * It's an intent URI string from {@code intent.toUri(Intent.URI_INTENT_SCHEME)}.
+     *
+     *     Input: {@link #EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY} must be included to
+     * specify a key that indicates the menu item which will be highlighted on settings home menu.
+     * <p>
+     * Output: Nothing.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY =
+            "android.settings.SETTINGS_EMBED_DEEP_LINK_ACTIVITY";
+
+    /**
+     * Activity Extra: Specify the intent for the {@link Activity} which will be embedded in
+     * Settings app. It's an intent URI string from
+     * {@code intent.toUri(Intent.URI_INTENT_SCHEME)}.
+     * <p>
+     * This must be passed as an extra field to
+     * {@link #ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY}.
+     */
+    public static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI =
+            "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI";
+
+    /**
+     * Activity Extra: Specify a key that indicates the menu item which should be highlighted on
+     * settings home menu.
+     * <p>
+     * This must be passed as an extra field to
+     * {@link #ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY}.
+     */
+    public static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY =
+            "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY";
+
+    /**
      * Performs a strict and comprehensive check of whether a calling package is allowed to
      * write/modify system settings, as the condition differs for pre-M, M+, and
      * privileged/preinstalled apps. If the provided uid does not match the
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 71f90fd2..c945954 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -83,11 +83,11 @@
  *     &lt;/intent-filter>
  *     &lt;meta-data
  *               android:name="android.service.notification.default_filter_types"
- *               android:value="conversations,alerting">
+ *               android:value="conversations|alerting">
  *           &lt;/meta-data>
  *     &lt;meta-data
  *               android:name="android.service.notification.disabled_filter_types"
- *               android:value="ongoing,silent">
+ *               android:value="ongoing|silent">
  *           &lt;/meta-data>
  * &lt;/service></pre>
  *
@@ -112,8 +112,9 @@
     private final String TAG = getClass().getSimpleName();
 
     /**
-     * The name of the {@code meta-data} tag containing a comma separated list of default
-     * integer notification types that should be provided to this listener. See
+     * The name of the {@code meta-data} tag containing a pipe separated list of default
+     * integer notification types or "ongoing", "conversations", "alerting", or "silent"
+     * that should be provided to this listener. See
      * {@link #FLAG_FILTER_TYPE_ONGOING},
      * {@link #FLAG_FILTER_TYPE_CONVERSATIONS}, {@link #FLAG_FILTER_TYPE_ALERTING),
      * and {@link #FLAG_FILTER_TYPE_SILENT}.
@@ -1698,7 +1699,7 @@
         private ArrayList<Notification.Action> mSmartActions;
         private ArrayList<CharSequence> mSmartReplies;
         private boolean mCanBubble;
-        private boolean mVisuallyInterruptive;
+        private boolean mIsTextChanged;
         private boolean mIsConversation;
         private ShortcutInfo mShortcutInfo;
         private @RankingAdjustment int mRankingAdjustment;
@@ -1735,7 +1736,7 @@
             out.writeTypedList(mSmartActions, flags);
             out.writeCharSequenceList(mSmartReplies);
             out.writeBoolean(mCanBubble);
-            out.writeBoolean(mVisuallyInterruptive);
+            out.writeBoolean(mIsTextChanged);
             out.writeBoolean(mIsConversation);
             out.writeParcelable(mShortcutInfo, flags);
             out.writeInt(mRankingAdjustment);
@@ -1773,7 +1774,7 @@
             mSmartActions = in.createTypedArrayList(Notification.Action.CREATOR);
             mSmartReplies = in.readCharSequenceList();
             mCanBubble = in.readBoolean();
-            mVisuallyInterruptive = in.readBoolean();
+            mIsTextChanged = in.readBoolean();
             mIsConversation = in.readBoolean();
             mShortcutInfo = in.readParcelable(cl);
             mRankingAdjustment = in.readInt();
@@ -1976,8 +1977,8 @@
         }
 
         /** @hide */
-        public boolean visuallyInterruptive() {
-            return mVisuallyInterruptive;
+        public boolean isTextChanged() {
+            return mIsTextChanged;
         }
 
         /** @hide */
@@ -2032,7 +2033,7 @@
                 int userSentiment, boolean hidden, long lastAudiblyAlertedMs,
                 boolean noisy, ArrayList<Notification.Action> smartActions,
                 ArrayList<CharSequence> smartReplies, boolean canBubble,
-                boolean visuallyInterruptive, boolean isConversation, ShortcutInfo shortcutInfo,
+                boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo,
                 int rankingAdjustment, boolean isBubble) {
             mKey = key;
             mRank = rank;
@@ -2054,7 +2055,7 @@
             mSmartActions = smartActions;
             mSmartReplies = smartReplies;
             mCanBubble = canBubble;
-            mVisuallyInterruptive = visuallyInterruptive;
+            mIsTextChanged = isTextChanged;
             mIsConversation = isConversation;
             mShortcutInfo = shortcutInfo;
             mRankingAdjustment = rankingAdjustment;
@@ -2095,7 +2096,7 @@
                     other.mSmartActions,
                     other.mSmartReplies,
                     other.mCanBubble,
-                    other.mVisuallyInterruptive,
+                    other.mIsTextChanged,
                     other.mIsConversation,
                     other.mShortcutInfo,
                     other.mRankingAdjustment,
@@ -2152,7 +2153,7 @@
                         == (other.mSmartActions == null ? 0 : other.mSmartActions.size()))
                     && Objects.equals(mSmartReplies, other.mSmartReplies)
                     && Objects.equals(mCanBubble, other.mCanBubble)
-                    && Objects.equals(mVisuallyInterruptive, other.mVisuallyInterruptive)
+                    && Objects.equals(mIsTextChanged, other.mIsTextChanged)
                     && Objects.equals(mIsConversation, other.mIsConversation)
                     // Shortcutinfo doesn't have equals either; use id
                     &&  Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()),
diff --git a/core/java/android/service/voice/IVoiceInteractionSession.aidl b/core/java/android/service/voice/IVoiceInteractionSession.aidl
index c142a53..59f1e8e 100644
--- a/core/java/android/service/voice/IVoiceInteractionSession.aidl
+++ b/core/java/android/service/voice/IVoiceInteractionSession.aidl
@@ -22,6 +22,7 @@
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.service.voice.VisibleActivityInfo;
 
 import com.android.internal.app.IVoiceInteractionSessionShowCallback;
 
@@ -39,4 +40,5 @@
     void closeSystemDialogs();
     void onLockscreenShown();
     void destroy();
+    void updateVisibleActivityInfo(in VisibleActivityInfo visibleActivityInfo, int type);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/core/java/android/service/voice/VisibleActivityInfo.aidl
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to core/java/android/service/voice/VisibleActivityInfo.aidl
index b391bd9..34bd57c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/core/java/android/service/voice/VisibleActivityInfo.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package android.service.voice;
 
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
-}
+parcelable VisibleActivityInfo;
diff --git a/core/java/android/service/voice/VisibleActivityInfo.java b/core/java/android/service/voice/VisibleActivityInfo.java
new file mode 100644
index 0000000..139544c
--- /dev/null
+++ b/core/java/android/service/voice/VisibleActivityInfo.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.voice;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * The class is used to represent a visible activity information. The system provides this to
+ * services that need to know {@link android.service.voice.VoiceInteractionSession.ActivityId}.
+ */
+@DataClass(
+        genConstructor = false,
+        genEqualsHashCode = true,
+        genHiddenConstDefs = false,
+        genGetters = false,
+        genToString = true
+)
+public final class VisibleActivityInfo implements Parcelable {
+
+    /**
+     * Indicates that it is a new visible activity.
+     *
+     * @hide
+     */
+    public static final int TYPE_ACTIVITY_ADDED = 1;
+
+    /**
+     * Indicates that it has become a invisible activity.
+     *
+     * @hide
+     */
+    public static final int TYPE_ACTIVITY_REMOVED = 2;
+
+    /**
+     * The identifier of the task this activity is in.
+     */
+    private final int mTaskId;
+
+    /**
+     * Token for targeting this activity for assist purposes.
+     */
+    @NonNull
+    private final IBinder mAssistToken;
+
+    /** @hide */
+    @TestApi
+    public VisibleActivityInfo(
+            int taskId,
+            @NonNull IBinder assistToken) {
+        Objects.requireNonNull(assistToken);
+        mTaskId = taskId;
+        mAssistToken = assistToken;
+    }
+
+    /**
+     * Returns the {@link android.service.voice.VoiceInteractionSession.ActivityId} of this
+     * visible activity which can be used to interact with an activity, for example through
+     * {@link VoiceInteractionSession#requestDirectActions(VoiceInteractionSession.ActivityId,
+     * CancellationSignal, Executor, Consumer)}.
+     */
+    public @NonNull VoiceInteractionSession.ActivityId getActivityId() {
+        return new VoiceInteractionSession.ActivityId(mTaskId, mAssistToken);
+    }
+
+
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/voice/VisibleActivityInfo.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "VisibleActivityInfo { " +
+                "taskId = " + mTaskId + ", " +
+                "assistToken = " + mAssistToken +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(@Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(VisibleActivityInfo other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        VisibleActivityInfo that = (VisibleActivityInfo) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && mTaskId == that.mTaskId
+                && Objects.equals(mAssistToken, that.mAssistToken);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + mTaskId;
+        _hash = 31 * _hash + Objects.hashCode(mAssistToken);
+        return _hash;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        dest.writeInt(mTaskId);
+        dest.writeStrongBinder(mAssistToken);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ VisibleActivityInfo(@NonNull Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        int taskId = in.readInt();
+        IBinder assistToken = (IBinder) in.readStrongBinder();
+
+        this.mTaskId = taskId;
+        this.mAssistToken = assistToken;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mAssistToken);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<VisibleActivityInfo> CREATOR
+            = new Parcelable.Creator<VisibleActivityInfo>() {
+        @Override
+        public VisibleActivityInfo[] newArray(int size) {
+            return new VisibleActivityInfo[size];
+        }
+
+        @Override
+        public VisibleActivityInfo createFromParcel(@NonNull Parcel in) {
+            return new VisibleActivityInfo(in);
+        }
+    };
+
+    @DataClass.Generated(
+            time = 1632383555284L,
+            codegenVersion = "1.0.23",
+            sourceFile = "frameworks/base/core/java/android/service/voice/VisibleActivityInfo.java",
+            inputSignatures = "public static final  int TYPE_ACTIVITY_ADDED\npublic static final  int TYPE_ACTIVITY_REMOVED\nprivate final  int mTaskId\nprivate final @android.annotation.NonNull android.os.IBinder mAssistToken\npublic @android.annotation.NonNull android.service.voice.VoiceInteractionSession.ActivityId getActivityId()\nclass VisibleActivityInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genEqualsHashCode=true, genHiddenConstDefs=false, genGetters=false, genToString=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/core/java/android/service/voice/VoiceInteractionManagerInternal.java b/core/java/android/service/voice/VoiceInteractionManagerInternal.java
index c048286..c806409 100644
--- a/core/java/android/service/voice/VoiceInteractionManagerInternal.java
+++ b/core/java/android/service/voice/VoiceInteractionManagerInternal.java
@@ -22,7 +22,6 @@
 
 import com.android.internal.annotations.Immutable;
 
-
 /**
  * @hide
  * Private interface to the VoiceInteractionManagerService for use by ActivityManagerService.
diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java
index 725e20f..9db856a 100644
--- a/core/java/android/service/voice/VoiceInteractionSession.java
+++ b/core/java/android/service/voice/VoiceInteractionSession.java
@@ -74,9 +74,11 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -177,6 +179,10 @@
 
     ICancellationSignal mKillCallback;
 
+    private final Map<VisibleActivityCallback, Executor> mVisibleActivityCallbacks =
+            new ArrayMap<>();
+    private final List<VisibleActivityInfo> mVisibleActivityInfos = new ArrayList<>();
+
     final IVoiceInteractor mInteractor = new IVoiceInteractor.Stub() {
         @Override
         public IVoiceInteractorRequest startConfirmation(String callingPackage,
@@ -352,6 +358,13 @@
         public void destroy() {
             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_DESTROY));
         }
+
+        @Override
+        public void updateVisibleActivityInfo(VisibleActivityInfo visibleActivityInfo, int type) {
+            mHandlerCaller.sendMessage(
+                    mHandlerCaller.obtainMessageIO(MSG_UPDATE_VISIBLE_ACTIVITY_INFO, type,
+                            visibleActivityInfo));
+        }
     };
 
     /**
@@ -843,6 +856,9 @@
     static final int MSG_SHOW = 106;
     static final int MSG_HIDE = 107;
     static final int MSG_ON_LOCKSCREEN_SHOWN = 108;
+    static final int MSG_UPDATE_VISIBLE_ACTIVITY_INFO = 109;
+    static final int MSG_REGISTER_VISIBLE_ACTIVITY_CALLBACK = 110;
+    static final int MSG_UNREGISTER_VISIBLE_ACTIVITY_CALLBACK = 111;
 
     class MyCallbacks implements HandlerCaller.Callback, SoftInputWindow.Callback {
         @Override
@@ -928,6 +944,27 @@
                     if (DEBUG) Log.d(TAG, "onLockscreenShown");
                     onLockscreenShown();
                     break;
+                case MSG_UPDATE_VISIBLE_ACTIVITY_INFO:
+                    if (DEBUG) {
+                        Log.d(TAG, "doUpdateVisibleActivityInfo: visibleActivityInfo=" + msg.obj
+                                + " type=" + msg.arg1);
+                    }
+                    doUpdateVisibleActivityInfo((VisibleActivityInfo) msg.obj, msg.arg1);
+                    break;
+                case MSG_REGISTER_VISIBLE_ACTIVITY_CALLBACK:
+                    if (DEBUG) {
+                        Log.d(TAG, "doRegisterVisibleActivityCallback");
+                    }
+                    args = (SomeArgs) msg.obj;
+                    doRegisterVisibleActivityCallback((Executor) args.arg1,
+                            (VisibleActivityCallback) args.arg2);
+                    break;
+                case MSG_UNREGISTER_VISIBLE_ACTIVITY_CALLBACK:
+                    if (DEBUG) {
+                        Log.d(TAG, "doUnregisterVisibleActivityCallback");
+                    }
+                    doUnregisterVisibleActivityCallback((VisibleActivityCallback) msg.obj);
+                    break;
             }
             if (args != null) {
                 args.recycle();
@@ -1122,6 +1159,86 @@
         }
     }
 
+    private void doUpdateVisibleActivityInfo(VisibleActivityInfo visibleActivityInfo, int type) {
+
+        if (mVisibleActivityCallbacks.isEmpty()) {
+            return;
+        }
+
+        switch (type) {
+            case VisibleActivityInfo.TYPE_ACTIVITY_ADDED:
+                informVisibleActivityChanged(visibleActivityInfo, type);
+                mVisibleActivityInfos.add(visibleActivityInfo);
+                break;
+            case VisibleActivityInfo.TYPE_ACTIVITY_REMOVED:
+                informVisibleActivityChanged(visibleActivityInfo, type);
+                mVisibleActivityInfos.remove(visibleActivityInfo);
+                break;
+        }
+    }
+
+    private void doRegisterVisibleActivityCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull VisibleActivityCallback callback) {
+        if (mVisibleActivityCallbacks.containsKey(callback)) {
+            if (DEBUG) {
+                Log.d(TAG, "doRegisterVisibleActivityCallback: callback has registered");
+            }
+            return;
+        }
+
+        int preCallbackCount = mVisibleActivityCallbacks.size();
+        mVisibleActivityCallbacks.put(callback, executor);
+
+        if (preCallbackCount == 0) {
+            try {
+                mSystemService.startListeningVisibleActivityChanged(mToken);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        } else {
+            for (int i = 0; i < mVisibleActivityInfos.size(); i++) {
+                final VisibleActivityInfo visibleActivityInfo = mVisibleActivityInfos.get(i);
+                executor.execute(() -> callback.onVisible(visibleActivityInfo));
+            }
+        }
+    }
+
+    private void doUnregisterVisibleActivityCallback(@NonNull VisibleActivityCallback callback) {
+        mVisibleActivityCallbacks.remove(callback);
+
+        if (mVisibleActivityCallbacks.size() == 0) {
+            mVisibleActivityInfos.clear();
+            try {
+                mSystemService.stopListeningVisibleActivityChanged(mToken);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    private void informVisibleActivityChanged(VisibleActivityInfo visibleActivityInfo, int type) {
+        for (Map.Entry<VisibleActivityCallback, Executor> e :
+                mVisibleActivityCallbacks.entrySet()) {
+            final Executor executor = e.getValue();
+            final VisibleActivityCallback visibleActivityCallback = e.getKey();
+
+            switch (type) {
+                case VisibleActivityInfo.TYPE_ACTIVITY_ADDED:
+                    Binder.withCleanCallingIdentity(() -> {
+                        executor.execute(
+                                () -> visibleActivityCallback.onVisible(visibleActivityInfo));
+                    });
+                    break;
+                case VisibleActivityInfo.TYPE_ACTIVITY_REMOVED:
+                    Binder.withCleanCallingIdentity(() -> {
+                        executor.execute(() -> visibleActivityCallback.onInvisible(
+                                visibleActivityInfo.getActivityId()));
+                    });
+                    break;
+            }
+        }
+    }
+
     void ensureWindowCreated() {
         if (mInitialized) {
             return;
@@ -1926,6 +2043,45 @@
     }
 
     /**
+     * Registers a callback that will be notified when visible activities have been changed.
+     *
+     * @param executor The handler to receive the callback.
+     * @param callback The callback to receive the response.
+     *
+     * @throws IllegalStateException if calling this method before onCreate().
+     */
+    public final void registerVisibleActivityCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull VisibleActivityCallback callback) {
+        if (DEBUG) {
+            Log.d(TAG, "registerVisibleActivityCallback");
+        }
+        if (mToken == null) {
+            throw new IllegalStateException("Can't call before onCreate()");
+        }
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        mHandlerCaller.sendMessage(
+                mHandlerCaller.obtainMessageOO(MSG_REGISTER_VISIBLE_ACTIVITY_CALLBACK, executor,
+                        callback));
+    }
+
+    /**
+     * Unregisters the callback.
+     *
+     * @param callback The callback to receive the response.
+     */
+    public final void unregisterVisibleActivityCallback(@NonNull VisibleActivityCallback callback) {
+        if (DEBUG) {
+            Log.d(TAG, "unregisterVisibleActivityCallback");
+        }
+        Objects.requireNonNull(callback);
+
+        mHandlerCaller.sendMessage(
+                mHandlerCaller.obtainMessageO(MSG_UNREGISTER_VISIBLE_ACTIVITY_CALLBACK, callback));
+    }
+
+    /**
      * Print the Service's state into the given stream.  This gets invoked by
      * {@link VoiceInteractionSessionService} when its Service
      * {@link android.app.Service#dump} method is called.
@@ -1975,6 +2131,17 @@
     }
 
     /**
+     * Callback interface for receiving visible activity changes used for assistant usage.
+     */
+    public interface VisibleActivityCallback {
+        /** Callback to inform that an activity has become visible. */
+        default void onVisible(@NonNull VisibleActivityInfo activityInfo) {}
+
+        /** Callback to inform that a visible activity has gone. */
+        default void onInvisible(@NonNull ActivityId activityId) {}
+    }
+
+    /**
      * Represents assist state captured when this session was started.
      * It contains the various assist data objects and a reference to
      * the source activity.
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index c9a0121..2d263a5 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -16,6 +16,8 @@
 
 package android.service.wallpaper;
 
+import static android.app.WallpaperManager.COMMAND_FREEZE;
+import static android.app.WallpaperManager.COMMAND_UNFREEZE;
 import static android.graphics.Matrix.MSCALE_X;
 import static android.graphics.Matrix.MSCALE_Y;
 import static android.graphics.Matrix.MSKEW_X;
@@ -42,12 +44,14 @@
 import android.graphics.BLASTBufferQueue;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.GraphicBuffer;
 import android.graphics.Matrix;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
+import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.os.Build;
@@ -56,6 +60,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -73,6 +78,7 @@
 import android.view.InputEventReceiver;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.MotionEvent;
 import android.view.PixelCopy;
 import android.view.Surface;
@@ -201,6 +207,12 @@
         boolean mVisible;
         boolean mReportedVisible;
         boolean mDestroyed;
+        // Set to true after receiving WallpaperManager#COMMAND_FREEZE. It's reset back to false
+        // after receiving WallpaperManager#COMMAND_UNFREEZE. COMMAND_FREEZE is fully applied once
+        // mScreenshotSurfaceControl isn't null. When this happens, then Engine is notified through
+        // doVisibilityChanged that main wallpaper surface is no longer visible and the wallpaper
+        // host receives onVisibilityChanged(false) callback.
+        private boolean mFrozenRequested = false;
 
         // Current window state.
         boolean mCreated;
@@ -226,10 +238,9 @@
         final ClientWindowFrames mWinFrames = new ClientWindowFrames();
         final Rect mDispatchedContentInsets = new Rect();
         final Rect mDispatchedStableInsets = new Rect();
-        final Rect mFinalSystemInsets = new Rect();
-        final Rect mFinalStableInsets = new Rect();
         DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT;
         final InsetsState mInsetsState = new InsetsState();
+        final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
         final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0];
         final MergedConfiguration mMergedConfiguration = new MergedConfiguration();
         private final Point mSurfaceSize = new Point();
@@ -266,6 +277,8 @@
         SurfaceControl mSurfaceControl = new SurfaceControl();
         SurfaceControl mBbqSurfaceControl;
         BLASTBufferQueue mBlastBufferQueue;
+        private SurfaceControl mScreenshotSurfaceControl;
+        private Point mScreenshotSize = new Point();
 
         final BaseSurfaceHolder mSurfaceHolder = new BaseSurfaceHolder() {
             {
@@ -966,6 +979,7 @@
         void updateSurface(boolean forceRelayout, boolean forceReport, boolean redrawNeeded) {
             if (mDestroyed) {
                 Log.w(TAG, "Ignoring updateSurface due to destroyed");
+                return;
             }
 
             boolean fixedSize = false;
@@ -1032,8 +1046,8 @@
                         InputChannel inputChannel = new InputChannel();
 
                         if (mSession.addToDisplay(mWindow, mLayout, View.VISIBLE,
-                                mDisplay.getDisplayId(), mInsetsState, inputChannel, mInsetsState,
-                                mTempControls) < 0) {
+                                mDisplay.getDisplayId(), mRequestedVisibilities, inputChannel,
+                                mInsetsState, mTempControls) < 0) {
                             Log.w(TAG, "Failed to add window while updating wallpaper surface.");
                             return;
                         }
@@ -1383,11 +1397,15 @@
             if (!mDestroyed) {
                 mVisible = visible;
                 reportVisibility();
-                if (visible) processLocalColors(mPendingXOffset, mPendingXOffsetStep);
+                if (mReportedVisible) processLocalColors(mPendingXOffset, mPendingXOffsetStep);
             }
         }
 
         void reportVisibility() {
+            if (mScreenshotSurfaceControl != null && mVisible) {
+                if (DEBUG) Log.v(TAG, "Frozen so don't report visibility change");
+                return;
+            }
             if (!mDestroyed) {
                 mDisplayState = mDisplay == null ? Display.STATE_UNKNOWN : mDisplay.getState();
                 boolean visible = mVisible && mDisplayState != Display.STATE_OFF;
@@ -1404,6 +1422,10 @@
                         updateSurface(true, false, false);
                     }
                     onVisibilityChanged(visible);
+                    if (mReportedVisible && mFrozenRequested) {
+                        if (DEBUG) Log.v(TAG, "Freezing wallpaper after visibility update");
+                        freeze();
+                    }
                 }
             }
         }
@@ -1864,6 +1886,9 @@
         void doCommand(WallpaperCommand cmd) {
             Bundle result;
             if (!mDestroyed) {
+                if (COMMAND_FREEZE.equals(cmd.action) || COMMAND_UNFREEZE.equals(cmd.action)) {
+                    updateFrozenState(/* frozenRequested= */ !COMMAND_UNFREEZE.equals(cmd.action));
+                }
                 result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z,
                         cmd.extras, cmd.sync);
             } else {
@@ -1878,6 +1903,159 @@
             }
         }
 
+        private void updateFrozenState(boolean frozenRequested) {
+            if (mIWallpaperEngine.mWallpaperManager.getWallpaperInfo() == null
+                    // Procees the unfreeze command in case the wallaper became static while
+                    // being paused.
+                    && frozenRequested) {
+                if (DEBUG) Log.v(TAG, "Ignoring the freeze command for static wallpapers");
+                return;
+            }
+            mFrozenRequested = frozenRequested;
+            boolean isFrozen = mScreenshotSurfaceControl != null;
+            if (mFrozenRequested == isFrozen) {
+                return;
+            }
+            if (mFrozenRequested) {
+                freeze();
+            } else {
+                unfreeze();
+            }
+        }
+
+        private void freeze() {
+            if (!mReportedVisible || mDestroyed) {
+                // Screenshot can't be taken until visibility is reported to the wallpaper host.
+                return;
+            }
+            if (!showScreenshotOfWallpaper()) {
+                return;
+            }
+            // Prevent a wallpaper host from rendering wallpaper behind a screeshot.
+            doVisibilityChanged(false);
+            // Remember that visibility is requested since it's not guaranteed that
+            // mWindow#dispatchAppVisibility will be called when letterboxed application with
+            // wallpaper background transitions to the Home screen.
+            mVisible = true;
+        }
+
+        private void unfreeze() {
+            cleanUpScreenshotSurfaceControl();
+            if (mVisible) {
+                doVisibilityChanged(true);
+            }
+        }
+
+        private void cleanUpScreenshotSurfaceControl() {
+            // TODO(b/194399558): Add crossfade transition.
+            if (mScreenshotSurfaceControl != null) {
+                new SurfaceControl.Transaction()
+                        .remove(mScreenshotSurfaceControl)
+                        .show(mBbqSurfaceControl)
+                        .apply();
+                mScreenshotSurfaceControl = null;
+            }
+        }
+
+        void scaleAndCropScreenshot() {
+            if (mScreenshotSurfaceControl == null) {
+                return;
+            }
+            if (mScreenshotSize.x <= 0 || mScreenshotSize.y <= 0) {
+                Log.w(TAG, "Unexpected screenshot size: " + mScreenshotSize);
+                return;
+            }
+            // Don't scale down and using the same scaling factor for both dimensions to
+            // avoid stretching wallpaper image.
+            float scaleFactor = Math.max(1, Math.max(
+                    ((float) mSurfaceSize.x) / mScreenshotSize.x,
+                    ((float) mSurfaceSize.y) / mScreenshotSize.y));
+            int diffX =  ((int) (mScreenshotSize.x * scaleFactor)) - mSurfaceSize.x;
+            int diffY =  ((int) (mScreenshotSize.y * scaleFactor)) - mSurfaceSize.y;
+            if (DEBUG) {
+                Log.v(TAG, "Adjusting screenshot: scaleFactor=" + scaleFactor
+                        + " diffX=" + diffX + " diffY=" + diffY + " mSurfaceSize=" + mSurfaceSize
+                        + " mScreenshotSize=" + mScreenshotSize);
+            }
+            new SurfaceControl.Transaction()
+                        .setMatrix(
+                                mScreenshotSurfaceControl,
+                                /* dsdx= */ scaleFactor, /* dtdx= */ 0,
+                                /* dtdy= */ 0, /* dsdy= */ scaleFactor)
+                        .setWindowCrop(
+                                mScreenshotSurfaceControl,
+                                new Rect(
+                                        /* left= */ diffX / 2,
+                                        /* top= */ diffY / 2,
+                                        /* right= */ diffX / 2 + mScreenshotSize.x,
+                                        /* bottom= */ diffY / 2 + mScreenshotSize.y))
+                        .setPosition(mScreenshotSurfaceControl, -diffX / 2, -diffY / 2)
+                        .apply();
+        }
+
+        private boolean showScreenshotOfWallpaper() {
+            if (mDestroyed || mSurfaceControl == null || !mSurfaceControl.isValid()) {
+                if (DEBUG) Log.v(TAG, "Failed to screenshot wallpaper: surface isn't valid");
+                return false;
+            }
+
+            final Rect bounds = new Rect(0, 0, mSurfaceSize.x,  mSurfaceSize.y);
+            if (bounds.isEmpty()) {
+                Log.w(TAG, "Failed to screenshot wallpaper: surface bounds are empty");
+                return false;
+            }
+
+            if (mScreenshotSurfaceControl != null) {
+                Log.e(TAG, "Screenshot is unexpectedly not null");
+                // Destroying previous screenshot since it can have different size.
+                cleanUpScreenshotSurfaceControl();
+            }
+
+            SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
+                    SurfaceControl.captureLayers(
+                            new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl)
+                                    // Needed because SurfaceFlinger#validateScreenshotPermissions
+                                    // uses this parameter to check whether a caller only attempts
+                                    // to screenshot itself when call doesn't come from the system.
+                                    .setUid(Process.myUid())
+                                    .setChildrenOnly(false)
+                                    .setSourceCrop(bounds)
+                                    .build());
+
+            if (screenshotBuffer == null) {
+                Log.w(TAG, "Failed to screenshot wallpaper: screenshotBuffer is null");
+                return false;
+            }
+
+            final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
+
+            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+
+            // TODO(b/194399558): Add crossfade transition.
+            mScreenshotSurfaceControl = new SurfaceControl.Builder()
+                    .setName("Wallpaper snapshot for engine " + this)
+                    .setFormat(hardwareBuffer.getFormat())
+                    .setParent(mSurfaceControl)
+                    .setSecure(screenshotBuffer.containsSecureLayers())
+                    .setCallsite("WallpaperService.Engine.showScreenshotOfWallpaper")
+                    .setBLASTLayer()
+                    .build();
+
+            mScreenshotSize.set(mSurfaceSize.x, mSurfaceSize.y);
+
+            GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(hardwareBuffer);
+
+            t.setBuffer(mScreenshotSurfaceControl, graphicBuffer);
+            t.setColorSpace(mScreenshotSurfaceControl, screenshotBuffer.getColorSpace());
+            // Place on top everything else.
+            t.setLayer(mScreenshotSurfaceControl, Integer.MAX_VALUE);
+            t.show(mScreenshotSurfaceControl);
+            t.hide(mBbqSurfaceControl);
+            t.apply();
+
+            return true;
+        }
+
         void reportSurfaceDestroyed() {
             if (mSurfaceCreated) {
                 mSurfaceCreated = false;
@@ -2202,6 +2380,7 @@
                     final boolean reportDraw = message.arg1 != 0;
                     mEngine.updateSurface(true, false, reportDraw);
                     mEngine.doOffsetsChanged(true);
+                    mEngine.scaleAndCropScreenshot();
                 } break;
                 case MSG_WINDOW_MOVED: {
                     // Do nothing. What does it mean for a Wallpaper to move?
diff --git a/core/java/android/util/DisplayUtils.java b/core/java/android/util/DisplayUtils.java
new file mode 100644
index 0000000..4fe7f83
--- /dev/null
+++ b/core/java/android/util/DisplayUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.content.res.Resources;
+
+import com.android.internal.R;
+
+/**
+ * Utils for loading resources for multi-display.
+ *
+ * @hide
+ */
+public class DisplayUtils {
+
+    /**
+     * Gets the index of the given display unique id in {@link R.array#config_displayUniqueIdArray}
+     * which is used to get the related cutout configs for that display.
+     *
+     * For multi-display device, {@link R.array#config_displayUniqueIdArray} should be set for each
+     * display if there are different type of cutouts on each display.
+     * For single display device, {@link R.array#config_displayUniqueIdArray} should not to be set
+     * and the system will load the default configs for main built-in display.
+     */
+    public static int getDisplayUniqueIdConfigIndex(Resources res, String displayUniqueId) {
+        int index = -1;
+        if (displayUniqueId == null || displayUniqueId.isEmpty()) {
+            return index;
+        }
+        final String[] ids = res.getStringArray(R.array.config_displayUniqueIdArray);
+        final int size = ids.length;
+        for (int i = 0; i < size; i++) {
+            if (displayUniqueId.equals(ids[i])) {
+                index = i;
+                break;
+            }
+        }
+        return index;
+    }
+}
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 6c3c383..3d39fbe 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -51,6 +51,9 @@
     /** @hide */
     public static final String SETTINGS_ENABLE_SECURITY_HUB = "settings_enable_security_hub";
 
+    /** @hide */
+    public static final String SETTINGS_SUPPORT_LARGE_SCREEN = "settings_support_large_screen";
+
     private static final Map<String, String> DEFAULT_FLAGS;
 
     static {
@@ -72,12 +75,14 @@
         DEFAULT_FLAGS.put(SETTINGS_PROVIDER_MODEL, "true");
         DEFAULT_FLAGS.put(SETTINGS_USE_NEW_BACKUP_ELIGIBILITY_RULES, "true");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_SECURITY_HUB, "true");
+        DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "false");
     }
 
     private static final Set<String> PERSISTENT_FLAGS;
     static {
         PERSISTENT_FLAGS = new HashSet<>();
         PERSISTENT_FLAGS.add(SETTINGS_PROVIDER_MODEL);
+        PERSISTENT_FLAGS.add(SETTINGS_SUPPORT_LARGE_SCREEN);
     }
 
     /**
diff --git a/core/java/android/util/MathUtils.java b/core/java/android/util/MathUtils.java
index 971e161..aecde44 100644
--- a/core/java/android/util/MathUtils.java
+++ b/core/java/android/util/MathUtils.java
@@ -165,6 +165,10 @@
         return start + (stop - start) * amount;
     }
 
+    public static float lerp(int start, int stop, float amount) {
+        return lerp((float) start, (float) stop, amount);
+    }
+
     /**
      * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link
      * #lerp}{@code (a, b, s)}
diff --git a/core/java/android/util/imetracing/ImeTracingServerImpl.java b/core/java/android/util/imetracing/ImeTracingServerImpl.java
index 06e4c50..d605430 100644
--- a/core/java/android/util/imetracing/ImeTracingServerImpl.java
+++ b/core/java/android/util/imetracing/ImeTracingServerImpl.java
@@ -41,9 +41,9 @@
  */
 class ImeTracingServerImpl extends ImeTracing {
     private static final String TRACE_DIRNAME = "/data/misc/wmtrace/";
-    private static final String TRACE_FILENAME_CLIENTS = "ime_trace_clients.pb";
-    private static final String TRACE_FILENAME_IMS = "ime_trace_service.pb";
-    private static final String TRACE_FILENAME_IMMS = "ime_trace_managerservice.pb";
+    private static final String TRACE_FILENAME_CLIENTS = "ime_trace_clients.winscope";
+    private static final String TRACE_FILENAME_IMS = "ime_trace_service.winscope";
+    private static final String TRACE_FILENAME_IMMS = "ime_trace_managerservice.winscope";
     private static final int BUFFER_CAPACITY = 4096 * 1024;
 
     // Needed for winscope to auto-detect the dump type. Explained further in
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index bcc5b56..69af2a5 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -18,6 +18,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UiThread;
+import android.hardware.HardwareBuffer;
 
 /**
  * Provides an interface to the root-Surface of a View Hierarchy or Window. This
@@ -53,4 +54,74 @@
      * to the View hierarchy you may need to call {@link android.view.View#invalidate}
      */
     boolean applyTransactionOnDraw(@NonNull SurfaceControl.Transaction t);
+
+    /**
+     * The transform hint can be used by a buffer producer to pre-rotate the rendering such that the
+     * final transformation in the system composer is identity. This can be very useful when used in
+     * conjunction with the h/w composer HAL in situations where it cannot handle rotations or
+     * handle them with an additional power cost.
+     *
+     * The transform hint should be used with ASurfaceControl APIs when submitting buffers.
+     * Example usage:
+     *
+     * 1. After a configuration change, before dequeuing a buffer, the buffer producer queries the
+     *    function for the transform hint.
+     *
+     * 2. The desired buffer width and height is rotated by the transform hint.
+     *
+     * 3. The producer dequeues a buffer of the new pre-rotated size.
+     *
+     * 4. The producer renders to the buffer such that the image is already transformed, that is
+     *    applying the transform hint to the rendering.
+     *
+     * 5. The producer applies the inverse transform hint to the buffer it just rendered.
+     *
+     * 6. The producer queues the pre-transformed buffer with the buffer transform.
+     *
+     * 7. The composer combines the buffer transform with the display transform.  If the buffer
+     *    transform happens to cancel out the display transform then no rotation is needed and there
+     *    will be no performance penalties.
+     *
+     * Note, when using ANativeWindow APIs in conjunction with a NativeActivity Surface or
+     * SurfaceView Surface, the buffer producer will already have access to the transform hint and
+     * no additional work is needed.
+     *
+     * @see HardwareBuffer
+     */
+    default @SurfaceControl.BufferTransform int getBufferTransformHint() {
+        return SurfaceControl.BUFFER_TRANSFORM_IDENTITY;
+    }
+
+    /**
+     * Buffer transform hint change listener.
+     * @see #getBufferTransformHint
+     */
+    @UiThread
+    interface OnBufferTransformHintChangedListener {
+        /**
+         * @param hint new surface transform hint
+         * @see #getBufferTransformHint
+         */
+        void onBufferTransformHintChanged(@SurfaceControl.BufferTransform int hint);
+    }
+
+    /**
+     * Registers a {@link OnBufferTransformHintChangedListener} to receive notifications about when
+     * the transform hint changes.
+     *
+     * @see #getBufferTransformHint
+     * @see #removeOnBufferTransformHintChangedListener
+     */
+    default void addOnBufferTransformHintChangedListener(
+            @NonNull OnBufferTransformHintChangedListener listener) {
+    }
+
+    /**
+     * Unregisters a {@link OnBufferTransformHintChangedListener}.
+     *
+     * @see #addOnBufferTransformHintChangedListener
+     */
+    default void removeOnBufferTransformHintChangedListener(
+            @NonNull OnBufferTransformHintChangedListener listener) {
+    }
 }
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 9cb0d1f..e7ff978 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1463,10 +1463,10 @@
             return false;
         }
         final Configuration config = mResources.getConfiguration();
-        // TODO(b/179308296) Temporarily exclude Launcher from being given max bounds, by checking
-        // if the caller is the recents component.
+        // TODO(b/179308296) Temporarily - never report max bounds to only Launcher if the feature
+        // is disabled.
         return config != null && !config.windowConfiguration.getMaxBounds().isEmpty()
-                && !isRecentsComponent();
+                && (mDisplayInfo.shouldConstrainMetricsForLauncher || !isRecentsComponent());
     }
 
     /**
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java
index e1a4402..c1a5636 100644
--- a/core/java/android/view/DisplayCutout.java
+++ b/core/java/android/view/DisplayCutout.java
@@ -31,6 +31,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Path;
@@ -38,6 +39,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
+import android.util.DisplayUtils;
 import android.util.Pair;
 import android.util.RotationUtils;
 import android.util.proto.ProtoOutputStream;
@@ -873,18 +875,122 @@
     }
 
     /**
+     * Gets the display cutout by the given display unique id.
+     *
+     * Loads the default config {@link R.string#config_mainBuiltInDisplayCutout) if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     */
+    private static String getDisplayCutoutPath(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final String[] array = res.getStringArray(R.array.config_displayCutoutPathArray);
+        if (index >= 0 && index < array.length) {
+            return array[index];
+        }
+        return res.getString(R.string.config_mainBuiltInDisplayCutout);
+    }
+
+    /**
+     * Gets the display cutout approximation rect by the given display unique id.
+     *
+     * Loads the default config {@link R.string#config_mainBuiltInDisplayCutoutRectApproximation} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     */
+    private static String getDisplayCutoutApproximationRect(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final String[] array = res.getStringArray(
+                R.array.config_displayCutoutApproximationRectArray);
+        if (index >= 0 && index < array.length) {
+            return array[index];
+        }
+        return res.getString(R.string.config_mainBuiltInDisplayCutoutRectApproximation);
+    }
+
+    /**
+     * Gets whether to mask a built-in display cutout of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default config {@link R.bool#config_maskMainBuiltInDisplayCutout} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static boolean getMaskBuiltInDisplayCutout(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_maskBuiltInDisplayCutoutArray);
+        boolean maskCutout;
+        if (index >= 0 && index < array.length()) {
+            maskCutout = array.getBoolean(index, false);
+        } else {
+            maskCutout = res.getBoolean(R.bool.config_maskMainBuiltInDisplayCutout);
+        }
+        array.recycle();
+        return maskCutout;
+    }
+
+    /**
+     * Gets whether to fill a built-in display cutout of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default config{@link R.bool#config_fillMainBuiltInDisplayCutout} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static boolean getFillBuiltInDisplayCutout(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_fillBuiltInDisplayCutoutArray);
+        boolean fillCutout;
+        if (index >= 0 && index < array.length()) {
+            fillCutout = array.getBoolean(index, false);
+        } else {
+            fillCutout = res.getBoolean(R.bool.config_fillMainBuiltInDisplayCutout);
+        }
+        array.recycle();
+        return fillCutout;
+    }
+
+    /**
+     * Gets the waterfall cutout by the given display unique id.
+     *
+     * Loads the default waterfall dimens if {@link R.array#config_displayUniqueIdArray} is not set.
+     * {@link R.dimen#waterfall_display_left_edge_size},
+     * {@link R.dimen#waterfall_display_top_edge_size},
+     * {@link R.dimen#waterfall_display_right_edge_size},
+     * {@link R.dimen#waterfall_display_bottom_edge_size}
+     */
+    private static Insets getWaterfallInsets(Resources res, String displayUniqueId) {
+        Insets insets;
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_waterfallCutoutArray);
+        if (index >= 0 && index < array.length() && array.getResourceId(index, 0) > 0) {
+            final int resourceId = array.getResourceId(index, 0);
+            final TypedArray waterfall = res.obtainTypedArray(resourceId);
+            insets = Insets.of(
+                    waterfall.getDimensionPixelSize(0 /* waterfall left edge size */, 0),
+                    waterfall.getDimensionPixelSize(1 /* waterfall top edge size */, 0),
+                    waterfall.getDimensionPixelSize(2 /* waterfall right edge size */, 0),
+                    waterfall.getDimensionPixelSize(3 /* waterfall bottom edge size */, 0));
+            waterfall.recycle();
+        } else {
+            insets = loadWaterfallInset(res);
+        }
+        array.recycle();
+        return insets;
+    }
+
+    /**
      * Creates the display cutout according to
      * @android:string/config_mainBuiltInDisplayCutoutRectApproximation, which is the closest
      * rectangle-base approximation of the cutout.
      *
      * @hide
      */
-    public static DisplayCutout fromResourcesRectApproximation(Resources res, int displayWidth,
-            int displayHeight) {
-        return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
-                res.getString(R.string.config_mainBuiltInDisplayCutoutRectApproximation),
+    public static DisplayCutout fromResourcesRectApproximation(Resources res,
+            String displayUniqueId, int displayWidth, int displayHeight) {
+        return pathAndDisplayCutoutFromSpec(getDisplayCutoutPath(res, displayUniqueId),
+                getDisplayCutoutApproximationRect(res, displayUniqueId),
                 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT,
-                loadWaterfallInset(res)).second;
+                getWaterfallInsets(res, displayUniqueId)).second;
     }
 
     /**
@@ -892,11 +998,11 @@
      *
      * @hide
      */
-    public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) {
-        return pathAndDisplayCutoutFromSpec(
-                res.getString(R.string.config_mainBuiltInDisplayCutout), null,
+    public static Path pathFromResources(Resources res, String displayUniqueId, int displayWidth,
+            int displayHeight) {
+        return pathAndDisplayCutoutFromSpec(getDisplayCutoutPath(res, displayUniqueId), null,
                 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT,
-                loadWaterfallInset(res)).first;
+                getWaterfallInsets(res, displayUniqueId)).first;
     }
 
     /**
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 8e5f905..6572510 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -306,6 +306,13 @@
     public float brightnessDefault;
 
     /**
+     * @hide
+     * True if Display#getRealSize and getRealMetrics should be constrained for Launcher, false
+     * otherwise.
+     */
+    public boolean shouldConstrainMetricsForLauncher = false;
+
+    /**
      * The {@link RoundedCorners} if present, otherwise {@code null}.
      */
     @Nullable
@@ -381,7 +388,8 @@
                 && brightnessMinimum == other.brightnessMinimum
                 && brightnessMaximum == other.brightnessMaximum
                 && brightnessDefault == other.brightnessDefault
-                && Objects.equals(roundedCorners, other.roundedCorners);
+                && Objects.equals(roundedCorners, other.roundedCorners)
+                && shouldConstrainMetricsForLauncher == other.shouldConstrainMetricsForLauncher;
     }
 
     @Override
@@ -432,6 +440,7 @@
         brightnessMaximum = other.brightnessMaximum;
         brightnessDefault = other.brightnessDefault;
         roundedCorners = other.roundedCorners;
+        shouldConstrainMetricsForLauncher = other.shouldConstrainMetricsForLauncher;
     }
 
     public void readFromParcel(Parcel source) {
@@ -488,6 +497,7 @@
         for (int i = 0; i < numUserDisabledFormats; i++) {
             userDisabledHdrTypes[i] = source.readInt();
         }
+        shouldConstrainMetricsForLauncher = source.readBoolean();
     }
 
     @Override
@@ -542,6 +552,7 @@
         for (int i = 0; i < userDisabledHdrTypes.length; i++) {
             dest.writeInt(userDisabledHdrTypes[i]);
         }
+        dest.writeBoolean(shouldConstrainMetricsForLauncher);
     }
 
     @Override
@@ -796,6 +807,8 @@
         sb.append(brightnessMaximum);
         sb.append(", brightnessDefault ");
         sb.append(brightnessDefault);
+        sb.append(", shouldConstrainMetricsForLauncher ");
+        sb.append(shouldConstrainMetricsForLauncher);
         sb.append("}");
         return sb.toString();
     }
diff --git a/core/java/android/view/IDisplayWindowListener.aidl b/core/java/android/view/IDisplayWindowListener.aidl
index 610e0f8..f95d6b3 100644
--- a/core/java/android/view/IDisplayWindowListener.aidl
+++ b/core/java/android/view/IDisplayWindowListener.aidl
@@ -32,7 +32,8 @@
 oneway interface IDisplayWindowListener {
 
     /**
-     * Called when a display is added to the WM hierarchy.
+     * Called when a new display is added to the WM hierarchy. The existing display ids are returned
+     * when this listener is registered with WM via {@link #registerDisplayWindowListener}.
      */
     void onDisplayAdded(int displayId);
 
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index a7ecf1f..1791d3a 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -33,6 +33,7 @@
 import android.os.IRemoteCallback;
 import android.os.ParcelFileDescriptor;
 import android.view.DisplayCutout;
+import android.view.DisplayInfo;
 import android.view.IApplicationToken;
 import android.view.IAppTransitionAnimationSpecsFuture;
 import android.view.ICrossWindowBlurEnabledListener;
@@ -53,6 +54,7 @@
 import android.view.KeyEvent;
 import android.view.InputEvent;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.MagnificationSpec;
 import android.view.MotionEvent;
 import android.view.InputChannel;
@@ -345,6 +347,14 @@
     Bitmap screenshotWallpaper();
 
     /**
+     * Mirrors the wallpaper for the given display.
+     *
+     * @param displayId ID of the display for the wallpaper.
+     * @return A SurfaceControl for the parent of the mirrored wallpaper.
+     */
+    SurfaceControl mirrorWallpaperSurface(int displayId);
+
+    /**
      * Registers a wallpaper visibility listener.
      * @return Current visibility.
      */
@@ -520,9 +530,10 @@
     void unregisterDisplayFoldListener(IDisplayFoldListener listener);
 
     /**
-     * Registers an IDisplayContainerListener
+     * Registers an IDisplayContainerListener, and returns the set of existing display ids. The
+     * listener's onDisplayAdded() will not be called for the displays returned.
      */
-    void registerDisplayWindowListener(IDisplayWindowListener listener);
+    int[] registerDisplayWindowListener(IDisplayWindowListener listener);
 
     /**
      * Unregisters an IDisplayContainerListener.
@@ -720,19 +731,31 @@
             int displayId, in IDisplayWindowInsetsController displayWindowInsetsController);
 
     /**
-     * Called when a remote process modifies insets on a display window container.
+     * Called when a remote process updates the requested visibilities of insets on a display window
+     * container.
      */
-    void modifyDisplayWindowInsets(int displayId, in InsetsState state);
+    void updateDisplayWindowRequestedVisibilities(int displayId, in InsetsVisibilities vis);
 
     /**
      * Called to get the expected window insets.
      *
-     * @return {@code true} if system bars are always comsumed.
+     * @return {@code true} if system bars are always consumed.
      */
     boolean getWindowInsets(in WindowManager.LayoutParams attrs, int displayId,
             out InsetsState outInsetsState);
 
     /**
+     * Returns a list of {@link android.view.DisplayInfo} for the logical display. This is not
+     * guaranteed to include all possible device states. The list items are unique.
+     *
+     * If invoked through a package other than a launcher app, returns an empty list.
+     *
+     * @param displayId the id of the logical display
+     * @param packageName the name of the calling package
+     */
+    List<DisplayInfo> getPossibleDisplayInfo(int displayId, String packageName);
+
+    /**
      * Called to show global actions.
      */
     void showGlobalActions();
@@ -866,4 +889,11 @@
     void unregisterCrossWindowBlurEnabledListener(ICrossWindowBlurEnabledListener listener);
 
     boolean isTaskSnapshotSupported();
+
+    /**
+     * Returns the preferred display ID to show software keyboard.
+     *
+     * @see android.window.WindowProviderService#getLaunchedDisplayId
+     */
+    int getImeDisplayId();
 }
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 7bad5cb..9da5088 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -32,6 +32,7 @@
 import android.view.WindowManager;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
@@ -46,12 +47,12 @@
  */
 interface IWindowSession {
     int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs,
-            in int viewVisibility, in int layerStackId, in InsetsState requestedVisibility,
+            in int viewVisibility, in int layerStackId, in InsetsVisibilities requestedVisibilities,
             out InputChannel outInputChannel, out InsetsState insetsState,
             out InsetsSourceControl[] activeControls);
     int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs,
             in int viewVisibility, in int layerStackId, in int userId,
-            in InsetsState requestedVisibility, out InputChannel outInputChannel,
+            in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel,
             out InsetsState insetsState, out InsetsSourceControl[] activeControls);
     int addToDisplayWithoutInputChannel(IWindow window, in WindowManager.LayoutParams attrs,
             in int viewVisibility, in int layerStackId, out InsetsState insetsState);
@@ -174,6 +175,11 @@
             float touchX, float touchY, float thumbCenterX, float thumbCenterY, in ClipData data);
 
     /**
+     * Drops the content of the current drag operation for accessibility
+     */
+    boolean dropForAccessibility(IWindow window, int x, int y);
+
+    /**
      * Report the result of a drop action targeted to the given window.
      * consumed is 'true' when the drop was accepted by a valid recipient,
      * 'false' otherwise.
@@ -285,10 +291,9 @@
     oneway void updateTapExcludeRegion(IWindow window, in Region region);
 
     /**
-     * Called when the client has changed the local insets state, and now the server should reflect
-     * that new state.
+     * Updates the requested visibilities of insets.
      */
-    oneway void insetsModified(IWindow window, in InsetsState state);
+    oneway void updateRequestedVisibilities(IWindow window, in InsetsVisibilities visibilities);
 
     /**
      * Called when the system gesture exclusion has changed.
diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java
index 5a34a92..139bff4b 100644
--- a/core/java/android/view/InputWindowHandle.java
+++ b/core/java/android/view/InputWindowHandle.java
@@ -19,9 +19,10 @@
 import static android.view.Display.INVALID_DISPLAY;
 
 import android.annotation.Nullable;
+import android.graphics.Matrix;
 import android.graphics.Region;
+import android.gui.TouchOcclusionMode;
 import android.os.IBinder;
-import android.os.TouchOcclusionMode;
 
 import java.lang.ref.WeakReference;
 
@@ -43,6 +44,17 @@
     // channel and the server input channel will both contain this token.
     public IBinder token;
 
+    /**
+     * The {@link IWindow} handle if InputWindowHandle is associated with a window, null otherwise.
+     */
+    @Nullable
+    private IBinder windowToken;
+    /**
+     * Used to cache IWindow from the windowToken so we don't need to convert every time getWindow
+     * is called.
+     */
+    private IWindow window;
+
     // The window name.
     public String name;
 
@@ -122,6 +134,12 @@
      */
     public boolean replaceTouchableRegionWithCrop;
 
+    /**
+     * The transform that should be applied to the Window to get it from screen coordinates to
+     * window coordinates
+     */
+    public Matrix transform;
+
     private native void nativeDispose();
 
     public InputWindowHandle(InputApplicationHandle inputApplicationHandle, int displayId) {
@@ -136,6 +154,9 @@
                         .append(frameRight).append(",").append(frameBottom).append("]")
                 .append(", touchableRegion=").append(touchableRegion)
                 .append(", visible=").append(visible)
+                .append(", scaleFactor=").append(scaleFactor)
+                .append(", transform=").append(transform)
+                .append(", windowToken=").append(windowToken)
                 .toString();
 
     }
@@ -167,4 +188,17 @@
     public void setTouchableRegionCrop(@Nullable SurfaceControl bounds) {
         touchableRegionSurfaceControl = new WeakReference<>(bounds);
     }
+
+    public void setWindowToken(IWindow iwindow) {
+        windowToken = iwindow.asBinder();
+        window = iwindow;
+    }
+
+    public IWindow getWindow() {
+        if (window != null) {
+            return window;
+        }
+        window = IWindow.Stub.asInterface(windowToken);
+        return window;
+    }
 }
diff --git a/core/java/android/view/InsetsAnimationControlCallbacks.java b/core/java/android/view/InsetsAnimationControlCallbacks.java
index 3431c3e..04bb609 100644
--- a/core/java/android/view/InsetsAnimationControlCallbacks.java
+++ b/core/java/android/view/InsetsAnimationControlCallbacks.java
@@ -20,7 +20,8 @@
 import android.view.WindowInsetsAnimation.Bounds;
 
 /**
- * Provide an interface to let InsetsAnimationControlImpl call back into its owner.
+ * Provide an interface to let InsetsAnimationControlImpl and InsetsResizeAnimationRunner call back
+ * into its owner.
  * @hide
  */
 public interface InsetsAnimationControlCallbacks {
@@ -34,10 +35,9 @@
      *     <li>Dispatch {@link WindowInsetsAnimationControlListener#onReady}</li>
      * </ul>
      */
-    void startAnimation(InsetsAnimationControlImpl controller,
-            WindowInsetsAnimationControlListener listener, int types,
-            WindowInsetsAnimation animation,
-            Bounds bounds);
+    <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController>
+    void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types,
+            WindowInsetsAnimation animation, Bounds bounds);
 
     /**
      * Schedule the apply by posting the animation callback.
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 17b3020..7d8d653 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -69,7 +69,7 @@
  * @hide
  */
 @VisibleForTesting
-public class InsetsAnimationControlImpl implements WindowInsetsAnimationController,
+public class InsetsAnimationControlImpl implements InternalInsetsAnimationController,
         InsetsAnimationControlRunner {
 
     private static final String TAG = "InsetsAnimationCtrlImpl";
@@ -105,7 +105,7 @@
     private float mCurrentAlpha = 1.0f;
     private float mPendingAlpha = 1.0f;
     @VisibleForTesting(visibility = PACKAGE)
-    public boolean mReadyDispatched;
+    private boolean mReadyDispatched;
     private Boolean mPerceptible;
 
     @VisibleForTesting
@@ -170,6 +170,11 @@
     }
 
     @Override
+    public void setReadyDispatched(boolean dispatched) {
+        mReadyDispatched = dispatched;
+    }
+
+    @Override
     public Insets getHiddenStateInsets() {
         return mHiddenInsets;
     }
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index 691e638..fc97541 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -54,8 +54,8 @@
 
         @Override
         @UiThread
-        public void startAnimation(InsetsAnimationControlImpl controller,
-                WindowInsetsAnimationControlListener listener, int types,
+        public <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController>
+        void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types,
                 WindowInsetsAnimation animation, Bounds bounds) {
             // Animation will be started in constructor already.
         }
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 6f915c9..adda721 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -107,9 +107,12 @@
                 boolean hasControl);
 
         /**
-         * Called when insets have been modified by the client and should be reported back to WM.
+         * Called when the requested visibilities of insets have been modified by the client.
+         * The visibilities should be reported back to WM.
+         *
+         * @param visibilities A collection of the requested visibilities.
          */
-        void onInsetsModified(InsetsState insetsState);
+        void updateRequestedVisibilities(InsetsVisibilities visibilities);
 
         /**
          * @return Whether the host has any callbacks it wants to synchronize the animations with.
@@ -202,6 +205,9 @@
     private static final int ANIMATION_DURATION_FADE_IN_MS = 500;
     private static final int ANIMATION_DURATION_FADE_OUT_MS = 1500;
 
+    /** Visible for WindowManagerWrapper */
+    public static final int ANIMATION_DURATION_RESIZE = 300;
+
     private static final int ANIMATION_DELAY_DIM_MS = 500;
 
     private static final int ANIMATION_DURATION_SYNC_IME_MS = 285;
@@ -232,6 +238,9 @@
     private static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR =
             new PathInterpolator(0.4f, 0f, 1f, 1f);
 
+    /** Visible for WindowManagerWrapper */
+    public static final Interpolator RESIZE_INTERPOLATOR = new LinearInterpolator();
+
     /** The amount IME will move up/down when animating in floating mode. */
     private static final int FLOATING_IME_BOTTOM_INSET_DP = -80;
 
@@ -285,9 +294,13 @@
     @VisibleForTesting
     public static final int ANIMATION_TYPE_USER = 2;
 
+    /** Running animation will resize insets */
+    @VisibleForTesting
+    public static final int ANIMATION_TYPE_RESIZE = 3;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {ANIMATION_TYPE_NONE, ANIMATION_TYPE_SHOW, ANIMATION_TYPE_HIDE,
-            ANIMATION_TYPE_USER})
+            ANIMATION_TYPE_USER, ANIMATION_TYPE_RESIZE})
     @interface AnimationType {
     }
 
@@ -317,7 +330,7 @@
         private final boolean mDisable;
         private final int mFloatingImeBottomInset;
 
-        private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
+        private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
                 new ThreadLocal<AnimationHandler>() {
             @Override
             protected AnimationHandler initialValue() {
@@ -536,10 +549,8 @@
     /** The state dispatched from server */
     private final InsetsState mLastDispatchedState = new InsetsState();
 
-    // TODO: Use other class to represent the requested visibility of each type, because the
-    //       display frame and the frame in each source are not used.
     /** The requested visibilities sent to server */
-    private final InsetsState mRequestedState = new InsetsState();
+    private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
 
     private final Rect mFrame = new Rect();
     private final BiFunction<InsetsController, Integer, InsetsSourceConsumer> mConsumerCreator;
@@ -549,7 +560,6 @@
 
     private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
     private final ArrayList<RunningAnimation> mRunningAnimations = new ArrayList<>();
-    private final ArrayList<InsetsAnimationControlImpl> mTmpFinishedControls = new ArrayList<>();
     private final ArraySet<InsetsSourceConsumer> mRequestedVisibilityChanged = new ArraySet<>();
     private WindowInsets mLastInsets;
 
@@ -569,7 +579,7 @@
     private int mCaptionInsetsHeight = 0;
     private boolean mAnimationsDisabled;
 
-    private Runnable mPendingControlTimeout = this::abortPendingImeControlRequest;
+    private final Runnable mPendingControlTimeout = this::abortPendingImeControlRequest;
     private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
             = new ArrayList<>();
 
@@ -579,7 +589,7 @@
     /** Set of inset types which cannot be controlled by the user animation */
     private @InsetsType int mDisabledUserAnimationInsetsTypes;
 
-    private Runnable mInvokeControllableInsetsChangedListeners =
+    private final Runnable mInvokeControllableInsetsChangedListeners =
             this::invokeControllableInsetsChangedListeners;
 
     public InsetsController(Host host) {
@@ -607,23 +617,23 @@
             }
 
             final List<WindowInsetsAnimation> runningAnimations = new ArrayList<>();
+            final List<WindowInsetsAnimation> finishedAnimations = new ArrayList<>();
             final InsetsState state = new InsetsState(mState, true /* copySources */);
             for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
                 RunningAnimation runningAnimation = mRunningAnimations.get(i);
                 if (DEBUG) Log.d(TAG, "Running animation type: " + runningAnimation.type);
-                InsetsAnimationControlRunner runner = runningAnimation.runner;
-                if (runner instanceof InsetsAnimationControlImpl) {
-                    InsetsAnimationControlImpl control = (InsetsAnimationControlImpl) runner;
+                final InsetsAnimationControlRunner runner = runningAnimation.runner;
+                if (runner instanceof WindowInsetsAnimationController) {
 
                     // Keep track of running animation to be dispatched. Aggregate it here such that
                     // if it gets finished within applyChangeInsets we still dispatch it to
                     // onProgress.
                     if (runningAnimation.startDispatched) {
-                        runningAnimations.add(control.getAnimation());
+                        runningAnimations.add(runner.getAnimation());
                     }
 
-                    if (control.applyChangeInsets(state)) {
-                        mTmpFinishedControls.add(control);
+                    if (((InternalInsetsAnimationController) runner).applyChangeInsets(state)) {
+                        finishedAnimations.add(runner.getAnimation());
                     }
                 }
             }
@@ -641,10 +651,9 @@
                 }
             }
 
-            for (int i = mTmpFinishedControls.size() - 1; i >= 0; i--) {
-                dispatchAnimationEnd(mTmpFinishedControls.get(i).getAnimation());
+            for (int i = finishedAnimations.size() - 1; i >= 0; i--) {
+                dispatchAnimationEnd(finishedAnimations.get(i));
             }
-            mTmpFinishedControls.clear();
         };
     }
 
@@ -690,15 +699,13 @@
                 true /* excludeInvisibleIme */)) {
             if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged");
             mHost.notifyInsetsChanged();
+            startResizingAnimationIfNeeded(lastState);
         }
         return true;
     }
 
     private void updateState(InsetsState newState) {
-        mState.setDisplayFrame(newState.getDisplayFrame());
-        mState.setDisplayCutout(newState.getDisplayCutout());
-        mState.setRoundedCorners(newState.getRoundedCorners());
-        mState.setPrivacyIndicatorBounds(newState.getPrivacyIndicatorBounds());
+        mState.set(newState, 0 /* types */);
         @InsetsType int disabledUserAnimationTypes = 0;
         @InsetsType int[] cancelledUserAnimationTypes = {0};
         for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
@@ -763,6 +770,39 @@
         return false;
     }
 
+    private void startResizingAnimationIfNeeded(InsetsState fromState) {
+        if (!fromState.getDisplayFrame().equals(mState.getDisplayFrame())) {
+            return;
+        }
+        @InsetsType int types = 0;
+        InsetsState toState = null;
+        final ArraySet<Integer> internalTypes = InsetsState.toInternalType(Type.systemBars());
+        for (int i = internalTypes.size() - 1; i >= 0; i--) {
+            final @InternalInsetsType int type = internalTypes.valueAt(i);
+            final InsetsSource fromSource = fromState.peekSource(type);
+            final InsetsSource toSource = mState.peekSource(type);
+            if (fromSource != null && toSource != null
+                    && fromSource.isVisible() && toSource.isVisible()
+                    && !fromSource.getFrame().equals(toSource.getFrame())
+                    && (Rect.intersects(mFrame, fromSource.getFrame())
+                            || Rect.intersects(mFrame, toSource.getFrame()))) {
+                types |= InsetsState.toPublicType(toSource.getType());
+                if (toState == null) {
+                    toState = new InsetsState();
+                }
+                toState.addSource(new InsetsSource(toSource));
+            }
+        }
+        if (types == 0) {
+            return;
+        }
+        cancelExistingControllers(types);
+        final InsetsAnimationControlRunner runner = new InsetsResizeAnimationRunner(
+                mFrame, fromState, toState, RESIZE_INTERPOLATOR, ANIMATION_DURATION_RESIZE, types,
+                this);
+        mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
+    }
+
     /**
      * @see InsetsState#calculateInsets
      */
@@ -784,7 +824,7 @@
     /**
      * @see InsetsState#calculateVisibleInsets(Rect, int)
      */
-    public Rect calculateVisibleInsets(@SoftInputModeFlags int softInputMode) {
+    public Insets calculateVisibleInsets(@SoftInputModeFlags int softInputMode) {
         return mState.calculateVisibleInsets(mFrame, softInputMode);
     }
 
@@ -801,7 +841,7 @@
             }
         }
 
-        boolean requestedStateStale = false;
+        boolean requestedVisibilityStale = false;
         final int[] showTypes = new int[1];
         final int[] hideTypes = new int[1];
 
@@ -822,20 +862,20 @@
             final InsetsSourceConsumer consumer = getSourceConsumer(type);
             consumer.setControl(control, showTypes, hideTypes);
 
-            if (!requestedStateStale) {
+            if (!requestedVisibilityStale) {
                 final boolean requestedVisible = consumer.isRequestedVisible();
 
                 // We might have changed our requested visibilities while we don't have the control,
                 // so we need to update our requested state once we have control. Otherwise, our
                 // requested state at the server side might be incorrect.
                 final boolean requestedVisibilityChanged =
-                        requestedVisible != mRequestedState.getSourceOrDefaultVisibility(type);
+                        requestedVisible != mRequestedVisibilities.getVisibility(type);
 
                 // The IME client visibility will be reset by insets source provider while updating
                 // control, so if IME is requested visible, we need to send the request to server.
                 final boolean imeRequestedVisible = type == ITYPE_IME && requestedVisible;
 
-                requestedStateStale = requestedVisibilityChanged || imeRequestedVisible;
+                requestedVisibilityStale = requestedVisibilityChanged || imeRequestedVisible;
             }
         }
 
@@ -861,7 +901,7 @@
         }
 
         // InsetsSourceConsumer#setControl might change the requested visibility.
-        updateRequestedVisibility();
+        updateRequestedVisibilities();
     }
 
     @Override
@@ -1015,7 +1055,7 @@
         if (types == 0) {
             // nothing to animate.
             listener.onCancelled(null);
-            updateRequestedVisibility();
+            updateRequestedVisibilities();
             if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked");
             return;
         }
@@ -1051,7 +1091,7 @@
                     }
                 });
             }
-            updateRequestedVisibility();
+            updateRequestedVisibilities();
             Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
             return;
         }
@@ -1059,7 +1099,7 @@
         if (typesReady == 0) {
             if (DEBUG) Log.d(TAG, "No types ready. onCancelled()");
             listener.onCancelled(null);
-            updateRequestedVisibility();
+            updateRequestedVisibilities();
             return;
         }
 
@@ -1091,7 +1131,7 @@
         } else {
             hideDirectly(types, false /* animationFinished */, animationType, fromIme);
         }
-        updateRequestedVisibility();
+        updateRequestedVisibilities();
     }
 
     /**
@@ -1348,7 +1388,7 @@
     /**
      * Sends the requested visibilities to window manager if any of them is changed.
      */
-    private void updateRequestedVisibility() {
+    private void updateRequestedVisibilities() {
         boolean changed = false;
         for (int i = mRequestedVisibilityChanged.size() - 1; i >= 0; i--) {
             final InsetsSourceConsumer consumer = mRequestedVisibilityChanged.valueAt(i);
@@ -1357,8 +1397,8 @@
                 continue;
             }
             final boolean requestedVisible = consumer.isRequestedVisible();
-            if (requestedVisible != mRequestedState.getSourceOrDefaultVisibility(type)) {
-                mRequestedState.getSource(type).setVisible(requestedVisible);
+            if (mRequestedVisibilities.getVisibility(type) != requestedVisible) {
+                mRequestedVisibilities.setVisibility(type, requestedVisible);
                 changed = true;
             }
         }
@@ -1366,11 +1406,11 @@
         if (!changed) {
             return;
         }
-        mHost.onInsetsModified(mRequestedState);
+        mHost.updateRequestedVisibilities(mRequestedVisibilities);
     }
 
-    InsetsState getRequestedVisibility() {
-        return mRequestedState;
+    InsetsVisibilities getRequestedVisibilities() {
+        return mRequestedVisibilities;
     }
 
     @VisibleForTesting
@@ -1425,7 +1465,7 @@
         for (int i = internalTypes.size() - 1; i >= 0; i--) {
             getSourceConsumer(internalTypes.valueAt(i)).hide(animationFinished, animationType);
         }
-        updateRequestedVisibility();
+        updateRequestedVisibilities();
 
         if (fromIme) {
             Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0);
@@ -1441,7 +1481,7 @@
         for (int i = internalTypes.size() - 1; i >= 0; i--) {
             getSourceConsumer(internalTypes.valueAt(i)).show(false /* fromIme */);
         }
-        updateRequestedVisibility();
+        updateRequestedVisibilities();
 
         if (fromIme) {
             Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0);
@@ -1473,12 +1513,12 @@
 
     @VisibleForTesting
     @Override
-    public void startAnimation(InsetsAnimationControlImpl controller,
-            WindowInsetsAnimationControlListener listener, int types,
+    public <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController>
+    void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types,
             WindowInsetsAnimation animation, Bounds bounds) {
         mHost.dispatchWindowInsetsAnimationPrepare(animation);
         mHost.addOnPreDrawRunnable(() -> {
-            if (controller.isCancelled()) {
+            if (runner.isCancelled()) {
                 if (WARN) Log.w(TAG, "startAnimation canceled before preDraw");
                 return;
             }
@@ -1486,15 +1526,15 @@
                     "InsetsAnimation: " + WindowInsets.Type.toString(types), types);
             for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
                 RunningAnimation runningAnimation = mRunningAnimations.get(i);
-                if (runningAnimation.runner == controller) {
+                if (runningAnimation.runner == runner) {
                     runningAnimation.startDispatched = true;
                 }
             }
             Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.pendingAnim", 0);
             mHost.dispatchWindowInsetsAnimationStart(animation, bounds);
             mStartingAnimation = true;
-            controller.mReadyDispatched = true;
-            listener.onReady(controller, types);
+            runner.setReadyDispatched(true);
+            listener.onReady(runner, types);
             mStartingAnimation = false;
         });
     }
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
new file mode 100644
index 0000000..e1352dd
--- /dev/null
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.InsetsAnimationControlImplProto.CURRENT_ALPHA;
+import static android.view.InsetsAnimationControlImplProto.IS_CANCELLED;
+import static android.view.InsetsAnimationControlImplProto.IS_FINISHED;
+import static android.view.InsetsAnimationControlImplProto.PENDING_ALPHA;
+import static android.view.InsetsAnimationControlImplProto.PENDING_FRACTION;
+import static android.view.InsetsAnimationControlImplProto.PENDING_INSETS;
+import static android.view.InsetsAnimationControlImplProto.SHOWN_ON_FINISH;
+import static android.view.InsetsAnimationControlImplProto.TMP_MATRIX;
+import static android.view.InsetsController.ANIMATION_TYPE_RESIZE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsState.InternalInsetsType;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowInsetsAnimation.Bounds;
+import android.view.animation.Interpolator;
+
+/**
+ * Runs a fake animation of resizing insets to produce insets animation callbacks.
+ * @hide
+ */
+public class InsetsResizeAnimationRunner implements InsetsAnimationControlRunner,
+        InternalInsetsAnimationController, WindowInsetsAnimationControlListener {
+
+    private final InsetsState mFromState;
+    private final InsetsState mToState;
+    private final @InsetsType int mTypes;
+    private final WindowInsetsAnimation mAnimation;
+    private final InsetsAnimationControlCallbacks mController;
+    private ValueAnimator mAnimator;
+    private boolean mCancelled;
+    private boolean mFinished;
+
+    public InsetsResizeAnimationRunner(Rect frame, InsetsState fromState, InsetsState toState,
+            Interpolator interpolator, long duration, @InsetsType int types,
+            InsetsAnimationControlCallbacks controller) {
+        mFromState = fromState;
+        mToState = toState;
+        mTypes = types;
+        mController = controller;
+        mAnimation = new WindowInsetsAnimation(types, interpolator, duration);
+        mAnimation.setAlpha(1f);
+        final Insets fromInsets = fromState.calculateInsets(
+                frame, types, false /* ignoreVisibility */);
+        final Insets toInsets = toState.calculateInsets(
+                frame, types, false /* ignoreVisibility */);
+        controller.startAnimation(this, this, types, mAnimation,
+                new Bounds(Insets.min(fromInsets, toInsets), Insets.max(fromInsets, toInsets)));
+    }
+
+    @Override
+    public int getTypes() {
+        return mTypes;
+    }
+
+    @Override
+    public int getControllingTypes() {
+        return mTypes;
+    }
+
+    @Override
+    public WindowInsetsAnimation getAnimation() {
+        return mAnimation;
+    }
+
+    @Override
+    public int getAnimationType() {
+        return ANIMATION_TYPE_RESIZE;
+    }
+
+    @Override
+    public void cancel() {
+        if (mCancelled || mFinished) {
+            return;
+        }
+        mCancelled = true;
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return mCancelled;
+    }
+
+    @Override
+    public void onReady(WindowInsetsAnimationController controller, int types) {
+        if (mCancelled) {
+            return;
+        }
+        mAnimator = ValueAnimator.ofFloat(0f, 1f);
+        mAnimator.setDuration(mAnimation.getDurationMillis());
+        mAnimator.addUpdateListener(animation -> {
+            mAnimation.setFraction(animation.getAnimatedFraction());
+            mController.scheduleApplyChangeInsets(InsetsResizeAnimationRunner.this);
+        });
+        mAnimator.addListener(new AnimatorListenerAdapter() {
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mFinished = true;
+                mController.scheduleApplyChangeInsets(InsetsResizeAnimationRunner.this);
+            }
+        });
+        mAnimator.start();
+    }
+
+    @Override
+    public boolean applyChangeInsets(InsetsState outState) {
+        final float fraction = mAnimation.getInterpolatedFraction();
+        for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
+            final InsetsSource fromSource = mFromState.peekSource(type);
+            final InsetsSource toSource = mToState.peekSource(type);
+            if (fromSource == null || toSource == null) {
+                continue;
+            }
+            final Rect fromFrame = fromSource.getFrame();
+            final Rect toFrame = toSource.getFrame();
+            final Rect frame = new Rect(
+                    (int) (fromFrame.left + fraction * (toFrame.left - fromFrame.left)),
+                    (int) (fromFrame.top + fraction * (toFrame.top - fromFrame.top)),
+                    (int) (fromFrame.right + fraction * (toFrame.right - fromFrame.right)),
+                    (int) (fromFrame.bottom + fraction * (toFrame.bottom - fromFrame.bottom)));
+            final InsetsSource source = new InsetsSource(type);
+            source.setFrame(frame);
+            source.setVisible(toSource.isVisible());
+            outState.addSource(source);
+        }
+        if (mFinished) {
+            mController.notifyFinished(this, true /* shown */);
+        }
+        return mFinished;
+    }
+
+    @Override
+    public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+        final long token = proto.start(fieldId);
+        proto.write(IS_CANCELLED, mCancelled);
+        proto.write(IS_FINISHED, mFinished);
+        proto.write(TMP_MATRIX, "null");
+        proto.write(PENDING_INSETS, "null");
+        proto.write(PENDING_FRACTION, mAnimation.getInterpolatedFraction());
+        proto.write(SHOWN_ON_FINISH, true);
+        proto.write(CURRENT_ALPHA, 1f);
+        proto.write(PENDING_ALPHA, 1f);
+        proto.end(token);
+    }
+
+    @Override
+    public Insets getHiddenStateInsets() {
+        return Insets.NONE;
+    }
+
+    @Override
+    public Insets getShownStateInsets() {
+        return Insets.NONE;
+    }
+
+    @Override
+    public Insets getCurrentInsets() {
+        return Insets.NONE;
+    }
+
+    @Override
+    public float getCurrentFraction() {
+        return 0;
+    }
+
+    @Override
+    public float getCurrentAlpha() {
+        return 0;
+    }
+
+    @Override
+    public void setInsetsAndAlpha(Insets insets, float alpha, float fraction) {
+    }
+
+    @Override
+    public void finish(boolean shown) {
+    }
+
+    @Override
+    public boolean isFinished() {
+        return false;
+    }
+
+    @Override
+    public void notifyControlRevoked(int types) {
+    }
+
+    @Override
+    public void updateSurfacePosition(SparseArray<InsetsSourceControl> controls) {
+    }
+
+    @Override
+    public boolean hasZeroInsetsIme() {
+        return false;
+    }
+
+    @Override
+    public void setReadyDispatched(boolean dispatched) {
+    }
+
+    @Override
+    public void onFinished(WindowInsetsAnimationController controller) {
+    }
+
+    @Override
+    public void onCancelled(WindowInsetsAnimationController controller) {
+    }
+}
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 1506ee4..9d98a3e 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -185,6 +185,15 @@
         return result;
     }
 
+    @Override
+    public String toString() {
+        return "InsetsSourceControl: {"
+                + "type=" + InsetsState.typeToString(mType)
+                + ", mSurfacePosition=" + mSurfacePosition
+                + ", mInsetsHint=" + mInsetsHint
+                + "}";
+    }
+
     public void dump(String prefix, PrintWriter pw) {
         pw.print(prefix);
         pw.print("InsetsSourceControl type="); pw.print(InsetsState.typeToString(mType));
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 37101b7..3917279 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -202,7 +202,7 @@
             @Nullable @InternalInsetsSide SparseIntArray typeSideMap) {
         Insets[] typeInsetsMap = new Insets[Type.SIZE];
         Insets[] typeMaxInsetsMap = new Insets[Type.SIZE];
-        boolean[] typeVisibilityMap = new boolean[SIZE];
+        boolean[] typeVisibilityMap = new boolean[Type.SIZE];
         final Rect relativeFrame = new Rect(frame);
         final Rect relativeFrameMax = new Rect(frame);
         for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
@@ -301,7 +301,7 @@
         return mPrivacyIndicatorBounds.inset(insetLeft, insetTop, insetRight, insetBottom);
     }
 
-    public Rect calculateInsets(Rect frame, @InsetsType int types, boolean ignoreVisibility) {
+    public Insets calculateInsets(Rect frame, @InsetsType int types, boolean ignoreVisibility) {
         Insets insets = Insets.NONE;
         for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
             InsetsSource source = mSources[type];
@@ -314,10 +314,10 @@
             }
             insets = Insets.max(source.calculateInsets(frame, ignoreVisibility), insets);
         }
-        return insets.toRect();
+        return insets;
     }
 
-    public Rect calculateVisibleInsets(Rect frame, @SoftInputModeFlags int softInputMode) {
+    public Insets calculateVisibleInsets(Rect frame, @SoftInputModeFlags int softInputMode) {
         Insets insets = Insets.NONE;
         for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
             InsetsSource source = mSources[type];
@@ -332,7 +332,7 @@
             }
             insets = Insets.max(source.calculateVisibleInsets(frame), insets);
         }
-        return insets.toRect();
+        return insets;
     }
 
     /**
@@ -878,16 +878,5 @@
                 + ", mSources= { " + joiner
                 + " }";
     }
-
-    public @NonNull String toSourceVisibilityString() {
-        StringJoiner joiner = new StringJoiner(", ");
-        for (int i = 0; i < SIZE; i++) {
-            InsetsSource source = mSources[i];
-            if (source != null) {
-                joiner.add(typeToString(i) + ": " + (source.isVisible() ? "visible" : "invisible"));
-            }
-        }
-        return joiner.toString();
-    }
 }
 
diff --git a/core/java/android/view/InsetsVisibilities.aidl b/core/java/android/view/InsetsVisibilities.aidl
new file mode 100644
index 0000000..bd573ea
--- /dev/null
+++ b/core/java/android/view/InsetsVisibilities.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+parcelable InsetsVisibilities;
diff --git a/core/java/android/view/InsetsVisibilities.java b/core/java/android/view/InsetsVisibilities.java
new file mode 100644
index 0000000..7d259fb
--- /dev/null
+++ b/core/java/android/view/InsetsVisibilities.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.StringJoiner;
+
+/**
+ * A collection of visibilities of insets. This is used for carrying the requested visibilities.
+ * @hide
+ */
+public class InsetsVisibilities implements Parcelable {
+
+    private static final int UNSPECIFIED = 0;
+    private static final int VISIBLE = 1;
+    private static final int INVISIBLE = -1;
+
+    private final int[] mVisibilities = new int[InsetsState.SIZE];
+
+    public InsetsVisibilities() {
+    }
+
+    public InsetsVisibilities(InsetsVisibilities other) {
+        set(other);
+    }
+
+    public InsetsVisibilities(Parcel in) {
+        in.readIntArray(mVisibilities);
+    }
+
+    /**
+     * Copies from another {@link InsetsVisibilities}.
+     *
+     * @param other an instance of {@link InsetsVisibilities}.
+     */
+    public void set(InsetsVisibilities other) {
+        System.arraycopy(other.mVisibilities, InsetsState.FIRST_TYPE, mVisibilities,
+                InsetsState.FIRST_TYPE, InsetsState.SIZE);
+    }
+
+    /**
+     * Sets a visibility to a type.
+     *
+     * @param type The {@link @InsetsState.InternalInsetsType}.
+     * @param visible {@code true} represents visible; {@code false} represents invisible.
+     */
+    public void setVisibility(@InsetsState.InternalInsetsType int type, boolean visible) {
+        mVisibilities[type] = visible ? VISIBLE : INVISIBLE;
+    }
+
+    /**
+     * Returns the specified insets visibility of the type. If it has never been specified,
+     * this returns the default visibility.
+     *
+     * @param type The {@link @InsetsState.InternalInsetsType}.
+     * @return The specified visibility or the default one if it is not specified.
+     */
+    public boolean getVisibility(@InsetsState.InternalInsetsType int type) {
+        final int visibility = mVisibilities[type];
+        return visibility == UNSPECIFIED
+                ? InsetsState.getDefaultVisibility(type)
+                : visibility == VISIBLE;
+    }
+
+    @Override
+    public String toString() {
+        StringJoiner joiner = new StringJoiner(", ");
+        for (int type = InsetsState.FIRST_TYPE; type <= InsetsState.LAST_TYPE; type++) {
+            final int visibility = mVisibilities[type];
+            if (visibility != UNSPECIFIED) {
+                joiner.add(InsetsState.typeToString(type) + ": "
+                        + (visibility == VISIBLE ? "visible" : "invisible"));
+            }
+        }
+        return joiner.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(mVisibilities);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof InsetsVisibilities)) {
+            return false;
+        }
+        return Arrays.equals(mVisibilities, ((InsetsVisibilities) other).mVisibilities);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeIntArray(mVisibilities);
+    }
+
+    public void readFromParcel(@NonNull Parcel in) {
+        in.readIntArray(mVisibilities);
+    }
+
+    public static final @NonNull Creator<InsetsVisibilities> CREATOR =
+            new Creator<InsetsVisibilities>() {
+
+        public InsetsVisibilities createFromParcel(Parcel in) {
+            return new InsetsVisibilities(in);
+        }
+
+        public InsetsVisibilities[] newArray(int size) {
+            return new InsetsVisibilities[size];
+        }
+    };
+}
diff --git a/core/java/android/view/InternalInsetsAnimationController.java b/core/java/android/view/InternalInsetsAnimationController.java
new file mode 100644
index 0000000..d7f3e20
--- /dev/null
+++ b/core/java/android/view/InternalInsetsAnimationController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/**
+ * An internal interface which provides methods that will be only used by the framework.
+ * @hide
+ */
+public interface InternalInsetsAnimationController extends WindowInsetsAnimationController {
+
+    /**
+     * Flags whether {@link WindowInsetsAnimationControlListener#onReady(
+     * WindowInsetsAnimationController, int)} has been invoked.
+     * @hide
+     */
+    void setReadyDispatched(boolean dispatched);
+
+    /**
+     * Returns the {@link InsetsState} based on the current animation progress.
+     *
+     * @param outState the insets state which matches the current animation progress.
+     * @return {@code true} if the animation has been finished; {@code false} otherwise.
+     * @hide
+     */
+    boolean applyChangeInsets(InsetsState outState);
+}
+
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 69ff64f..40942ea7 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -4008,6 +4008,22 @@
         public float orientation;
 
         /**
+         * The movement of x position of a motion event.
+         *
+         * @see MotionEvent#AXIS_RELATIVE_X
+         * @hide
+         */
+        public float relativeX;
+
+        /**
+         * The movement of y position of a motion event.
+         *
+         * @see MotionEvent#AXIS_RELATIVE_Y
+         * @hide
+         */
+        public float relativeY;
+
+        /**
          * Clears the contents of this object.
          * Resets all axes to zero.
          */
@@ -4023,6 +4039,8 @@
             toolMajor = 0;
             toolMinor = 0;
             orientation = 0;
+            relativeX = 0;
+            relativeY = 0;
         }
 
         /**
@@ -4053,6 +4071,8 @@
             toolMajor = other.toolMajor;
             toolMinor = other.toolMinor;
             orientation = other.orientation;
+            relativeX = other.relativeX;
+            relativeY = other.relativeY;
         }
 
         /**
@@ -4084,6 +4104,10 @@
                     return toolMinor;
                 case AXIS_ORIENTATION:
                     return orientation;
+                case AXIS_RELATIVE_X:
+                    return relativeX;
+                case AXIS_RELATIVE_Y:
+                    return relativeY;
                 default: {
                     if (axis < 0 || axis > 63) {
                         throw new IllegalArgumentException("Axis out of range.");
@@ -4137,6 +4161,12 @@
                 case AXIS_ORIENTATION:
                     orientation = value;
                     break;
+                case AXIS_RELATIVE_X:
+                    relativeX = value;
+                    break;
+                case AXIS_RELATIVE_Y:
+                    relativeY = value;
+                    break;
                 default: {
                     if (axis < 0 || axis > 63) {
                         throw new IllegalArgumentException("Axis out of range.");
diff --git a/core/java/android/view/RemoteAnimationAdapter.java b/core/java/android/view/RemoteAnimationAdapter.java
index a78036f..e1cc604 100644
--- a/core/java/android/view/RemoteAnimationAdapter.java
+++ b/core/java/android/view/RemoteAnimationAdapter.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import android.app.ActivityOptions;
+import android.app.IApplicationThread;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Parcel;
@@ -58,6 +59,9 @@
     private int mCallingPid;
     private int mCallingUid;
 
+    /** @see #getCallingApplication */
+    private IApplicationThread mCallingApplication;
+
     /**
      * @param runner The interface that gets notified when we actually need to start the animation.
      * @param duration The duration of the animation.
@@ -81,11 +85,19 @@
         this(runner, duration, statusBarTransitionDelay, false /* changeNeedsSnapshot */);
     }
 
+    @UnsupportedAppUsage
+    public RemoteAnimationAdapter(IRemoteAnimationRunner runner, long duration,
+            long statusBarTransitionDelay, IApplicationThread callingApplication) {
+        this(runner, duration, statusBarTransitionDelay, false /* changeNeedsSnapshot */);
+        mCallingApplication = callingApplication;
+    }
+
     public RemoteAnimationAdapter(Parcel in) {
         mRunner = IRemoteAnimationRunner.Stub.asInterface(in.readStrongBinder());
         mDuration = in.readLong();
         mStatusBarTransitionDelay = in.readLong();
         mChangeNeedsSnapshot = in.readBoolean();
+        mCallingApplication = IApplicationThread.Stub.asInterface(in.readStrongBinder());
     }
 
     public IRemoteAnimationRunner getRunner() {
@@ -126,6 +138,15 @@
         return mCallingUid;
     }
 
+    /**
+     * Gets the ApplicationThread that will run the animation. Instead it is intended to pass the
+     * calling information among client processes (eg. shell + launcher) through one-way binder
+     * calls (where binder itself doesn't track calling information).
+     */
+    public IApplicationThread getCallingApplication() {
+        return mCallingApplication;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -137,6 +158,7 @@
         dest.writeLong(mDuration);
         dest.writeLong(mStatusBarTransitionDelay);
         dest.writeBoolean(mChangeNeedsSnapshot);
+        dest.writeStrongInterface(mCallingApplication);
     }
 
     public static final @android.annotation.NonNull Creator<RemoteAnimationAdapter> CREATOR
diff --git a/core/java/android/view/RemoteAnimationTarget.java b/core/java/android/view/RemoteAnimationTarget.java
index cdc099b..046232a 100644
--- a/core/java/android/view/RemoteAnimationTarget.java
+++ b/core/java/android/view/RemoteAnimationTarget.java
@@ -197,19 +197,35 @@
     public ActivityManager.RunningTaskInfo taskInfo;
 
     /**
+     * {@code true} if picture-in-picture permission is granted in {@link android.app.AppOpsManager}
+     */
+    @UnsupportedAppUsage
+    public boolean allowEnterPip;
+
+    /**
      * The {@link android.view.WindowManager.LayoutParams.WindowType} of this window. It's only used
      * for non-app window.
      */
     public final @WindowManager.LayoutParams.WindowType int windowType;
 
+    /**
+     * {@code true} if its parent is also a {@link RemoteAnimationTarget} in the same transition.
+     *
+     * For example, when a TaskFragment is resizing while one of its children is open/close, both
+     * windows will be animation targets. This value will be {@code true} for the child, so that
+     * the handler can choose to handle it differently.
+     */
+    public boolean hasAnimatingParent;
+
     public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
             Rect clipRect, Rect contentInsets, int prefixOrderIndex, Point position,
             Rect localBounds, Rect screenSpaceBounds,
             WindowConfiguration windowConfig, boolean isNotInRecents,
-            SurfaceControl startLeash, Rect startBounds, ActivityManager.RunningTaskInfo taskInfo) {
+            SurfaceControl startLeash, Rect startBounds, ActivityManager.RunningTaskInfo taskInfo,
+            boolean allowEnterPip) {
         this(taskId, mode, leash, isTranslucent, clipRect, contentInsets, prefixOrderIndex,
                 position, localBounds, screenSpaceBounds, windowConfig, isNotInRecents, startLeash,
-                startBounds, taskInfo, INVALID_WINDOW_TYPE);
+                startBounds, taskInfo, allowEnterPip, INVALID_WINDOW_TYPE);
     }
 
     public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
@@ -217,7 +233,7 @@
             Rect localBounds, Rect screenSpaceBounds,
             WindowConfiguration windowConfig, boolean isNotInRecents,
             SurfaceControl startLeash, Rect startBounds,
-            ActivityManager.RunningTaskInfo taskInfo,
+            ActivityManager.RunningTaskInfo taskInfo, boolean allowEnterPip,
             @WindowManager.LayoutParams.WindowType int windowType) {
         this.mode = mode;
         this.taskId = taskId;
@@ -226,7 +242,7 @@
         this.clipRect = new Rect(clipRect);
         this.contentInsets = new Rect(contentInsets);
         this.prefixOrderIndex = prefixOrderIndex;
-        this.position = new Point(position);
+        this.position = position == null ? new Point() : new Point(position);
         this.localBounds = new Rect(localBounds);
         this.sourceContainerBounds = new Rect(screenSpaceBounds);
         this.screenSpaceBounds = new Rect(screenSpaceBounds);
@@ -235,6 +251,7 @@
         this.startLeash = startLeash;
         this.startBounds = startBounds == null ? null : new Rect(startBounds);
         this.taskInfo = taskInfo;
+        this.allowEnterPip = allowEnterPip;
         this.windowType = windowType;
     }
 
@@ -255,7 +272,9 @@
         startLeash = in.readTypedObject(SurfaceControl.CREATOR);
         startBounds = in.readTypedObject(Rect.CREATOR);
         taskInfo = in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR);
+        allowEnterPip = in.readBoolean();
         windowType = in.readInt();
+        hasAnimatingParent = in.readBoolean();
     }
 
     @Override
@@ -281,7 +300,9 @@
         dest.writeTypedObject(startLeash, 0 /* flags */);
         dest.writeTypedObject(startBounds, 0 /* flags */);
         dest.writeTypedObject(taskInfo, 0 /* flags */);
+        dest.writeBoolean(allowEnterPip);
         dest.writeInt(windowType);
+        dest.writeBoolean(hasAnimatingParent);
     }
 
     public void dump(PrintWriter pw, String prefix) {
@@ -299,7 +320,9 @@
         pw.print(prefix); pw.print("windowConfiguration="); pw.println(windowConfiguration);
         pw.print(prefix); pw.print("leash="); pw.println(leash);
         pw.print(prefix); pw.print("taskInfo="); pw.println(taskInfo);
+        pw.print(prefix); pw.print("allowEnterPip="); pw.println(allowEnterPip);
         pw.print(prefix); pw.print("windowType="); pw.print(windowType);
+        pw.print(prefix); pw.print("hasAnimatingParent="); pw.print(hasAnimatingParent);
     }
 
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
diff --git a/core/java/android/view/RoundedCorners.java b/core/java/android/view/RoundedCorners.java
index 623d969..6079d8e 100644
--- a/core/java/android/view/RoundedCorners.java
+++ b/core/java/android/view/RoundedCorners.java
@@ -27,9 +27,11 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Point;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.DisplayUtils;
 import android.util.Pair;
 import android.view.RoundedCorner.Position;
 
@@ -94,8 +96,8 @@
      * @android:dimen/rounded_corner_radius_top and @android:dimen/rounded_corner_radius_bottom
      */
     public static RoundedCorners fromResources(
-            Resources res, int displayWidth, int displayHeight) {
-        return fromRadii(loadRoundedCornerRadii(res), displayWidth, displayHeight);
+            Resources res, String displayUniqueId, int displayWidth, int displayHeight) {
+        return fromRadii(loadRoundedCornerRadii(res, displayUniqueId), displayWidth, displayHeight);
     }
 
     /**
@@ -140,14 +142,16 @@
      * Loads the rounded corner radii from resources.
      *
      * @param res
+     * @param displayUniqueId the display unique id.
      * @return a Pair of radius. The first is the top rounded corner radius and second is the
      * bottom corner radius.
      */
     @Nullable
-    private static Pair<Integer, Integer> loadRoundedCornerRadii(Resources res) {
-        final int radiusDefault = res.getDimensionPixelSize(R.dimen.rounded_corner_radius);
-        final int radiusTop = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_top);
-        final int radiusBottom = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_bottom);
+    private static Pair<Integer, Integer> loadRoundedCornerRadii(
+            Resources res, String displayUniqueId) {
+        final int radiusDefault = getRoundedCornerRadius(res, displayUniqueId);
+        final int radiusTop = getRoundedCornerTopRadius(res, displayUniqueId);
+        final int radiusBottom = getRoundedCornerBottomRadius(res, displayUniqueId);
         if (radiusDefault == 0 && radiusTop == 0 && radiusBottom == 0) {
             return null;
         }
@@ -158,6 +162,164 @@
     }
 
     /**
+     * Gets the default rounded corner radius of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default dimen{@link R.dimen#rounded_corner_radius} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static int getRoundedCornerRadius(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_roundedCornerRadiusArray);
+        int radius;
+        if (index >= 0 && index < array.length()) {
+            radius = array.getDimensionPixelSize(index, 0);
+        } else {
+            radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius);
+        }
+        array.recycle();
+        return radius;
+    }
+
+    /**
+     * Gets the top rounded corner radius of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default dimen{@link R.dimen#rounded_corner_radius_top} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static int getRoundedCornerTopRadius(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_roundedCornerTopRadiusArray);
+        int radius;
+        if (index >= 0 && index < array.length()) {
+            radius = array.getDimensionPixelSize(index, 0);
+        } else {
+            radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_top);
+        }
+        array.recycle();
+        return radius;
+    }
+
+    /**
+     * Gets the bottom rounded corner radius of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default dimen{@link R.dimen#rounded_corner_radius_bottom} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static int getRoundedCornerBottomRadius(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(
+                R.array.config_roundedCornerBottomRadiusArray);
+        int radius;
+        if (index >= 0 && index < array.length()) {
+            radius = array.getDimensionPixelSize(index, 0);
+        } else {
+            radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_bottom);
+        }
+        array.recycle();
+        return radius;
+    }
+
+    /**
+     * Gets the rounded corner radius adjustment of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default dimen{@link R.dimen#rounded_corner_radius_adjustment} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static int getRoundedCornerRadiusAdjustment(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(
+                R.array.config_roundedCornerRadiusAdjustmentArray);
+        int radius;
+        if (index >= 0 && index < array.length()) {
+            radius = array.getDimensionPixelSize(index, 0);
+        } else {
+            radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_adjustment);
+        }
+        array.recycle();
+        return radius;
+    }
+
+    /**
+     * Gets the rounded corner top radius adjustment of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default dimen{@link R.dimen#rounded_corner_radius_top_adjustment} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static int getRoundedCornerRadiusTopAdjustment(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(
+                R.array.config_roundedCornerTopRadiusAdjustmentArray);
+        int radius;
+        if (index >= 0 && index < array.length()) {
+            radius = array.getDimensionPixelSize(index, 0);
+        } else {
+            radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_top_adjustment);
+        }
+        array.recycle();
+        return radius;
+    }
+
+    /**
+     * Gets the rounded corner bottom radius adjustment of a display which is determined by the
+     * given display unique id.
+     *
+     * Loads the default dimen{@link R.dimen#rounded_corner_radius_bottom_adjustment} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static int getRoundedCornerRadiusBottomAdjustment(
+            Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(
+                R.array.config_roundedCornerBottomRadiusAdjustmentArray);
+        int radius;
+        if (index >= 0 && index < array.length()) {
+            radius = array.getDimensionPixelSize(index, 0);
+        } else {
+            radius = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_bottom_adjustment);
+        }
+        array.recycle();
+        return radius;
+    }
+
+    /**
+     * Gets whether a built-in display is round.
+     *
+     * Loads the default config{@link R.bool#config_mainBuiltInDisplayIsRound} if
+     * {@link R.array#config_displayUniqueIdArray} is not set.
+     *
+     * @hide
+     */
+    public static boolean getBuiltInDisplayIsRound(Resources res, String displayUniqueId) {
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_builtInDisplayIsRoundArray);
+        boolean isRound;
+        if (index >= 0 && index < array.length()) {
+            isRound = array.getBoolean(index, false);
+        } else {
+            isRound = res.getBoolean(R.bool.config_mainBuiltInDisplayIsRound);
+        }
+        array.recycle();
+        return isRound;
+    }
+
+    /**
      * Insets the reference frame of the rounded corners.
      *
      * @return a copy of this instance which has been inset
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
index fa7330f..904aa73 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -29,6 +29,7 @@
 import android.graphics.ColorSpace;
 import android.graphics.HardwareRenderer;
 import android.graphics.Matrix;
+import android.graphics.Point;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
 import android.graphics.RenderNode;
@@ -412,6 +413,20 @@
     }
 
     /**
+     * Returns the default size of this Surface provided by the consumer of the surface.
+     * Should only be used by the producer of the surface.
+     *
+     * @hide
+     */
+    @NonNull
+    public Point getDefaultSize() {
+        synchronized (mLock) {
+            checkNotReleasedLocked();
+            return new Point(nativeGetWidth(mNativeObject), nativeGetHeight(mNativeObject));
+        }
+    }
+
+    /**
      * Gets a {@link Canvas} for drawing into this surface.
      *
      * After drawing into the provided {@link Canvas}, the caller must
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 8143cf9..960d23d 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -23,6 +23,7 @@
 import static android.graphics.Matrix.MTRANS_X;
 import static android.graphics.Matrix.MTRANS_Y;
 import static android.view.SurfaceControlProto.HASH_CODE;
+import static android.view.SurfaceControlProto.LAYER_ID;
 import static android.view.SurfaceControlProto.NAME;
 
 import android.annotation.FloatRange;
@@ -41,6 +42,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.gui.DropInputMode;
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DeviceProductInfo;
 import android.hardware.display.DisplayedContentSample;
@@ -150,13 +152,15 @@
             float childRelativeTop, float childRelativeRight, float childRelativeBottom);
     private static native void nativeSetTrustedOverlay(long transactionObj, long nativeObject,
             boolean isTrustedOverlay);
-
+    private static native void nativeSetDropInputMode(
+            long transactionObj, long nativeObject, int flags);
     private static native boolean nativeClearContentFrameStats(long nativeObject);
     private static native boolean nativeGetContentFrameStats(long nativeObject, WindowContentFrameStats outStats);
     private static native boolean nativeClearAnimationFrameStats();
     private static native boolean nativeGetAnimationFrameStats(WindowAnimationFrameStats outStats);
 
     private static native long[] nativeGetPhysicalDisplayIds();
+    private static native long nativeGetPrimaryPhysicalDisplayId();
     private static native IBinder nativeGetPhysicalDisplayToken(long physicalDisplayId);
     private static native IBinder nativeCreateDisplay(String name, boolean secure);
     private static native void nativeDestroyDisplay(IBinder displayToken);
@@ -164,6 +168,8 @@
             IBinder displayToken, long nativeSurfaceObject);
     private static native void nativeSetDisplayLayerStack(long transactionObj,
             IBinder displayToken, int layerStack);
+    private static native void nativeSetDisplayFlags(long transactionObj,
+            IBinder displayToken, int flags);
     private static native void nativeSetDisplayProjection(long transactionObj,
             IBinder displayToken, int orientation,
             int l, int t, int r, int b,
@@ -235,8 +241,80 @@
     private static native void nativeRemoveJankDataListener(long nativeListener);
     private static native long nativeCreateJankDataListenerWrapper(OnJankDataListener listener);
     private static native int nativeGetGPUContextPriority();
-    private static native void nativeSetTransformHint(long nativeObject, int transformHint);
+    private static native void nativeSetTransformHint(long nativeObject,
+            @SurfaceControl.BufferTransform int transformHint);
     private static native int nativeGetTransformHint(long nativeObject);
+    private static native int nativeGetLayerId(long nativeObject);
+
+    /**
+     * Transforms that can be applied to buffers as they are displayed to a window.
+     *
+     * Supported transforms are any combination of horizontal mirror, vertical mirror, and
+     * clock-wise 90 degree rotation, in that order. Rotations of 180 and 270 degrees are made up
+     * of those basic transforms.
+     * Mirrors {@code ANativeWindowTransform} definitions.
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"BUFFER_TRANSFORM_"},
+            value = {BUFFER_TRANSFORM_IDENTITY, BUFFER_TRANSFORM_MIRROR_HORIZONTAL,
+                    BUFFER_TRANSFORM_MIRROR_VERTICAL, BUFFER_TRANSFORM_ROTATE_90,
+                    BUFFER_TRANSFORM_ROTATE_180, BUFFER_TRANSFORM_ROTATE_270,
+                    BUFFER_TRANSFORM_MIRROR_HORIZONTAL | BUFFER_TRANSFORM_ROTATE_90,
+                    BUFFER_TRANSFORM_MIRROR_VERTICAL | BUFFER_TRANSFORM_ROTATE_90})
+    public @interface BufferTransform {
+    }
+
+    /**
+     * Identity transform.
+     *
+     * These transforms that can be applied to buffers as they are displayed to a window.
+     * @see HardwareBuffer
+     *
+     * Supported transforms are any combination of horizontal mirror, vertical mirror, and
+     * clock-wise 90 degree rotation, in that order. Rotations of 180 and 270 degrees are
+     * made up of those basic transforms.
+     */
+    public static final int BUFFER_TRANSFORM_IDENTITY = 0x00;
+    /**
+     * Mirror horizontally. Can be combined with {@link #BUFFER_TRANSFORM_MIRROR_VERTICAL}
+     * and {@link #BUFFER_TRANSFORM_ROTATE_90}.
+     */
+    public static final int BUFFER_TRANSFORM_MIRROR_HORIZONTAL = 0x01;
+    /**
+     * Mirror vertically. Can be combined with {@link #BUFFER_TRANSFORM_MIRROR_HORIZONTAL}
+     * and {@link #BUFFER_TRANSFORM_ROTATE_90}.
+     */
+    public static final int BUFFER_TRANSFORM_MIRROR_VERTICAL = 0x02;
+    /**
+     * Rotate 90 degrees clock-wise. Can be combined with {@link
+     * #BUFFER_TRANSFORM_MIRROR_HORIZONTAL} and {@link #BUFFER_TRANSFORM_MIRROR_VERTICAL}.
+     */
+    public static final int BUFFER_TRANSFORM_ROTATE_90 = 0x04;
+    /**
+     * Rotate 180 degrees clock-wise. Cannot be combined with other transforms.
+     */
+    public static final int BUFFER_TRANSFORM_ROTATE_180 =
+            BUFFER_TRANSFORM_MIRROR_HORIZONTAL | BUFFER_TRANSFORM_MIRROR_VERTICAL;
+    /**
+     * Rotate 270 degrees clock-wise. Cannot be combined with other transforms.
+     */
+    public static final int BUFFER_TRANSFORM_ROTATE_270 =
+            BUFFER_TRANSFORM_ROTATE_180 | BUFFER_TRANSFORM_ROTATE_90;
+
+    /**
+     * @hide
+     */
+    public static @BufferTransform int rotationToBufferTransform(@Surface.Rotation int rotation) {
+        switch (rotation) {
+            case Surface.ROTATION_0: return BUFFER_TRANSFORM_IDENTITY;
+            case Surface.ROTATION_90: return BUFFER_TRANSFORM_ROTATE_90;
+            case Surface.ROTATION_180: return BUFFER_TRANSFORM_ROTATE_180;
+            case Surface.ROTATION_270: return BUFFER_TRANSFORM_ROTATE_270;
+        }
+        Log.e(TAG, "Trying to convert unknown rotation=" + rotation);
+        return BUFFER_TRANSFORM_IDENTITY;
+    }
 
     @Nullable
     @GuardedBy("mLock")
@@ -352,8 +430,6 @@
     @GuardedBy("mLock")
     private int mHeight;
 
-    private int mTransformHint;
-
     private WeakReference<View> mLocalOwnerView;
 
     static GlobalTransactionWrapper sGlobalTransaction;
@@ -550,6 +626,15 @@
      */
     private static final int SURFACE_OPAQUE = 0x02;
 
+    /* flags used with setDisplayFlags() (keep in sync with DisplayDevice.h) */
+
+    /**
+     * DisplayDevice flag: This display's transform is sent to inputflinger and used for input
+     * dispatch. This flag is used to disambiguate displays which share a layerstack.
+     * @hide
+     */
+    public static final int DISPLAY_RECEIVES_INPUT = 0x01;
+
     // Display power modes.
     /**
      * Display power mode off: used while blanking the screen.
@@ -1527,6 +1612,7 @@
         final long token = proto.start(fieldId);
         proto.write(HASH_CODE, System.identityHashCode(this));
         proto.write(NAME, mName);
+        proto.write(LAYER_ID, getLayerId());
         proto.end(token);
     }
 
@@ -2266,6 +2352,15 @@
     }
 
     /**
+     * Exposed to identify the correct display to apply the primary display orientation. Avoid using
+     * for any other purpose.
+     * @hide
+     */
+    public static long getPrimaryPhysicalDisplayId() {
+        return nativeGetPrimaryPhysicalDisplayId();
+    }
+
+    /**
      * @hide
      */
     public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
@@ -3169,6 +3264,17 @@
         /**
          * @hide
          */
+        public Transaction setDisplayFlags(IBinder displayToken, int flags) {
+            if (displayToken == null) {
+                throw new IllegalArgumentException("displayToken must not be null");
+            }
+            nativeSetDisplayFlags(mNativeObject, displayToken, flags);
+            return this;
+        }
+
+        /**
+         * @hide
+         */
         public Transaction setDisplayProjection(IBinder displayToken,
                 int orientation, Rect layerStackRect, Rect displayRect) {
             if (displayToken == null) {
@@ -3426,7 +3532,18 @@
             return this;
         }
 
-         /**
+        /**
+         * Sets the input event drop mode on this SurfaceControl and its children. The caller must
+         * hold the ACCESS_SURFACE_FLINGER permission. See {@code InputEventDropMode}.
+         * @hide
+         */
+        public Transaction setDropInputMode(SurfaceControl sc, @DropInputMode int mode) {
+            checkPreconditions(sc);
+            nativeSetDropInputMode(mNativeObject, sc.mNativeObject, mode);
+            return this;
+        }
+
+        /**
          * Merge the other transaction into this transaction, clearing the
          * other transaction as if it had been applied.
          *
@@ -3613,7 +3730,7 @@
     /**
      * @hide
      */
-    public int getTransformHint() {
+    public @SurfaceControl.BufferTransform int getTransformHint() {
         checkNotReleased();
         return nativeGetTransformHint(mNativeObject);
     }
@@ -3627,7 +3744,18 @@
      * with the same size.
      * @hide
      */
-    public void setTransformHint(@Surface.Rotation int transformHint) {
+    public void setTransformHint(@SurfaceControl.BufferTransform int transformHint) {
         nativeSetTransformHint(mNativeObject, transformHint);
     }
+
+    /**
+     * @hide
+     */
+    public int getLayerId() {
+        if (mNativeObject != 0) {
+            return nativeGetLayerId(mNativeObject);
+        }
+
+        return -1;
+    }
 }
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 11b161a..a6c5042d 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -292,11 +292,18 @@
      */
     @TestApi
     public void relayout(WindowManager.LayoutParams attrs) {
+        relayout(attrs, SurfaceControl.Transaction::apply);
+    }
+
+    /**
+     * Forces relayout and draw and allows to set a custom callback when it is finished
+     * @hide
+     */
+    public void relayout(WindowManager.LayoutParams attrs,
+            WindowlessWindowManager.ResizeCompleteCallback callback) {
         mViewRoot.setLayoutParams(attrs, false);
         mViewRoot.setReportNextDraw();
-        mWm.setCompletionCallback(mViewRoot.mWindow.asBinder(), (SurfaceControl.Transaction t) -> {
-            t.apply();
-        });
+        mWm.setCompletionCallback(mViewRoot.mWindow.asBinder(), callback);
     }
 
     /**
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 4b8b607d..856dfe5 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -214,7 +214,7 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     final Rect mSurfaceFrame = new Rect();
     int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1;
-    int mTransformHint = 0;
+    @SurfaceControl.BufferTransform int mTransformHint = 0;
 
     private boolean mGlobalListenersAdded;
     private boolean mAttachedToWindow;
@@ -1104,7 +1104,7 @@
             || mWindowSpaceTop != mLocation[1];
         final boolean layoutSizeChanged = getWidth() != mScreenRect.width()
             || getHeight() != mScreenRect.height();
-        final boolean hintChanged = (viewRoot.getSurfaceTransformHint() != mTransformHint)
+        final boolean hintChanged = (viewRoot.getBufferTransformHint() != mTransformHint)
                 && mRequestedVisible;
 
         if (creating || formatChanged || sizeChanged || visibleChanged ||
@@ -1130,7 +1130,7 @@
                 mSurfaceHeight = myHeight;
                 mFormat = mRequestedFormat;
                 mLastWindowVisibility = mWindowVisibility;
-                mTransformHint = viewRoot.getSurfaceTransformHint();
+                mTransformHint = viewRoot.getBufferTransformHint();
 
                 mScreenRect.left = mWindowSpaceLeft;
                 mScreenRect.top = mWindowSpaceTop;
@@ -1362,7 +1362,7 @@
         if (mBlastBufferQueue != null) {
             mBlastBufferQueue.destroy();
         }
-        mTransformHint = viewRoot.getSurfaceTransformHint();
+        mTransformHint = viewRoot.getBufferTransformHint();
         mBlastSurfaceControl.setTransformHint(mTransformHint);
         mBlastBufferQueue = new BLASTBufferQueue(name, mBlastSurfaceControl, mSurfaceWidth,
                 mSurfaceHeight, mFormat);
@@ -1464,19 +1464,18 @@
 
         @Override
         public void positionChanged(long frameNumber, int left, int top, int right, int bottom) {
-            if (mSurfaceControl == null) {
-                return;
-            }
-
-            // TODO: This is teensy bit racey in that a brand new SurfaceView moving on
-            // its 2nd frame if RenderThread is running slowly could potentially see
-            // this as false, enter the branch, get pre-empted, then this comes along
-            // and reports a new position, then the UI thread resumes and reports
-            // its position. This could therefore be de-sync'd in that interval, but
-            // the synchronization would violate the rule that RT must never block
-            // on the UI thread which would open up potential deadlocks. The risk of
-            // a single-frame desync is therefore preferable for now.
             synchronized(mSurfaceControlLock) {
+                if (mSurfaceControl == null) {
+                    return;
+                }
+                // TODO: This is teensy bit racey in that a brand new SurfaceView moving on
+                // its 2nd frame if RenderThread is running slowly could potentially see
+                // this as false, enter the branch, get pre-empted, then this comes along
+                // and reports a new position, then the UI thread resumes and reports
+                // its position. This could therefore be de-sync'd in that interval, but
+                // the synchronization would violate the rule that RT must never block
+                // on the UI thread which would open up potential deadlocks. The risk of
+                // a single-frame desync is therefore preferable for now.
                 mRtHandlingPositionUpdates = true;
             }
             if (mRTLastReportedPosition.left == left
@@ -1506,8 +1505,11 @@
                 if (mViewVisibility) {
                     mPositionChangedTransaction.show(mSurfaceControl);
                 }
-                applyChildSurfaceTransaction_renderWorker(mPositionChangedTransaction,
-                        getViewRootImpl().mSurface, frameNumber);
+                final ViewRootImpl viewRoot = getViewRootImpl();
+                if (viewRoot != null) {
+                    applyChildSurfaceTransaction_renderWorker(mPositionChangedTransaction,
+                            viewRoot.mSurface, frameNumber);
+                }
                 applyOrMergeTransaction(mPositionChangedTransaction, frameNumber);
                 mPendingTransaction = false;
             } catch (Exception ex) {
@@ -1528,7 +1530,7 @@
 
         @Override
         public void positionLost(long frameNumber) {
-            if (DEBUG) {
+            if (DEBUG_POSITION) {
                 Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d",
                         System.identityHashCode(this), frameNumber));
             }
@@ -1540,15 +1542,15 @@
                 mPositionChangedTransaction.clear();
                 mPendingTransaction = false;
             }
-            if (mSurfaceControl == null) {
-                return;
-            }
 
             /**
              * positionLost can be called while UI thread is un-paused so we
              * need to hold the lock here.
              */
             synchronized (mSurfaceControlLock) {
+                if (mSurfaceControl == null) {
+                    return;
+                }
                 mRtTransaction.hide(mSurfaceControl);
                 if (mRtReleaseSurfaces) {
                     mRtReleaseSurfaces = false;
@@ -1604,12 +1606,21 @@
      * @hide
      */
     public void setResizeBackgroundColor(int bgColor) {
+        setResizeBackgroundColor(mTmpTransaction, bgColor);
+        mTmpTransaction.apply();
+    }
+
+    /**
+     * Version of {@link #setResizeBackgroundColor(int)} that allows you to provide
+     * {@link SurfaceControl.Transaction}.
+     * @hide
+     */
+    public void setResizeBackgroundColor(@NonNull SurfaceControl.Transaction t, int bgColor) {
         if (mBackgroundControl == null) {
             return;
         }
-
         mBackgroundColor = bgColor;
-        updateBackgroundColor(mTmpTransaction).apply();
+        updateBackgroundColor(t);
     }
 
     @UnsupportedAppUsage
@@ -1878,18 +1889,45 @@
      * @param p The SurfacePackage to embed.
      */
     public void setChildSurfacePackage(@NonNull SurfaceControlViewHost.SurfacePackage p) {
+        setChildSurfacePackage(p, false /* applyTransactionOnDraw */);
+    }
+
+    /**
+     * Similar to setChildSurfacePackage, but using the BLAST queue so the transaction can be
+     * synchronized with the ViewRootImpl frame.
+     * @hide
+     */
+    public void setChildSurfacePackageOnDraw(
+            @NonNull SurfaceControlViewHost.SurfacePackage p) {
+        setChildSurfacePackage(p, true /* applyTransactionOnDraw */);
+    }
+
+    /**
+     * @param applyTransactionOnDraw Whether to apply transaction at onDraw or immediately.
+     */
+    private void setChildSurfacePackage(
+            @NonNull SurfaceControlViewHost.SurfacePackage p, boolean applyTransactionOnDraw) {
         final SurfaceControl lastSc = mSurfacePackage != null ?
                 mSurfacePackage.getSurfaceControl() : null;
         if (mSurfaceControl != null && lastSc != null) {
-            mTmpTransaction.reparent(lastSc, null).apply();
+            mTmpTransaction.reparent(lastSc, null);
             mSurfacePackage.release();
+            applyTransaction(applyTransactionOnDraw);
         } else if (mSurfaceControl != null) {
             reparentSurfacePackage(mTmpTransaction, p);
-            mTmpTransaction.apply();
+            applyTransaction(applyTransactionOnDraw);
         }
         mSurfacePackage = p;
     }
 
+    private void applyTransaction(boolean applyTransactionOnDraw) {
+        if (applyTransactionOnDraw) {
+            getViewRootImpl().applyTransactionOnDraw(mTmpTransaction);
+        } else {
+            mTmpTransaction.apply();
+        }
+    }
+
     private void reparentSurfacePackage(SurfaceControl.Transaction t,
             SurfaceControlViewHost.SurfacePackage p) {
         final SurfaceControl sc = p.getSurfaceControl();
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index cf12955..572a7cd 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -3515,6 +3515,7 @@
      *                    1             PFLAG4_ALLOW_CLICK_WHEN_DISABLED
      *                   1              PFLAG4_DETACHED
      *                  1               PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE
+     *                 1                PFLAG4_DRAG_A11Y_STARTED
      * |-------|-------|-------|-------|
      */
 
@@ -3586,6 +3587,11 @@
      */
     private static final int PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE = 0x000004000;
 
+    /**
+     * Indicates that the view has started a drag with {@link AccessibilityAction#ACTION_DRAG_START}
+     */
+    private static final int PFLAG4_DRAG_A11Y_STARTED = 0x000008000;
+
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -5036,6 +5042,14 @@
     public static final int DRAG_FLAG_OPAQUE = 1 << 9;
 
     /**
+     * Flag indicating that the drag was initiated with
+     * {@link AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_START}. When
+     * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)} is called, this
+     * is used by the system to perform a drag without animations.
+     */
+    public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1 << 10;
+
+    /**
      * Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
      */
     private float mVerticalScrollFactor;
@@ -10376,8 +10390,17 @@
         if (mTouchDelegate != null) {
             info.setTouchDelegateInfo(mTouchDelegate.getTouchDelegateInfo());
         }
+
+        if (startedSystemDragForAccessibility()) {
+            info.addAction(AccessibilityAction.ACTION_DRAG_CANCEL);
+        }
+
+        if (canAcceptAccessibilityDrop()) {
+            info.addAction(AccessibilityAction.ACTION_DRAG_DROP);
+        }
     }
 
+
     /**
      * Adds extra data to an {@link AccessibilityNodeInfo} based on an explicit request for the
      * additional data.
@@ -14214,9 +14237,45 @@
                 return true;
             }
         }
+
+        if (action == R.id.accessibilityActionDragDrop) {
+            if (!canAcceptAccessibilityDrop()) {
+                return false;
+            }
+            try {
+                if (mAttachInfo != null && mAttachInfo.mSession != null) {
+                    final int[] location = new int[2];
+                    getLocationInWindow(location);
+                    final int centerX = location[0] + getWidth() / 2;
+                    final int centerY = location[1] + getHeight() / 2;
+                    return mAttachInfo.mSession.dropForAccessibility(mAttachInfo.mWindow,
+                            centerX, centerY);
+                }
+            } catch (RemoteException e) {
+                Log.e(VIEW_LOG_TAG, "Unable to drop for accessibility", e);
+            }
+            return false;
+        } else if (action == R.id.accessibilityActionDragCancel) {
+            if (!startedSystemDragForAccessibility()) {
+                return false;
+            }
+            if (mAttachInfo != null && mAttachInfo.mDragToken != null) {
+                cancelDragAndDrop();
+                return true;
+            }
+            return false;
+        }
         return false;
     }
 
+    private boolean canAcceptAccessibilityDrop() {
+        if (!canAcceptDrag()) {
+            return false;
+        }
+        ListenerInfo li = mListenerInfo;
+        return (li != null) && (li.mOnDragListener != null || li.mOnReceiveContentListener != null);
+    }
+
     private boolean traverseAtGranularity(int granularity, boolean forward,
             boolean extendSelection) {
         CharSequence text = getIterableTextForAccessibility();
@@ -26633,6 +26692,7 @@
      *         <li>{@link #DRAG_FLAG_GLOBAL_URI_READ}</li>
      *         <li>{@link #DRAG_FLAG_GLOBAL_URI_WRITE}</li>
      *         <li>{@link #DRAG_FLAG_OPAQUE}</li>
+     *         <li>{@link #DRAG_FLAG_ACCESSIBILITY_ACTION}</li>
      *     </ul>
      * @return {@code true} if the method completes successfully, or
      * {@code false} if it fails anywhere. Returning {@code false} means the system was unable to
@@ -26656,6 +26716,37 @@
             data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
         }
 
+        Rect bounds = new Rect();
+        getBoundsOnScreen(bounds, true);
+
+        Point lastTouchPoint = new Point();
+        mAttachInfo.mViewRootImpl.getLastTouchPoint(lastTouchPoint);
+        final ViewRootImpl root = mAttachInfo.mViewRootImpl;
+
+        // Skip surface logic since shadows and animation are not required during the a11y drag
+        final boolean a11yEnabled = AccessibilityManager.getInstance(mContext).isEnabled();
+        if (a11yEnabled && (flags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) != 0) {
+            try {
+                IBinder token = mAttachInfo.mSession.performDrag(
+                        mAttachInfo.mWindow, flags, null,
+                        mAttachInfo.mViewRootImpl.getLastTouchSource(),
+                        0f, 0f, 0f, 0f, data);
+                if (ViewDebug.DEBUG_DRAG) {
+                    Log.d(VIEW_LOG_TAG, "startDragAndDrop via a11y action returned " + token);
+                }
+                if (token != null) {
+                    root.setLocalDragState(myLocalState);
+                    mAttachInfo.mDragToken = token;
+                    mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
+                    setAccessibilityDragStarted(true);
+                }
+                return token != null;
+            } catch (Exception e) {
+                Log.e(VIEW_LOG_TAG, "Unable to initiate a11y drag", e);
+                return false;
+            }
+        }
+
         Point shadowSize = new Point();
         Point shadowTouchPoint = new Point();
         shadowBuilder.onProvideShadowMetrics(shadowSize, shadowTouchPoint);
@@ -26680,7 +26771,6 @@
                     + " shadowX=" + shadowTouchPoint.x + " shadowY=" + shadowTouchPoint.y);
         }
 
-        final ViewRootImpl root = mAttachInfo.mViewRootImpl;
         final SurfaceSession session = new SurfaceSession();
         final SurfaceControl surfaceControl = new SurfaceControl.Builder(session)
                 .setName("drag surface")
@@ -26703,12 +26793,9 @@
                 surface.unlockCanvasAndPost(canvas);
             }
 
-            // repurpose 'shadowSize' for the last touch point
-            root.getLastTouchPoint(shadowSize);
-
-            token = mAttachInfo.mSession.performDrag(
-                    mAttachInfo.mWindow, flags, surfaceControl, root.getLastTouchSource(),
-                    shadowSize.x, shadowSize.y, shadowTouchPoint.x, shadowTouchPoint.y, data);
+            token = mAttachInfo.mSession.performDrag(mAttachInfo.mWindow, flags, surfaceControl,
+                    root.getLastTouchSource(), lastTouchPoint.x, lastTouchPoint.y,
+                    shadowTouchPoint.x, shadowTouchPoint.y, data);
             if (ViewDebug.DEBUG_DRAG) {
                 Log.d(VIEW_LOG_TAG, "performDrag returned " + token);
             }
@@ -26720,6 +26807,10 @@
                 mAttachInfo.mDragToken = token;
                 // Cache the local state object for delivery with DragEvents
                 root.setLocalDragState(myLocalState);
+                if (a11yEnabled) {
+                    // Set for AccessibilityEvents
+                    mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
+                }
             }
             return token != null;
         } catch (Exception e) {
@@ -26733,6 +26824,24 @@
         }
     }
 
+    void setAccessibilityDragStarted(boolean started) {
+        int pflags4 = mPrivateFlags4;
+        if (started) {
+            pflags4 |= PFLAG4_DRAG_A11Y_STARTED;
+        } else {
+            pflags4 &= ~PFLAG4_DRAG_A11Y_STARTED;
+        }
+
+        if (pflags4 != mPrivateFlags4) {
+            mPrivateFlags4 = pflags4;
+            sendWindowContentChangedAccessibilityEvent(CONTENT_CHANGE_TYPE_UNDEFINED);
+        }
+    }
+
+    private boolean startedSystemDragForAccessibility() {
+        return (mPrivateFlags4 & PFLAG4_DRAG_A11Y_STARTED) != 0;
+    }
+
     /**
      * Cancels an ongoing drag and drop operation.
      * <p>
@@ -26950,6 +27059,12 @@
         }
 
         switch (event.mAction) {
+            case DragEvent.ACTION_DRAG_STARTED: {
+                if (result && li != null && li.mOnDragListener != null) {
+                    sendWindowContentChangedAccessibilityEvent(
+                            AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+                }
+            } break;
             case DragEvent.ACTION_DRAG_ENTERED: {
                 mPrivateFlags2 |= View.PFLAG2_DRAG_HOVERED;
                 refreshDrawableState();
@@ -26958,7 +27073,16 @@
                 mPrivateFlags2 &= ~View.PFLAG2_DRAG_HOVERED;
                 refreshDrawableState();
             } break;
+            case DragEvent.ACTION_DROP: {
+                if (result && li != null && (li.mOnDragListener != null
+                        || li.mOnReceiveContentListener != null)) {
+                    sendWindowContentChangedAccessibilityEvent(
+                            AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_DROPPED);
+                }
+            } break;
             case DragEvent.ACTION_DRAG_ENDED: {
+                sendWindowContentChangedAccessibilityEvent(
+                        AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
                 mPrivateFlags2 &= ~View.DRAG_MASK;
                 refreshDrawableState();
             } break;
@@ -26971,6 +27095,15 @@
         return (mPrivateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0;
     }
 
+    void sendWindowContentChangedAccessibilityEvent(int changeType) {
+        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+            AccessibilityEvent event = AccessibilityEvent.obtain();
+            event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+            event.setContentChangeTypes(changeType);
+            sendAccessibilityEventUnchecked(event);
+        }
+    }
+
     /**
      * This needs to be a better API (NOT ON VIEW) before it is exposed.  If
      * it is ever exposed at all.
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 0a3d0da..4b18d3a 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -433,9 +433,8 @@
         mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat());
 
         // Size of the screen in bytes, in ARGB_8888 format
-        final WindowManager windowManager = context.getSystemService(WindowManager.class);
-        final Rect maxWindowBounds = windowManager.getMaximumWindowMetrics().getBounds();
-        mMaximumDrawingCacheSize = 4 * maxWindowBounds.width() * maxWindowBounds.height();
+        final Rect maxBounds = config.windowConfiguration.getMaxBounds();
+        mMaximumDrawingCacheSize = 4 * maxBounds.width() * maxBounds.height();
 
         mOverscrollDistance = (int) (sizeAndDensity * OVERSCROLL_DISTANCE + 0.5f);
         mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index 775c15e..49f5229 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -412,6 +412,9 @@
      *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
      *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
      *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
+     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_STARTED}
+     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_CANCELLED}
+     *            <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_DROPPED}
      *            </ul>
      */
     public void notifySubtreeAccessibilityStateChanged(
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 0ecd76f..5a3a9d5 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -291,12 +291,31 @@
      */
     private static final int SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS = 2500;
 
+    /**
+     * If set to {@code true}, the new logic to layout system bars as normal window and to use
+     * layout result to get insets will be applied. Otherwise, the old hard-coded window logic will
+     * be applied.
+     */
+    private static final String USE_FLEXIBLE_INSETS = "persist.debug.flexible_insets";
+
+    /**
+     * A flag to indicate to use the new generalized insets window logic, or the old hard-coded
+     * insets window layout logic.
+     * {@hide}
+     */
+    public static final boolean INSETS_LAYOUT_GENERALIZATION =
+            SystemProperties.getBoolean(USE_FLEXIBLE_INSETS, false);
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
 
     static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<>();
     static boolean sFirstDrawComplete = false;
 
+    private ArrayList<OnBufferTransformHintChangedListener> mTransformHintListeners =
+            new ArrayList<>();
+    private @SurfaceControl.BufferTransform
+            int mPreviousTransformHint = SurfaceControl.BUFFER_TRANSFORM_IDENTITY;
     /**
      * Callback for notifying about global configuration changes.
      */
@@ -633,6 +652,7 @@
     /* Drag/drop */
     ClipDescription mDragDescription;
     View mCurrentDragView;
+    View mStartedDragViewForA11y;
     volatile Object mLocalDragState;
     final PointF mDragPoint = new PointF();
     final PointF mLastTouchPoint = new PointF();
@@ -1118,7 +1138,7 @@
                     controlInsetsForCompatibility(mWindowAttributes);
                     res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                             getHostVisibility(), mDisplay.getDisplayId(), userId,
-                            mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets,
+                            mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
                             mTempControls);
                     if (mTranslator != null) {
                         mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
@@ -2452,7 +2472,7 @@
             mAttachInfo.mContentInsets.set(mLastWindowInsets.getSystemWindowInsets().toRect());
             mAttachInfo.mStableInsets.set(mLastWindowInsets.getStableInsets().toRect());
             mAttachInfo.mVisibleInsets.set(mInsetsController.calculateVisibleInsets(
-                    mWindowAttributes.softInputMode));
+                    mWindowAttributes.softInputMode).toRect());
         }
         return mLastWindowInsets;
     }
@@ -2502,6 +2522,14 @@
                 || lp.type == TYPE_VOLUME_OVERLAY;
     }
 
+    private Rect getWindowBoundsInsetSystemBars() {
+        final Rect bounds = new Rect(
+                mContext.getResources().getConfiguration().windowConfiguration.getBounds());
+        bounds.inset(mInsetsController.getState().calculateInsets(
+                bounds, Type.systemBars(), false /* ignoreVisibility */));
+        return bounds;
+    }
+
     int dipToPx(int dip) {
         final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
         return (int) (displayMetrics.density * dip + 0.5f);
@@ -2588,8 +2616,9 @@
                     || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                 // For wrap content, we have to remeasure later on anyways. Use size consistent with
                 // below so we get best use of the measure cache.
-                desiredWindowWidth = dipToPx(config.screenWidthDp);
-                desiredWindowHeight = dipToPx(config.screenHeightDp);
+                final Rect bounds = getWindowBoundsInsetSystemBars();
+                desiredWindowWidth = bounds.width();
+                desiredWindowHeight = bounds.height();
             } else {
                 // After addToDisplay, the frame contains the frameHint from window manager, which
                 // for most windows is going to be the same size as the result of relayoutWindow.
@@ -2666,9 +2695,9 @@
                         desiredWindowWidth = size.x;
                         desiredWindowHeight = size.y;
                     } else {
-                        Configuration config = res.getConfiguration();
-                        desiredWindowWidth = dipToPx(config.screenWidthDp);
-                        desiredWindowHeight = dipToPx(config.screenHeightDp);
+                        final Rect bounds = getWindowBoundsInsetSystemBars();
+                        desiredWindowWidth = bounds.width();
+                        desiredWindowHeight = bounds.height();
                     }
                 }
             }
@@ -4252,6 +4281,14 @@
         try {
             if (!isContentCaptureEnabled()) return;
 
+            // Initial dispatch of window bounds to content capture
+            if (mAttachInfo.mContentCaptureManager != null) {
+                MainContentCaptureSession session =
+                        mAttachInfo.mContentCaptureManager.getMainContentCaptureSession();
+                session.notifyWindowBoundsChanged(session.getId(),
+                        getConfiguration().windowConfiguration.getBounds());
+            }
+
             // Content capture is a go!
             rootView.dispatchInitialProvideContentCaptureStructure();
         } finally {
@@ -7552,6 +7589,11 @@
             if (what == DragEvent.ACTION_DRAG_STARTED) {
                 mCurrentDragView = null;    // Start the current-recipient tracking
                 mDragDescription = event.mClipDescription;
+                if (mStartedDragViewForA11y != null) {
+                    // Send a drag started a11y event
+                    mStartedDragViewForA11y.sendWindowContentChangedAccessibilityEvent(
+                            AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_STARTED);
+                }
             } else {
                 if (what == DragEvent.ACTION_DRAG_ENDED) {
                     mDragDescription = null;
@@ -7626,6 +7668,16 @@
 
                 // When the drag operation ends, reset drag-related state
                 if (what == DragEvent.ACTION_DRAG_ENDED) {
+                    if (mStartedDragViewForA11y != null) {
+                        // If the drag failed, send a cancelled event from the source. Otherwise,
+                        // the View that accepted the drop sends CONTENT_CHANGE_TYPE_DRAG_DROPPED
+                        if (!event.getResult()) {
+                            mStartedDragViewForA11y.sendWindowContentChangedAccessibilityEvent(
+                                    AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_CANCELLED);
+                        }
+                        mStartedDragViewForA11y.setAccessibilityDragStarted(false);
+                    }
+                    mStartedDragViewForA11y = null;
                     mCurrentDragView = null;
                     setLocalDragState(null);
                     mAttachInfo.mDragToken = null;
@@ -7705,6 +7757,13 @@
         mCurrentDragView = newDragTarget;
     }
 
+    /** Sets the view that started drag and drop for the purpose of sending AccessibilityEvents */
+    void setDragStartedViewForAccessibility(View view) {
+        if (mStartedDragViewForA11y == null) {
+            mStartedDragViewForA11y = view;
+        }
+    }
+
     private AudioManager getAudioManager() {
         if (mView == null) {
             throw new IllegalStateException("getAudioManager called when there is no mView");
@@ -7783,6 +7842,14 @@
                 insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                 mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
                 mTempControls, mSurfaceSize);
+
+        if (mAttachInfo.mContentCaptureManager != null) {
+            MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
+                    .getMainContentCaptureSession();
+            mainSession.notifyWindowBoundsChanged(mainSession.getId(),
+                    getConfiguration().windowConfiguration.getBounds());
+        }
+
         mPendingBackDropFrame.set(mTmpFrames.backdropFrame);
         if (mSurfaceControl.isValid()) {
             if (!useBLAST()) {
@@ -7803,6 +7870,11 @@
                 }
                 mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
             }
+            int transformHint = mSurfaceControl.getTransformHint();
+            if (mPreviousTransformHint != transformHint) {
+                mPreviousTransformHint = transformHint;
+                dispatchTransformHintChanged(transformHint);
+            }
         } else {
             destroySurface();
         }
@@ -9912,7 +9984,10 @@
         if (!mUseMTRenderer) {
             return;
         }
-        mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size());
+        // Only wait if it will report next draw.
+        if (mReportNextDraw) {
+            mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size());
+        }
         for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
             mWindowCallbacks.get(i).onRequestDraw(mReportNextDraw);
         }
@@ -10424,7 +10499,39 @@
         return true;
     }
 
-    int getSurfaceTransformHint() {
+    @Override
+    public @SurfaceControl.BufferTransform int getBufferTransformHint() {
         return mSurfaceControl.getTransformHint();
     }
+
+    @Override
+    public void addOnBufferTransformHintChangedListener(
+            OnBufferTransformHintChangedListener listener) {
+        Objects.requireNonNull(listener);
+        if (mTransformHintListeners.contains(listener)) {
+            throw new IllegalArgumentException(
+                    "attempt to call addOnBufferTransformHintChangedListener() "
+                            + "with a previously registered listener");
+        }
+        mTransformHintListeners.add(listener);
+    }
+
+    @Override
+    public void removeOnBufferTransformHintChangedListener(
+            OnBufferTransformHintChangedListener listener) {
+        Objects.requireNonNull(listener);
+        mTransformHintListeners.remove(listener);
+    }
+
+    private void dispatchTransformHintChanged(@SurfaceControl.BufferTransform int hint) {
+        if (mTransformHintListeners.isEmpty()) {
+            return;
+        }
+        ArrayList<OnBufferTransformHintChangedListener> listeners =
+                (ArrayList<OnBufferTransformHintChangedListener>) mTransformHintListeners.clone();
+        for (int i = 0; i < listeners.size(); i++) {
+            OnBufferTransformHintChangedListener listener = listeners.get(i);
+            listener.onBufferTransformHintChanged(hint);
+        }
+    }
 }
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index ce882da..efffa2b 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -149,10 +149,10 @@
     }
 
     @Override
-    public void onInsetsModified(InsetsState insetsState) {
+    public void updateRequestedVisibilities(InsetsVisibilities vis) {
         try {
             if (mViewRoot.mAdded) {
-                mViewRoot.mWindowSession.insetsModified(mViewRoot.mWindow, insetsState);
+                mViewRoot.mWindowSession.updateRequestedVisibilities(mViewRoot.mWindow, vis);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to call insetsModified", e);
diff --git a/core/java/android/view/WindowInfo.java b/core/java/android/view/WindowInfo.java
index 57dfc62..1edbbba 100644
--- a/core/java/android/view/WindowInfo.java
+++ b/core/java/android/view/WindowInfo.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import android.app.ActivityTaskManager;
 import android.graphics.Region;
 import android.os.IBinder;
 import android.os.Parcel;
@@ -51,6 +52,7 @@
     public boolean inPictureInPicture;
     public boolean hasFlagWatchOutsideTouch;
     public int displayId = Display.INVALID_DISPLAY;
+    public int taskId = ActivityTaskManager.INVALID_TASK_ID;
 
     private WindowInfo() {
         /* do nothing - hide constructor */
@@ -67,6 +69,7 @@
     public static WindowInfo obtain(WindowInfo other) {
         WindowInfo window = obtain();
         window.displayId = other.displayId;
+        window.taskId = other.taskId;
         window.type = other.type;
         window.layer = other.layer;
         window.token = other.token;
@@ -103,6 +106,7 @@
     @Override
     public void writeToParcel(Parcel parcel, int flags) {
         parcel.writeInt(displayId);
+        parcel.writeInt(taskId);
         parcel.writeInt(type);
         parcel.writeInt(layer);
         parcel.writeStrongBinder(token);
@@ -129,6 +133,7 @@
         builder.append("WindowInfo[");
         builder.append("title=").append(title);
         builder.append(", displayId=").append(displayId);
+        builder.append(", taskId=").append(taskId);
         builder.append(", type=").append(type);
         builder.append(", layer=").append(layer);
         builder.append(", token=").append(token);
@@ -146,6 +151,7 @@
 
     private void initFromParcel(Parcel parcel) {
         displayId = parcel.readInt();
+        taskId = parcel.readInt();
         type = parcel.readInt();
         layer = parcel.readInt();
         token = parcel.readStrongBinder();
@@ -169,6 +175,7 @@
 
     private void clear() {
         displayId = Display.INVALID_DISPLAY;
+        taskId = ActivityTaskManager.INVALID_TASK_ID;
         type = 0;
         layer = 0;
         token = null;
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 0f1a9d9..cde1cc7 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -903,6 +903,16 @@
         result.append(mPrivacyIndicatorBounds != null ? "privacyIndicatorBounds="
                 + mPrivacyIndicatorBounds : "");
         result.append("\n    ");
+        result.append("compatInsetsTypes=" + mCompatInsetsTypes);
+        result.append("\n    ");
+        result.append("compatIgnoreVisibility=" + mCompatIgnoreVisibility);
+        result.append("\n    ");
+        result.append("systemWindowInsetsConsumed=" + mSystemWindowInsetsConsumed);
+        result.append("\n    ");
+        result.append("stableInsetsConsumed=" + mStableInsetsConsumed);
+        result.append("\n    ");
+        result.append("displayCutoutConsumed=" + mDisplayCutoutConsumed);
+        result.append("\n    ");
         result.append(isRound() ? "round" : "");
         result.append("}");
         return result.toString();
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 55beae0f..51cd95e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -97,6 +97,7 @@
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
+import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -122,6 +123,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -319,6 +321,25 @@
     int TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE = 27;
 
     /**
+     * A window in a new task fragment is being opened.
+     * @hide
+     */
+    int TRANSIT_OLD_TASK_FRAGMENT_OPEN = 28;
+
+    /**
+     * A window in the top-most activity of task fragment is being closed to reveal the activity
+     * below.
+     * @hide
+     */
+    int TRANSIT_OLD_TASK_FRAGMENT_CLOSE = 29;
+
+    /**
+     * A window of task fragment is changing bounds.
+     * @hide
+     */
+    int TRANSIT_OLD_TASK_FRAGMENT_CHANGE = 30;
+
+    /**
      * @hide
      */
     @IntDef(prefix = { "TRANSIT_OLD_" }, value = {
@@ -343,7 +364,10 @@
             TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN,
             TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE,
             TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE,
-            TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE
+            TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE,
+            TRANSIT_OLD_TASK_FRAGMENT_OPEN,
+            TRANSIT_OLD_TASK_FRAGMENT_CLOSE,
+            TRANSIT_OLD_TASK_FRAGMENT_CHANGE
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface TransitionOldType {}
@@ -380,8 +404,11 @@
     int TRANSIT_CHANGE = 6;
     /**
      * The keyguard was visible and has been dismissed.
+     * @deprecated use {@link #TRANSIT_TO_BACK} + {@link #TRANSIT_FLAG_KEYGUARD_GOING_AWAY} for
+     *             keyguard going away with Shell transition.
      * @hide
      */
+    @Deprecated
     int TRANSIT_KEYGUARD_GOING_AWAY = 7;
     /**
      * A window is appearing above a locked keyguard.
@@ -394,6 +421,16 @@
      */
     int TRANSIT_KEYGUARD_UNOCCLUDE = 9;
     /**
+     * A window is starting to enter PiP.
+     * @hide
+     */
+    int TRANSIT_PIP = 10;
+    /**
+     * The screen is turning on.
+     * @hide
+     */
+    int TRANSIT_WAKE = 11;
+    /**
      * The first slot for custom transition types. Callers (like Shell) can make use of custom
      * transition types for dealing with special cases. These types are effectively ignored by
      * Core and will just be passed along as part of TransitionInfo objects. An example is
@@ -402,7 +439,7 @@
      * implementation.
      * @hide
      */
-    int TRANSIT_FIRST_CUSTOM = 10;
+    int TRANSIT_FIRST_CUSTOM = 12;
 
     /**
      * @hide
@@ -418,6 +455,8 @@
             TRANSIT_KEYGUARD_GOING_AWAY,
             TRANSIT_KEYGUARD_OCCLUDE,
             TRANSIT_KEYGUARD_UNOCCLUDE,
+            TRANSIT_PIP,
+            TRANSIT_WAKE,
             TRANSIT_FIRST_CUSTOM
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -467,6 +506,19 @@
     int TRANSIT_FLAG_KEYGUARD_LOCKED = 0x40;
 
     /**
+     * Transition flag: Indicates that this transition is for recents animation.
+     * TODO(b/188669821): Remove once special-case logic moves to shell.
+     * @hide
+     */
+    int TRANSIT_FLAG_IS_RECENTS = 0x80;
+
+    /**
+     * Transition flag: Indicates that keyguard should go away with this transition.
+     * @hide
+     */
+    int TRANSIT_FLAG_KEYGUARD_GOING_AWAY = 0x100;
+
+    /**
      * @hide
      */
     @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
@@ -476,7 +528,9 @@
             TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION,
             TRANSIT_FLAG_APP_CRASHED,
             TRANSIT_FLAG_OPEN_BEHIND,
-            TRANSIT_FLAG_KEYGUARD_LOCKED
+            TRANSIT_FLAG_KEYGUARD_LOCKED,
+            TRANSIT_FLAG_IS_RECENTS,
+            TRANSIT_FLAG_KEYGUARD_GOING_AWAY
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface TransitionFlags {}
@@ -661,6 +715,20 @@
     }
 
     /**
+     * Returns a set of {@link WindowMetrics} for the given display. Each WindowMetrics instance
+     * is the maximum WindowMetrics for a device state, including rotations. This is not guaranteed
+     * to include all possible device states.
+     *
+     * This API can only be used by Launcher.
+     *
+     * @param displayId the id of the logical display
+     * @hide
+     */
+    default @NonNull Set<WindowMetrics> getPossibleMaximumWindowMetrics(int displayId) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
      * Used to asynchronously request Keyboard Shortcuts from the focused window.
      *
      * @hide
@@ -922,6 +990,8 @@
             case TRANSIT_KEYGUARD_GOING_AWAY: return "KEYGUARD_GOING_AWAY";
             case TRANSIT_KEYGUARD_OCCLUDE: return "KEYGUARD_OCCLUDE";
             case TRANSIT_KEYGUARD_UNOCCLUDE: return "KEYGUARD_UNOCCLUDE";
+            case TRANSIT_PIP: return "PIP";
+            case TRANSIT_WAKE: return "WAKE";
             case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM";
             default:
                 if (type > TRANSIT_FIRST_CUSTOM) {
@@ -3468,6 +3538,30 @@
         public @InsetsState.InternalInsetsType int[] providesInsetsTypes;
 
         /**
+         * If specified, the insets provided by this window will be our window frame minus the
+         * insets specified by providedInternalInsets.
+         *
+         * @hide
+         */
+        public Insets providedInternalInsets = Insets.NONE;
+
+        /**
+         * If specified, the insets provided by this window for the IME will be our window frame
+         * minus the insets specified by providedInternalImeInsets.
+         *
+         * @hide
+         */
+        public Insets providedInternalImeInsets = Insets.NONE;
+
+        /**
+         * {@link LayoutParams} to be applied to the window when layout with a assigned rotation.
+         * This will make layout during rotation change smoothly.
+         *
+         * @hide
+         */
+        public LayoutParams[] paramsForRotation;
+
+        /**
          * Specifies types of insets that this window should avoid overlapping during layout.
          *
          * @param types which {@link WindowInsets.Type}s of insets that this window should avoid.
@@ -3566,6 +3660,18 @@
             return mFitInsetsIgnoringVisibility;
         }
 
+        private void checkNonRecursiveParams() {
+            if (paramsForRotation == null) {
+                return;
+            }
+            for (int i = paramsForRotation.length - 1; i >= 0; i--) {
+                if (paramsForRotation[i].paramsForRotation != null) {
+                    throw new IllegalArgumentException(
+                            "Params cannot contain params recursively.");
+                }
+            }
+        }
+
         public LayoutParams() {
             super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
             type = TYPE_APPLICATION;
@@ -3820,6 +3926,15 @@
             } else {
                 out.writeInt(0);
             }
+            providedInternalInsets.writeToParcel(out, 0 /* parcelableFlags */);
+            providedInternalImeInsets.writeToParcel(out, 0 /* parcelableFlags */);
+            if (paramsForRotation != null) {
+                checkNonRecursiveParams();
+                out.writeInt(paramsForRotation.length);
+                out.writeTypedArray(paramsForRotation, 0 /* parcelableFlags */);
+            } else {
+                out.writeInt(0);
+            }
         }
 
         public static final @android.annotation.NonNull Parcelable.Creator<LayoutParams> CREATOR
@@ -3891,6 +4006,13 @@
                 providesInsetsTypes = new int[insetsTypesLength];
                 in.readIntArray(providesInsetsTypes);
             }
+            providedInternalInsets = Insets.CREATOR.createFromParcel(in);
+            providedInternalImeInsets = Insets.CREATOR.createFromParcel(in);
+            int paramsForRotationLength = in.readInt();
+            if (paramsForRotationLength > 0) {
+                paramsForRotation = new LayoutParams[paramsForRotationLength];
+                in.readTypedArray(paramsForRotation, LayoutParams.CREATOR);
+            }
         }
 
         @SuppressWarnings({"PointlessBitwiseExpression"})
@@ -4187,6 +4309,22 @@
                 changes |= LAYOUT_CHANGED;
             }
 
+            if (!providedInternalInsets.equals(o.providedInternalInsets)) {
+                providedInternalInsets = o.providedInternalInsets;
+                changes |= LAYOUT_CHANGED;
+            }
+
+            if (!providedInternalImeInsets.equals(o.providedInternalImeInsets)) {
+                providedInternalImeInsets = o.providedInternalImeInsets;
+                changes |= LAYOUT_CHANGED;
+            }
+
+            if (!Arrays.equals(paramsForRotation, o.paramsForRotation)) {
+                paramsForRotation = o.paramsForRotation;
+                checkNonRecursiveParams();
+                changes |= LAYOUT_CHANGED;
+            }
+
             return changes;
         }
 
@@ -4382,6 +4520,22 @@
                     sb.append(InsetsState.typeToString(providesInsetsTypes[i]));
                 }
             }
+            if (!providedInternalInsets.equals(Insets.NONE)) {
+                sb.append(" providedInternalInsets=");
+                sb.append(providedInternalInsets);
+            }
+            if (!providedInternalImeInsets.equals(Insets.NONE)) {
+                sb.append(" providedInternalImeInsets=");
+                sb.append(providedInternalImeInsets);
+            }
+            if (paramsForRotation != null && paramsForRotation.length != 0) {
+                sb.append(System.lineSeparator());
+                sb.append(prefix).append("  paramsForRotation=");
+                for (int i = 0; i < paramsForRotation.length; ++i) {
+                    if (i > 0) sb.append(' ');
+                    sb.append(paramsForRotation[i].toString());
+                }
+            }
 
             sb.append('}');
             return sb.toString();
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 18013e8..c92a3a0 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -18,6 +18,7 @@
 
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentCallbacks2;
@@ -709,6 +710,16 @@
             }
         }
     }
+
+    /** @hide */
+    @Nullable
+    public SurfaceControl mirrorWallpaperSurface(int displayId) {
+        try {
+            return getWindowManagerService().mirrorWallpaperSurface(displayId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
 
 final class WindowLeaked extends AndroidRuntimeException {
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index f8009919..1c915cb 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -16,12 +16,12 @@
 
 package android.view;
 
-import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
-import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
-import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
-import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+import static android.window.WindowProviderService.isWindowProviderService;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
@@ -36,12 +36,16 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.StrictMode;
 import android.window.WindowContext;
+import android.window.WindowProvider;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IResultReceiver;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -145,6 +149,7 @@
             throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
         }
         final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
+        assertWindowContextTypeMatches(wparams.type);
         // Only use the default token if we don't have a parent window and a token.
         if (mDefaultToken != null && mParentWindow == null && wparams.token == null) {
             wparams.token = mDefaultToken;
@@ -152,6 +157,34 @@
         wparams.mWindowContextToken = mWindowContextToken;
     }
 
+    private void assertWindowContextTypeMatches(@LayoutParams.WindowType int windowType) {
+        if (!(mContext instanceof WindowProvider)) {
+            return;
+        }
+        // Don't need to check sub-window type because sub window should be allowed to be attached
+        // to the parent window.
+        if (windowType >= FIRST_SUB_WINDOW && windowType <= LAST_SUB_WINDOW) {
+            return;
+        }
+        final WindowProvider windowProvider = (WindowProvider) mContext;
+        if (windowProvider.getWindowType() == windowType) {
+            return;
+        }
+        IllegalArgumentException exception = new IllegalArgumentException("Window type mismatch."
+                + " Window Context's window type is " + windowProvider.getWindowType()
+                + ", while LayoutParams' type is set to " + windowType + "."
+                + " Please create another Window Context via"
+                + " createWindowContext(getDisplay(), " + windowType + ", null)"
+                + " to add window with type:" + windowType);
+        if (!isWindowProviderService(windowProvider.getWindowContextOptions())) {
+            throw exception;
+        }
+        // Throw IncorrectCorrectViolation if the Window Context is allowed to provide multiple
+        // window types. Usually it's because the Window Context is a WindowProviderService.
+        StrictMode.onIncorrectContextUsed("WindowContext's window type must"
+                + " match type in WindowManager.LayoutParams", exception);
+    }
+
     @Override
     public void removeView(View view) {
         mGlobal.removeView(view, false);
@@ -270,34 +303,86 @@
     private WindowInsets computeWindowInsets(Rect bounds) {
         // Initialize params which used for obtaining all system insets.
         final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
-        params.flags = FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR;
         final Context context = (mParentWindow != null) ? mParentWindow.getContext() : mContext;
         params.token = Context.getToken(context);
-        params.systemUiVisibility = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
-        params.setFitInsetsTypes(0);
-        params.setFitInsetsSides(0);
-
-        return getWindowInsetsFromServer(params, bounds);
+        return getWindowInsetsFromServerForCurrentDisplay(params, bounds);
     }
 
-    private WindowInsets getWindowInsetsFromServer(WindowManager.LayoutParams attrs, Rect bounds) {
+    private WindowInsets getWindowInsetsFromServerForCurrentDisplay(
+            WindowManager.LayoutParams attrs, Rect bounds) {
+        final Configuration config = mContext.getResources().getConfiguration();
+        return getWindowInsetsFromServerForDisplay(mContext.getDisplayId(), attrs, bounds,
+                config.isScreenRound(), config.windowConfiguration.getWindowingMode());
+    }
+
+    /**
+     * Retrieves WindowInsets for the given context and display, given the window bounds.
+     *
+     * @param displayId the ID of the logical display to calculate insets for
+     * @param attrs the LayoutParams for the calling app
+     * @param bounds the window bounds to calculate insets for
+     * @param isScreenRound if the display identified by displayId is round
+     * @param windowingMode the windowing mode of the window to calculate insets for
+     * @return WindowInsets calculated for the given window bounds, on the given display
+     */
+    private static WindowInsets getWindowInsetsFromServerForDisplay(int displayId,
+            WindowManager.LayoutParams attrs, Rect bounds, boolean isScreenRound,
+            int windowingMode) {
         try {
             final InsetsState insetsState = new InsetsState();
             final boolean alwaysConsumeSystemBars = WindowManagerGlobal.getWindowManagerService()
-                    .getWindowInsets(attrs, mContext.getDisplayId(), insetsState);
-            final Configuration config = mContext.getResources().getConfiguration();
-            final boolean isScreenRound = config.isScreenRound();
-            final int windowingMode = config.windowConfiguration.getWindowingMode();
+                    .getWindowInsets(attrs, displayId, insetsState);
             return insetsState.calculateInsets(bounds, null /* ignoringVisibilityState*/,
                     isScreenRound, alwaysConsumeSystemBars, SOFT_INPUT_ADJUST_NOTHING, attrs.flags,
-                    SYSTEM_UI_FLAG_VISIBLE, attrs.type, windowingMode, null /* typeSideMap */);
+                    SYSTEM_UI_FLAG_VISIBLE, attrs.type, windowingMode,
+                    null /* typeSideMap */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
     @Override
+    @NonNull
+    public Set<WindowMetrics> getPossibleMaximumWindowMetrics(int displayId) {
+        List<DisplayInfo> possibleDisplayInfos;
+        try {
+            possibleDisplayInfos = WindowManagerGlobal.getWindowManagerService()
+                    .getPossibleDisplayInfo(displayId, mContext.getPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        Set<WindowMetrics> maxMetrics = new HashSet<>();
+        WindowInsets windowInsets;
+        DisplayInfo currentDisplayInfo;
+        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        for (int i = 0; i < possibleDisplayInfos.size(); i++) {
+            currentDisplayInfo = possibleDisplayInfos.get(i);
+
+            // Calculate max bounds for this rotation and state.
+            Rect maxBounds = new Rect(0, 0, currentDisplayInfo.logicalWidth,
+                    currentDisplayInfo.logicalHeight);
+
+            // Calculate insets for the rotated max bounds.
+            final boolean isScreenRound = (currentDisplayInfo.flags & Display.FLAG_ROUND) != 0;
+            // Initialize insets based upon display rotation. Note any window-provided insets
+            // will not be set.
+            windowInsets = getWindowInsetsFromServerForDisplay(
+                    currentDisplayInfo.displayId, params,
+                    new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
+                            currentDisplayInfo.getNaturalHeight()), isScreenRound,
+                    WINDOWING_MODE_FULLSCREEN);
+            // Set the hardware-provided insets.
+            windowInsets = new WindowInsets.Builder(windowInsets).setRoundedCorners(
+                    currentDisplayInfo.roundedCorners)
+                    .setDisplayCutout(currentDisplayInfo.displayCutout).build();
+
+            maxMetrics.add(new WindowMetrics(maxBounds, windowInsets));
+        }
+        return maxMetrics;
+    }
+
+    @Override
     public void holdLock(IBinder token, int durationMs) {
         try {
             WindowManagerGlobal.getWindowManagerService().holdLock(token, durationMs);
diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java
index bbef3e6..e634d60 100644
--- a/core/java/android/view/WindowManagerPolicyConstants.java
+++ b/core/java/android/view/WindowManagerPolicyConstants.java
@@ -209,4 +209,41 @@
                 return Integer.toString(why);
         }
     }
+
+    /**
+     * How much to multiply the policy's type layer, to reserve room
+     * for multiple windows of the same type and Z-ordering adjustment
+     * with TYPE_LAYER_OFFSET.
+     */
+    int TYPE_LAYER_MULTIPLIER = 10000;
+
+    /**
+     * Offset from TYPE_LAYER_MULTIPLIER for moving a group of windows above
+     * or below others in the same layer.
+     */
+    int TYPE_LAYER_OFFSET = 1000;
+
+    /**
+     * How much to increment the layer for each window, to reserve room
+     * for effect surfaces between them.
+     */
+    int WINDOW_LAYER_MULTIPLIER = 5;
+
+    /**
+     * Animation thumbnail is as far as possible below the window above
+     * the thumbnail (or in other words as far as possible above the window
+     * below it).
+     */
+    int LAYER_OFFSET_THUMBNAIL = WINDOW_LAYER_MULTIPLIER - 1;
+
+    int SPLIT_DIVIDER_LAYER = TYPE_LAYER_MULTIPLIER * 3;
+    int WATERMARK_LAYER = TYPE_LAYER_MULTIPLIER * 100;
+    int STRICT_MODE_LAYER = TYPE_LAYER_MULTIPLIER * 101;
+    int WINDOW_FREEZE_LAYER = TYPE_LAYER_MULTIPLIER * 200;
+
+    /**
+     * Layers for screen rotation animation. We put these layers above
+     * WINDOW_FREEZE_LAYER so that screen freeze will cover all windows.
+     */
+    int SCREEN_FREEZE_LAYER_BASE = WINDOW_FREEZE_LAYER + TYPE_LAYER_MULTIPLIER;
 }
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index ae54f51..ffb7efa 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -135,7 +135,7 @@
      */
     @Override
     public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, InsetsState requestedVisibility,
+            int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls) {
         final SurfaceControl.Builder b = new SurfaceControl.Builder(mSurfaceSession)
@@ -181,10 +181,10 @@
      */
     @Override
     public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, int userId, InsetsState requestedVisibility,
+            int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls) {
-        return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibility,
+        return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibilities,
                 outInputChannel, outInsetsState, outActiveControls);
     }
 
@@ -454,7 +454,7 @@
     }
 
     @Override
-    public void insetsModified(android.view.IWindow window, android.view.InsetsState state) {
+    public void updateRequestedVisibilities(IWindow window, InsetsVisibilities visibilities)  {
     }
 
     @Override
@@ -498,4 +498,9 @@
     public void generateDisplayHash(IWindow window, Rect boundsInWindow, String hashAlgorithm,
             RemoteCallback callback) {
     }
+
+    @Override
+    public boolean dropForAccessibility(IWindow window, int x, int y) {
+        return false;
+    }
 }
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index f6d6fde..3b65ffd 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -613,6 +613,36 @@
     public static final int CONTENT_CHANGE_TYPE_STATE_DESCRIPTION = 0x00000040;
 
     /**
+     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+     * A drag has started while accessibility is enabled. This is either via an
+     * AccessibilityAction, or via touch events. This is sent from the source that initiated the
+     * drag.
+     *
+     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_START
+     */
+    public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 0x00000080;
+
+    /**
+     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+     * A drag in with accessibility enabled has ended. This means the content has been
+     * successfully dropped. This is sent from the target that accepted the dragged content.
+     *
+     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_DROP
+     */
+    public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 0x00000100;
+
+    /**
+     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+     * A drag in with accessibility enabled has ended. This means the content has been
+     * unsuccessfully dropped, the user has canceled the action via an AccessibilityAction, or
+     * no drop has been detected within a timeout and the drag was automatically cancelled. This is
+     * sent from the source that initiated the drag.
+     *
+     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_CANCEL
+     */
+    public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 0x0000200;
+
+    /**
      * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
      * The window was added.
      */
@@ -710,7 +740,10 @@
                     CONTENT_CHANGE_TYPE_STATE_DESCRIPTION,
                     CONTENT_CHANGE_TYPE_PANE_TITLE,
                     CONTENT_CHANGE_TYPE_PANE_APPEARED,
-                    CONTENT_CHANGE_TYPE_PANE_DISAPPEARED
+                    CONTENT_CHANGE_TYPE_PANE_DISAPPEARED,
+                    CONTENT_CHANGE_TYPE_DRAG_STARTED,
+                    CONTENT_CHANGE_TYPE_DRAG_DROPPED,
+                    CONTENT_CHANGE_TYPE_DRAG_CANCELLED
             })
     public @interface ContentChangeTypes {}
 
@@ -959,6 +992,9 @@
             case CONTENT_CHANGE_TYPE_PANE_APPEARED: return "CONTENT_CHANGE_TYPE_PANE_APPEARED";
             case CONTENT_CHANGE_TYPE_PANE_DISAPPEARED:
                 return "CONTENT_CHANGE_TYPE_PANE_DISAPPEARED";
+            case CONTENT_CHANGE_TYPE_DRAG_STARTED: return "CONTENT_CHANGE_TYPE_DRAG_STARTED";
+            case CONTENT_CHANGE_TYPE_DRAG_DROPPED: return "CONTENT_CHANGE_TYPE_DRAG_DROPPED";
+            case CONTENT_CHANGE_TYPE_DRAG_CANCELLED: return "CONTENT_CHANGE_TYPE_DRAG_CANCELLED";
             default: return Integer.toHexString(type);
         }
     }
@@ -1017,6 +1053,7 @@
     /**
      * Sets the event type.
      *
+     * <b>Note: An event must represent a single event type.</b>
      * @param eventType The event type.
      *
      * @throws IllegalStateException If called from an AccessibilityService.
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index dd81dd9..aac09b8 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -16,6 +16,9 @@
 
 package android.view.accessibility;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CLIENT;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK;
+
 import android.accessibilityservice.IAccessibilityServiceConnection;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -86,6 +89,7 @@
     public static final int NO_ID = -1;
 
     public static final String CALL_STACK = "call_stack";
+    public static final String IGNORE_CALL_STACK = "ignore_call_stack";
 
     private static final String LOG_TAG = "AccessibilityInteractionClient";
 
@@ -121,6 +125,12 @@
 
     private volatile int mInteractionId = -1;
     private volatile int mCallingUid = Process.INVALID_UID;
+    // call stack for IAccessibilityInteractionConnectionCallback APIs. These callback APIs are
+    // shared by multiple requests APIs in IAccessibilityServiceConnection. To correctly log the
+    // request API which triggers the callback, we log trace entries for callback after the
+    // request API thread waiting for the callback returns. To log the correct callback stack in
+    // the request API thread, we save the callback stack in this member variables.
+    private List<StackTraceElement> mCallStackOfCallback;
 
     private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
 
@@ -307,18 +317,30 @@
                         if (DEBUG) {
                             Log.i(LOG_TAG, "Window cache hit");
                         }
+                        if (shouldTraceClient()) {
+                            logTraceClient(connection, "getWindow cache",
+                                    "connectionId=" + connectionId + ";accessibilityWindowId="
+                                    + accessibilityWindowId + ";bypassCache=false");
+                        }
                         return window;
                     }
                     if (DEBUG) {
                         Log.i(LOG_TAG, "Window cache miss");
                     }
                 }
+
                 final long identityToken = Binder.clearCallingIdentity();
                 try {
                     window = connection.getWindow(accessibilityWindowId);
                 } finally {
                     Binder.restoreCallingIdentity(identityToken);
                 }
+                if (shouldTraceClient()) {
+                    logTraceClient(connection, "getWindow", "connectionId=" + connectionId
+                            + ";accessibilityWindowId=" + accessibilityWindowId + ";bypassCache="
+                            + bypassCache);
+                }
+
                 if (window != null) {
                     if (!bypassCache) {
                         sAccessibilityCache.addWindow(window);
@@ -368,6 +390,10 @@
                     if (DEBUG) {
                         Log.i(LOG_TAG, "Windows cache hit");
                     }
+                    if (shouldTraceClient()) {
+                        logTraceClient(
+                                connection, "getWindows cache", "connectionId=" + connectionId);
+                    }
                     return windows;
                 }
                 if (DEBUG) {
@@ -379,6 +405,9 @@
                 } finally {
                     Binder.restoreCallingIdentity(identityToken);
                 }
+                if (shouldTraceClient()) {
+                    logTraceClient(connection, "getWindows", "connectionId=" + connectionId);
+                }
                 if (windows != null) {
                     sAccessibilityCache.setWindowsOnAllDisplays(windows);
                     return windows;
@@ -472,6 +501,15 @@
                             Log.i(LOG_TAG, "Node cache hit for "
                                     + idToString(accessibilityWindowId, accessibilityNodeId));
                         }
+                        if (shouldTraceClient()) {
+                            logTraceClient(connection,
+                                    "findAccessibilityNodeInfoByAccessibilityId cache",
+                                    "connectionId=" + connectionId + ";accessibilityWindowId="
+                                    + accessibilityWindowId + ";accessibilityNodeId="
+                                    + accessibilityNodeId + ";bypassCache=" + bypassCache
+                                    + ";prefetchFlags=" + prefetchFlags + ";arguments="
+                                    + arguments);
+                        }
                         return cachedInfo;
                     }
                     if (DEBUG) {
@@ -488,6 +526,14 @@
                     prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
                 }
                 final int interactionId = mInteractionIdCounter.getAndIncrement();
+                if (shouldTraceClient()) {
+                    logTraceClient(connection, "findAccessibilityNodeInfoByAccessibilityId",
+                            "InteractionId:" + interactionId + "connectionId=" + connectionId
+                            + ";accessibilityWindowId=" + accessibilityWindowId
+                            + ";accessibilityNodeId=" + accessibilityNodeId + ";bypassCache="
+                            + bypassCache + ";prefetchFlags=" + prefetchFlags + ";arguments="
+                            + arguments);
+                }
                 final String[] packageNames;
                 final long identityToken = Binder.clearCallingIdentity();
                 try {
@@ -500,16 +546,10 @@
                 if (packageNames != null) {
                     AccessibilityNodeInfo info =
                             getFindAccessibilityNodeInfoResultAndClear(interactionId);
-                    if (mAccessibilityManager != null
-                            && mAccessibilityManager.isAccessibilityTracingEnabled()) {
-                        logTrace(connection, "findAccessibilityNodeInfoByAccessibilityId",
-                                "InteractionId:" + interactionId + ";Result: " + info
-                                        + ";connectionId=" + connectionId
-                                        + ";accessibilityWindowId="
-                                        + accessibilityWindowId + ";accessibilityNodeId="
-                                        + accessibilityNodeId + ";bypassCache=" + bypassCache
-                                        + ";prefetchFlags=" + prefetchFlags
-                                        + ";arguments=" + arguments);
+                    if (shouldTraceCallback()) {
+                        logTraceCallback(connection, "findAccessibilityNodeInfoByAccessibilityId",
+                                "InteractionId:" + interactionId + ";connectionId="
+                                + connectionId + ";Result: " + info);
                     }
                     if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
                             && info != null) {
@@ -571,6 +611,14 @@
                 final String[] packageNames;
                 final long identityToken = Binder.clearCallingIdentity();
                 try {
+                    if (shouldTraceClient()) {
+                        logTraceClient(connection, "findAccessibilityNodeInfosByViewId",
+                                "InteractionId=" + interactionId + ";connectionId=" + connectionId
+                                + ";accessibilityWindowId=" + accessibilityWindowId
+                                + ";accessibilityNodeId=" + accessibilityNodeId + ";viewId="
+                                + viewId);
+                    }
+
                     packageNames = connection.findAccessibilityNodeInfosByViewId(
                             accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
                             Thread.currentThread().getId());
@@ -581,13 +629,10 @@
                 if (packageNames != null) {
                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
                             interactionId);
-                    if (mAccessibilityManager != null
-                            && mAccessibilityManager.isAccessibilityTracingEnabled()) {
-                        logTrace(connection, "findAccessibilityNodeInfosByViewId", "InteractionId="
-                                + interactionId + ":Result: " + infos + ";connectionId="
-                                + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
-                                + ";accessibilityNodeId=" + accessibilityNodeId + ";viewId="
-                                + viewId);
+                    if (shouldTraceCallback()) {
+                        logTraceCallback(connection, "findAccessibilityNodeInfosByViewId",
+                                "InteractionId=" + interactionId + ";connectionId=" + connectionId
+                                + ":Result: " + infos);
                     }
                     if (infos != null) {
                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
@@ -630,6 +675,12 @@
             IAccessibilityServiceConnection connection = getConnection(connectionId);
             if (connection != null) {
                 final int interactionId = mInteractionIdCounter.getAndIncrement();
+                if (shouldTraceClient()) {
+                    logTraceClient(connection, "findAccessibilityNodeInfosByText",
+                            "InteractionId:" + interactionId + "connectionId=" + connectionId
+                            + ";accessibilityWindowId=" + accessibilityWindowId
+                            + ";accessibilityNodeId=" + accessibilityNodeId + ";text=" + text);
+                }
                 final String[] packageNames;
                 final long identityToken = Binder.clearCallingIdentity();
                 try {
@@ -643,12 +694,10 @@
                 if (packageNames != null) {
                     List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
                             interactionId);
-                    if (mAccessibilityManager != null
-                            && mAccessibilityManager.isAccessibilityTracingEnabled()) {
-                        logTrace(connection, "findAccessibilityNodeInfosByText", "InteractionId="
-                                + interactionId + ":Result: " + infos + ";connectionId="
-                                + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
-                                + ";accessibilityNodeId=" + accessibilityNodeId + ";text=" + text);
+                    if (shouldTraceCallback()) {
+                        logTraceCallback(connection, "findAccessibilityNodeInfosByText",
+                                "InteractionId=" + interactionId + ";connectionId=" + connectionId
+                                + ";Result: " + infos);
                     }
                     if (infos != null) {
                         finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
@@ -690,6 +739,13 @@
             IAccessibilityServiceConnection connection = getConnection(connectionId);
             if (connection != null) {
                 final int interactionId = mInteractionIdCounter.getAndIncrement();
+                if (shouldTraceClient()) {
+                    logTraceClient(connection, "findFocus",
+                            "InteractionId:" + interactionId + "connectionId=" + connectionId
+                            + ";accessibilityWindowId=" + accessibilityWindowId
+                            + ";accessibilityNodeId=" + accessibilityNodeId + ";focusType="
+                            + focusType);
+                }
                 final String[] packageNames;
                 final long identityToken = Binder.clearCallingIdentity();
                 try {
@@ -703,13 +759,9 @@
                 if (packageNames != null) {
                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
                             interactionId);
-                    if (mAccessibilityManager != null
-                            && mAccessibilityManager.isAccessibilityTracingEnabled()) {
-                        logTrace(connection, "findFocus", "InteractionId=" + interactionId
-                                + ":Result: " + info + ";connectionId=" + connectionId
-                                + ";accessibilityWindowId=" + accessibilityWindowId
-                                + ";accessibilityNodeId=" + accessibilityNodeId + ";focusType="
-                                + focusType);
+                    if (shouldTraceCallback()) {
+                        logTraceCallback(connection, "findFocus", "InteractionId=" + interactionId
+                                + ";connectionId=" + connectionId + ";Result:" + info);
                     }
                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
                     return info;
@@ -747,6 +799,13 @@
             IAccessibilityServiceConnection connection = getConnection(connectionId);
             if (connection != null) {
                 final int interactionId = mInteractionIdCounter.getAndIncrement();
+                if (shouldTraceClient()) {
+                    logTraceClient(connection, "focusSearch",
+                            "InteractionId:" + interactionId + "connectionId=" + connectionId
+                            + ";accessibilityWindowId=" + accessibilityWindowId
+                            + ";accessibilityNodeId=" + accessibilityNodeId + ";direction="
+                            + direction);
+                }
                 final String[] packageNames;
                 final long identityToken = Binder.clearCallingIdentity();
                 try {
@@ -761,13 +820,9 @@
                     AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
                             interactionId);
                     finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
-                    if (mAccessibilityManager != null
-                            && mAccessibilityManager.isAccessibilityTracingEnabled()) {
-                        logTrace(connection, "focusSearch", "InteractionId=" + interactionId
-                                + ":Result: " + info + ";connectionId=" + connectionId
-                                + ";accessibilityWindowId=" + accessibilityWindowId
-                                + ";accessibilityNodeId=" + accessibilityNodeId + ";direction="
-                                + direction);
+                    if (shouldTraceCallback()) {
+                        logTraceCallback(connection, "focusSearch", "InteractionId=" + interactionId
+                                + ";connectionId=" + connectionId + ";Result:" + info);
                     }
                     return info;
                 }
@@ -803,6 +858,13 @@
             IAccessibilityServiceConnection connection = getConnection(connectionId);
             if (connection != null) {
                 final int interactionId = mInteractionIdCounter.getAndIncrement();
+                if (shouldTraceClient()) {
+                    logTraceClient(connection, "performAccessibilityAction",
+                            "InteractionId:" + interactionId + "connectionId=" + connectionId
+                            + ";accessibilityWindowId=" + accessibilityWindowId
+                            + ";accessibilityNodeId=" + accessibilityNodeId + ";action=" + action
+                            + ";arguments=" + arguments);
+                }
                 final boolean success;
                 final long identityToken = Binder.clearCallingIdentity();
                 try {
@@ -816,13 +878,10 @@
                 if (success) {
                     final boolean result =
                             getPerformAccessibilityActionResultAndClear(interactionId);
-                    if (mAccessibilityManager != null
-                            && mAccessibilityManager.isAccessibilityTracingEnabled()) {
-                        logTrace(connection, "performAccessibilityAction", "InteractionId="
-                                + interactionId + ":Result: " + result + ";connectionId="
-                                + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
-                                + ";accessibilityNodeId=" + accessibilityNodeId + ";action="
-                                + action + ";arguments=" + arguments);
+                    if (shouldTraceCallback()) {
+                        logTraceCallback(connection, "performAccessibilityAction",
+                                "InteractionId=" + interactionId + ";connectionId=" + connectionId
+                                + ";Result: " + result);
                     }
                     return result;
                 }
@@ -886,6 +945,8 @@
                 mFindAccessibilityNodeInfoResult = info;
                 mInteractionId = interactionId;
                 mCallingUid = Binder.getCallingUid();
+                mCallStackOfCallback = new ArrayList<StackTraceElement>(
+                        Arrays.asList(Thread.currentThread().getStackTrace()));
             }
             mInstanceLock.notifyAll();
         }
@@ -936,6 +997,8 @@
                 }
                 mInteractionId = interactionId;
                 mCallingUid = Binder.getCallingUid();
+                mCallStackOfCallback = new ArrayList<StackTraceElement>(
+                    Arrays.asList(Thread.currentThread().getStackTrace()));
             }
             mInstanceLock.notifyAll();
         }
@@ -975,13 +1038,15 @@
             finalizeAndCacheAccessibilityNodeInfos(
                     infos, connectionIdWaitingForPrefetchResultCopy, false,
                     packageNamesForNextPrefetchResultCopy);
-            if (mAccessibilityManager != null
-                    && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+            if (shouldTraceCallback()) {
                 logTrace(getConnection(connectionIdWaitingForPrefetchResultCopy),
                         "setPrefetchAccessibilityNodeInfoResult",
-                        "InteractionId:" + interactionId + ";Result: " + infos
-                                + ";connectionId=" + connectionIdWaitingForPrefetchResultCopy,
-                        Binder.getCallingUid());
+                        "InteractionId:" + interactionId + ";connectionId="
+                        + connectionIdWaitingForPrefetchResultCopy + ";Result: " + infos,
+                        Binder.getCallingUid(),
+                        Arrays.asList(Thread.currentThread().getStackTrace()),
+                        new HashSet<String>(Arrays.asList("getStackTrace")),
+                        FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK);
             }
         } else if (DEBUG) {
             Log.w(LOG_TAG, "Prefetching for interaction with id " + interactionId + " dropped "
@@ -1013,6 +1078,8 @@
                 mPerformAccessibilityActionResult = succeeded;
                 mInteractionId = interactionId;
                 mCallingUid = Binder.getCallingUid();
+                mCallStackOfCallback = new ArrayList<StackTraceElement>(
+                    Arrays.asList(Thread.currentThread().getStackTrace()));
             }
             mInstanceLock.notifyAll();
         }
@@ -1222,24 +1289,45 @@
         return true;
     }
 
+    private boolean shouldTraceClient() {
+        return (mAccessibilityManager != null)
+                && mAccessibilityManager.isA11yInteractionClientTraceEnabled();
+    }
+
+    private boolean shouldTraceCallback() {
+        return (mAccessibilityManager != null)
+                && mAccessibilityManager.isA11yInteractionConnectionCBTraceEnabled();
+    }
+
     private void logTrace(
             IAccessibilityServiceConnection connection, String method, String params,
-            int callingUid) {
+            int callingUid, List<StackTraceElement> callStack, HashSet<String> ignoreSet,
+            long logTypes) {
         try {
             Bundle b = new Bundle();
-            ArrayList<StackTraceElement> callStack = new ArrayList<StackTraceElement>(
-                    Arrays.asList(Thread.currentThread().getStackTrace()));
-            b.putSerializable(CALL_STACK, callStack);
+            b.putSerializable(CALL_STACK, new ArrayList<StackTraceElement>(callStack));
+            if (ignoreSet != null) {
+                b.putSerializable(IGNORE_CALL_STACK, ignoreSet);
+            }
             connection.logTrace(SystemClock.elapsedRealtimeNanos(),
-                    LOG_TAG + ".callback for " + method, params, Process.myPid(),
-                    Thread.currentThread().getId(), callingUid, b);
+                    LOG_TAG + "." + method,
+                    logTypes, params, Process.myPid(), Thread.currentThread().getId(),
+                    callingUid, b);
         } catch (RemoteException e) {
             Log.e(LOG_TAG, "Failed to log trace. " + e);
         }
     }
 
-    private void logTrace(
+    private void logTraceCallback(
             IAccessibilityServiceConnection connection, String method, String params) {
-        logTrace(connection, method, params, mCallingUid);
+        logTrace(connection, method + " callback", params, mCallingUid, mCallStackOfCallback,
+                new HashSet<String>(Arrays.asList("getStackTrace")),
+                FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK);
+    }
+
+    private void logTraceClient(
+            IAccessibilityServiceConnection connection, String method, String params) {
+        logTrace(connection, method, params, Binder.getCallingUid(),
+                Collections.emptyList(), null, FLAGS_ACCESSIBILITY_INTERACTION_CLIENT);
     }
 }
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index f9cdbd3..17fad7e 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -111,7 +111,13 @@
     public static final int STATE_FLAG_REQUEST_MULTI_FINGER_GESTURES = 0x00000010;
 
     /** @hide */
-    public static final int STATE_FLAG_ACCESSIBILITY_TRACING_ENABLED = 0x00000020;
+    public static final int STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_ENABLED = 0x00000100;
+    /** @hide */
+    public static final int STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_CB_ENABLED = 0x00000200;
+    /** @hide */
+    public static final int STATE_FLAG_TRACE_A11Y_INTERACTION_CLIENT_ENABLED = 0x00000400;
+    /** @hide */
+    public static final int STATE_FLAG_TRACE_A11Y_SERVICE_ENABLED = 0x00000800;
 
     /** @hide */
     public static final int DALTONIZER_DISABLED = -1;
@@ -235,8 +241,8 @@
     @UnsupportedAppUsage(trackingBug = 123768939L)
     boolean mIsHighTextContrastEnabled;
 
-    // Whether accessibility tracing is enabled or not
-    boolean mIsAccessibilityTracingEnabled = false;
+    // accessibility tracing state
+    int mAccessibilityTracingState = 0;
 
     AccessibilityPolicy mAccessibilityPolicy;
 
@@ -1029,13 +1035,50 @@
     }
 
     /**
-     * Gets accessibility tracing enabled state.
+     * Gets accessibility interaction connection tracing enabled state.
      *
      * @hide
      */
-    public boolean isAccessibilityTracingEnabled() {
+    public boolean isA11yInteractionConnectionTraceEnabled() {
         synchronized (mLock) {
-            return mIsAccessibilityTracingEnabled;
+            return ((mAccessibilityTracingState
+                    & STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_ENABLED) != 0);
+        }
+    }
+
+    /**
+     * Gets accessibility interaction connection callback tracing enabled state.
+     *
+     * @hide
+     */
+    public boolean isA11yInteractionConnectionCBTraceEnabled() {
+        synchronized (mLock) {
+            return ((mAccessibilityTracingState
+                    & STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_CB_ENABLED) != 0);
+        }
+    }
+
+    /**
+     * Gets accessibility interaction client tracing enabled state.
+     *
+     * @hide
+     */
+    public boolean isA11yInteractionClientTraceEnabled() {
+        synchronized (mLock) {
+            return ((mAccessibilityTracingState
+                    & STATE_FLAG_TRACE_A11Y_INTERACTION_CLIENT_ENABLED) != 0);
+        }
+    }
+
+    /**
+     * Gets accessibility service tracing enabled state.
+     *
+     * @hide
+     */
+    public boolean isA11yServiceTraceEnabled() {
+        synchronized (mLock) {
+            return ((mAccessibilityTracingState
+                    & STATE_FLAG_TRACE_A11Y_SERVICE_ENABLED) != 0);
         }
     }
 
@@ -1233,8 +1276,6 @@
                 (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;
         final boolean highTextContrastEnabled =
                 (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;
-        final boolean accessibilityTracingEnabled =
-                (stateFlags & STATE_FLAG_ACCESSIBILITY_TRACING_ENABLED) != 0;
 
         final boolean wasEnabled = isEnabled();
         final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
@@ -1257,7 +1298,7 @@
             notifyHighTextContrastStateChanged();
         }
 
-        updateAccessibilityTracingState(accessibilityTracingEnabled);
+        updateAccessibilityTracingState(stateFlags);
     }
 
     /**
@@ -1715,11 +1756,11 @@
     }
 
     /**
-     * Update mIsAccessibilityTracingEnabled.
+     * Update mAccessibilityTracingState.
      */
-    private void updateAccessibilityTracingState(boolean enabled) {
+    private void updateAccessibilityTracingState(int stateFlag) {
         synchronized (mLock) {
-            mIsAccessibilityTracingEnabled = enabled;
+            mAccessibilityTracingState = stateFlag;
         }
     }
 
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 085eb81..587a270 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -27,6 +27,7 @@
 import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ClipData;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Build;
@@ -4353,6 +4354,14 @@
             case R.id.accessibilityActionImeEnter:
                 return "ACTION_IME_ENTER";
             default:
+                // TODO(197520937): Use finalized constants in switch
+                if (action == R.id.accessibilityActionDragStart) {
+                    return "ACTION_DRAG";
+                } else if (action == R.id.accessibilityActionDragCancel) {
+                    return "ACTION_CANCEL_DRAG";
+                } else if (action == R.id.accessibilityActionDragDrop) {
+                    return "ACTION_DROP";
+                }
                 return "ACTION_UNKNOWN";
         }
     }
@@ -4995,6 +5004,46 @@
         @NonNull public static final AccessibilityAction ACTION_IME_ENTER =
                 new AccessibilityAction(R.id.accessibilityActionImeEnter);
 
+        /**
+         * Action to start a drag.
+         * <p>
+         * This action initiates a drag & drop within the system. The source's dragged content is
+         * prepared before the drag begins. In View, this action should prepare the arguments to
+         * {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)} and then
+         * call {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}. The
+         * equivalent should be performed for other UI toolkits.
+         * </p>
+         *
+         * @see AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_STARTED
+         */
+        @NonNull public static final AccessibilityAction ACTION_DRAG_START =
+                new AccessibilityAction(R.id.accessibilityActionDragStart);
+
+        /**
+         * Action to trigger a drop of the content being dragged.
+         * <p>
+         * This action is added to potential drop targets if the source started a drag with
+         * {@link #ACTION_DRAG_START}. In View, these targets are Views that accepted
+         * {@link android.view.DragEvent#ACTION_DRAG_STARTED} and have an
+         * {@link View.OnDragListener}.
+         * </p>
+         *
+         * @see AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_DROPPED
+         */
+        @NonNull public static final AccessibilityAction ACTION_DRAG_DROP =
+                new AccessibilityAction(R.id.accessibilityActionDragDrop);
+
+        /**
+         * Action to cancel a drag.
+         * <p>
+         * This action is added to the source that started a drag with {@link #ACTION_DRAG_START}.
+         * </p>
+         *
+         * @see AccessibilityEvent#CONTENT_CHANGE_TYPE_DRAG_CANCELLED
+         */
+        @NonNull public static final AccessibilityAction ACTION_DRAG_CANCEL =
+                new AccessibilityAction(R.id.accessibilityActionDragCancel);
+
         private final int mActionId;
         private final CharSequence mLabel;
 
diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
index edcb59a..76e2261 100644
--- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
+import android.app.ActivityTaskManager;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Parcel;
@@ -114,6 +115,7 @@
     private int mBooleanProperties;
     private int mId = UNDEFINED_WINDOW_ID;
     private int mParentId = UNDEFINED_WINDOW_ID;
+    private int mTaskId = ActivityTaskManager.INVALID_TASK_ID;
     private Region mRegionInScreen = new Region();
     private LongArray mChildIds;
     private CharSequence mTitle;
@@ -307,6 +309,28 @@
     }
 
     /**
+     * Gets the task ID.
+     *
+     * @return The task ID.
+     *
+     * @hide
+     */
+    public int getTaskId() {
+        return mTaskId;
+    }
+
+    /**
+     * Sets the task ID.
+     *
+     * @param taskId The task ID.
+     *
+     * @hide
+     */
+    public void setTaskId(int taskId) {
+        mTaskId = taskId;
+    }
+
+    /**
      * Sets the unique id of the IAccessibilityServiceConnection over which
      * this instance can send requests to the system.
      *
@@ -578,6 +602,7 @@
         parcel.writeInt(mBooleanProperties);
         parcel.writeInt(mId);
         parcel.writeInt(mParentId);
+        parcel.writeInt(mTaskId);
         mRegionInScreen.writeToParcel(parcel, flags);
         parcel.writeCharSequence(mTitle);
         parcel.writeLong(mAnchorId);
@@ -608,6 +633,7 @@
         mBooleanProperties = other.mBooleanProperties;
         mId = other.mId;
         mParentId = other.mParentId;
+        mTaskId = other.mTaskId;
         mRegionInScreen.set(other.mRegionInScreen);
         mTitle = other.mTitle;
         mAnchorId = other.mAnchorId;
@@ -631,6 +657,7 @@
         mBooleanProperties = parcel.readInt();
         mId = parcel.readInt();
         mParentId = parcel.readInt();
+        mTaskId = parcel.readInt();
         mRegionInScreen = Region.CREATOR.createFromParcel(parcel);
         mTitle = parcel.readCharSequence();
         mAnchorId = parcel.readLong();
@@ -676,6 +703,7 @@
         builder.append("title=").append(mTitle);
         builder.append(", displayId=").append(mDisplayId);
         builder.append(", id=").append(mId);
+        builder.append(", taskId=").append(mTaskId);
         builder.append(", type=").append(typeToString(mType));
         builder.append(", layer=").append(mLayer);
         builder.append(", region=").append(mRegionInScreen);
@@ -719,6 +747,7 @@
         mBooleanProperties = 0;
         mId = UNDEFINED_WINDOW_ID;
         mParentId = UNDEFINED_WINDOW_ID;
+        mTaskId = ActivityTaskManager.INVALID_TASK_ID;
         mRegionInScreen.setEmpty();
         mChildIds = null;
         mConnectionId = UNDEFINED_WINDOW_ID;
diff --git a/core/java/android/view/contentcapture/ContentCaptureContext.java b/core/java/android/view/contentcapture/ContentCaptureContext.java
index 9998fbc..0da54e5 100644
--- a/core/java/android/view/contentcapture/ContentCaptureContext.java
+++ b/core/java/android/view/contentcapture/ContentCaptureContext.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.content.LocusId;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.Display;
@@ -105,6 +106,7 @@
     private final int mFlags;
     private final int mDisplayId;
     private final ActivityId mActivityId;
+    private final IBinder mWindowToken;
 
     // Fields below are set by the service upon "delivery" and are not marshalled in the parcel
     private int mParentSessionId = NO_SESSION_ID;
@@ -112,7 +114,7 @@
     /** @hide */
     public ContentCaptureContext(@Nullable ContentCaptureContext clientContext,
             @NonNull ActivityId activityId, @NonNull ComponentName componentName, int displayId,
-            int flags) {
+            IBinder windowToken, int flags) {
         if (clientContext != null) {
             mHasClientContext = true;
             mExtras = clientContext.mExtras;
@@ -126,6 +128,7 @@
         mFlags = flags;
         mDisplayId = displayId;
         mActivityId = activityId;
+        mWindowToken = windowToken;
     }
 
     private ContentCaptureContext(@NonNull Builder builder) {
@@ -137,6 +140,7 @@
         mFlags = 0;
         mDisplayId = Display.INVALID_DISPLAY;
         mActivityId = null;
+        mWindowToken = null;
     }
 
     /** @hide */
@@ -148,6 +152,7 @@
         mFlags = original.mFlags | extraFlags;
         mDisplayId = original.mDisplayId;
         mActivityId = original.mActivityId;
+        mWindowToken = original.mWindowToken;
     }
 
     /**
@@ -230,6 +235,20 @@
     }
 
     /**
+     * Gets the window token of the activity associated with this context.
+     *
+     * <p>The token can be used to attach relevant overlay views to the activity's window. This can
+     * be done through {@link android.view.WindowManager.LayoutParams#token}.
+     *
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public IBinder getWindowToken() {
+        return mWindowToken;
+    }
+
+    /**
      * Gets the flags associated with this context.
      *
      * @return any combination of {@link #FLAG_DISABLED_BY_FLAG_SECURE},
@@ -328,6 +347,7 @@
         }
         pw.print(", activityId="); pw.print(mActivityId);
         pw.print(", displayId="); pw.print(mDisplayId);
+        pw.print(", windowToken="); pw.print(mWindowToken);
         if (mParentSessionId != NO_SESSION_ID) {
             pw.print(", parentId="); pw.print(mParentSessionId);
         }
@@ -352,6 +372,7 @@
             builder.append("act=").append(ComponentName.flattenToShortString(mComponentName))
                 .append(", activityId=").append(mActivityId)
                 .append(", displayId=").append(mDisplayId)
+                .append(", windowToken=").append(mWindowToken)
                 .append(", flags=").append(mFlags);
         } else {
             builder.append("id=").append(mId);
@@ -381,6 +402,7 @@
         parcel.writeParcelable(mComponentName, flags);
         if (fromServer()) {
             parcel.writeInt(mDisplayId);
+            parcel.writeStrongBinder(mWindowToken);
             parcel.writeInt(mFlags);
             mActivityId.writeToParcel(parcel, flags);
         }
@@ -411,11 +433,12 @@
                 return clientContext;
             } else {
                 final int displayId = parcel.readInt();
+                final IBinder windowToken = parcel.readStrongBinder();
                 final int flags = parcel.readInt();
                 final ActivityId activityId = new ActivityId(parcel);
 
                 return new ContentCaptureContext(clientContext, activityId, componentName,
-                        displayId, flags);
+                        displayId, windowToken, flags);
             }
         }
 
diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java
index ce6d034..4b2d3a9 100644
--- a/core/java/android/view/contentcapture/ContentCaptureEvent.java
+++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.Selection;
@@ -122,6 +123,12 @@
      */
     public static final int TYPE_VIEW_INSETS_CHANGED = 9;
 
+    /**
+     * Called before {@link #TYPE_VIEW_TREE_APPEARING}, or after the size of the window containing
+     * the views changed.
+     */
+    public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10;
+
     /** @hide */
     @IntDef(prefix = { "TYPE_" }, value = {
             TYPE_VIEW_APPEARED,
@@ -132,7 +139,8 @@
             TYPE_CONTEXT_UPDATED,
             TYPE_SESSION_PAUSED,
             TYPE_SESSION_RESUMED,
-            TYPE_VIEW_INSETS_CHANGED
+            TYPE_VIEW_INSETS_CHANGED,
+            TYPE_WINDOW_BOUNDS_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EventType{}
@@ -150,6 +158,7 @@
     private int mParentSessionId = NO_SESSION_ID;
     private @Nullable ContentCaptureContext mClientContext;
     private @Nullable Insets mInsets;
+    private @Nullable Rect mBounds;
 
     private int mComposingStart = MAX_INVALID_VALUE;
     private int mComposingEnd = MAX_INVALID_VALUE;
@@ -346,6 +355,13 @@
         return this;
     }
 
+    /** @hide */
+    @NonNull
+    public ContentCaptureEvent setBounds(@NonNull Rect bounds) {
+        mBounds = bounds;
+        return this;
+    }
+
     /**
      * Gets the type of the event.
      *
@@ -419,6 +435,16 @@
     }
 
     /**
+     * Gets the {@link Rect} bounds of the window associated with the event. Valid bounds will only
+     * be returned if the type of the event is {@link #TYPE_WINDOW_BOUNDS_CHANGED}, otherwise they
+     * will be null.
+     */
+    @Nullable
+    public Rect getBounds() {
+        return mBounds;
+    }
+
+    /**
      * Merges event of the same type, either {@link #TYPE_VIEW_TEXT_CHANGED}
      * or {@link #TYPE_VIEW_DISAPPEARED}.
      *
@@ -489,6 +515,9 @@
         if (mInsets != null) {
             pw.print(", insets="); pw.println(mInsets);
         }
+        if (mBounds != null) {
+            pw.print(", bounds="); pw.println(mBounds);
+        }
         if (mComposingStart > MAX_INVALID_VALUE) {
             pw.print(", composing("); pw.print(mComposingStart);
             pw.print(", "); pw.print(mComposingEnd); pw.print(")");
@@ -533,6 +562,9 @@
         if (mInsets != null) {
             string.append(", insets=").append(mInsets);
         }
+        if (mBounds != null) {
+            string.append(", bounds=").append(mBounds);
+        }
         if (mComposingStart > MAX_INVALID_VALUE) {
             string.append(", composing=[")
                     .append(mComposingStart).append(",").append(mComposingEnd).append("]");
@@ -568,6 +600,9 @@
         if (mType == TYPE_VIEW_INSETS_CHANGED) {
             parcel.writeParcelable(mInsets, flags);
         }
+        if (mType == TYPE_WINDOW_BOUNDS_CHANGED) {
+            parcel.writeParcelable(mBounds, flags);
+        }
         if (mType == TYPE_VIEW_TEXT_CHANGED) {
             parcel.writeInt(mComposingStart);
             parcel.writeInt(mComposingEnd);
@@ -608,6 +643,9 @@
             if (type == TYPE_VIEW_INSETS_CHANGED) {
                 event.setInsets(parcel.readParcelable(null));
             }
+            if (type == TYPE_WINDOW_BOUNDS_CHANGED) {
+                event.setBounds(parcel.readParcelable(null));
+            }
             if (type == TYPE_VIEW_TEXT_CHANGED) {
                 event.setComposingIndex(parcel.readInt(), parcel.readInt());
                 event.restoreComposingSpan();
@@ -649,6 +687,8 @@
                 return "CONTEXT_UPDATED";
             case TYPE_VIEW_INSETS_CHANGED:
                 return "VIEW_INSETS_CHANGED";
+            case TYPE_WINDOW_BOUNDS_CHANGED:
+                return "TYPE_WINDOW_BOUNDS_CHANGED";
             default:
                 return "UKNOWN_TYPE: " + type;
         }
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 4cf5532..98ef4e7 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -26,6 +26,7 @@
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_WINDOW_BOUNDS_CHANGED;
 import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
@@ -38,6 +39,7 @@
 import android.content.Context;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -776,6 +778,14 @@
                 .setClientContext(context), FORCE_FLUSH));
     }
 
+    /** public because is also used by ViewRootImpl */
+    public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
+        mHandler.post(() -> sendEvent(
+                new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
+                .setBounds(bounds)
+        ));
+    }
+
     @Override
     void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
         super.dump(prefix, pw);
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index d2db0df..5b2068f 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -96,8 +96,6 @@
      *
      * @param token special token for the system to identify
      *              {@link InputMethodService}
-     * @param displayId The id of the display that current IME shown.
-     *                  Used for {{@link #updateInputMethodDisplay(int)}}
      * @param privilegedOperations IPC endpoint to do some privileged
      *                             operations that are allowed only to the
      *                             current IME.
@@ -105,9 +103,8 @@
      * @hide
      */
     @MainThread
-    default void initializeInternal(IBinder token, int displayId,
+    default void initializeInternal(IBinder token,
             IInputMethodPrivilegedOperations privilegedOperations, int configChanges) {
-        updateInputMethodDisplay(displayId);
         attachToken(token);
     }
 
@@ -143,16 +140,6 @@
     public void attachToken(IBinder token);
 
     /**
-     * Update context display according to given displayId.
-     *
-     * @param displayId The id of the display that need to update for context.
-     * @hide
-     */
-    @MainThread
-    default void updateInputMethodDisplay(int displayId) {
-    }
-
-    /**
      * Bind a new application environment in to the input method, so that it
      * can later start and stop input processing.
      * Typically this method is called when this input method is enabled in an
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 42d77cd..e6f103e 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2154,6 +2154,7 @@
      * @hide
      */
     public boolean requestImeShow(IBinder windowToken) {
+        checkFocus();
         synchronized (mH) {
             final View servedView = getServedViewLocked();
             if (servedView == null || servedView.getWindowToken() != windowToken) {
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index d078c2c..aa73ed7 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -110,11 +110,10 @@
     public void updateUiTranslationState(@UiTranslationState int state, TranslationSpec sourceSpec,
             TranslationSpec targetSpec, List<AutofillId> views,
             UiTranslationSpec uiTranslationSpec) {
-        if (!mActivity.isResumed() && (state == STATE_UI_TRANSLATION_STARTED
-                || state == STATE_UI_TRANSLATION_RESUMED)) {
+        if (mActivity.isDestroyed()) {
+            Log.i(TAG, "Cannot update " + stateToString(state) + " for destroyed " + mActivity);
             return;
         }
-
         Log.i(TAG, "updateUiTranslationState state: " + stateToString(state)
                 + (DEBUG ? (", views: " + views + ", spec: " + uiTranslationSpec) : ""));
         synchronized (mLock) {
@@ -342,10 +341,8 @@
      */
     private void onVirtualViewTranslationCompleted(
             SparseArray<LongSparseArray<ViewTranslationResponse>> translatedResult) {
-        if (!mActivity.isResumed()) {
-            if (DEBUG) {
-                Log.v(TAG, "onTranslationCompleted: Activity is not resumed.");
-            }
+        if (mActivity.isDestroyed()) {
+            Log.v(TAG, "onTranslationCompleted:" + mActivity + "is destroyed.");
             return;
         }
         synchronized (mLock) {
@@ -393,10 +390,8 @@
      * The method is used to handle the translation result for non-vertual views.
      */
     private void onTranslationCompleted(SparseArray<ViewTranslationResponse> translatedResult) {
-        if (!mActivity.isResumed()) {
-            if (DEBUG) {
-                Log.v(TAG, "onTranslationCompleted: Activity is not resumed.");
-            }
+        if (mActivity.isDestroyed()) {
+            Log.v(TAG, "onTranslationCompleted:" + mActivity + "is destroyed.");
             return;
         }
         final int resultCount = translatedResult.size();
diff --git a/core/java/android/window/ConfigurationHelper.java b/core/java/android/window/ConfigurationHelper.java
new file mode 100644
index 0000000..9a07975
--- /dev/null
+++ b/core/java/android/window/ConfigurationHelper.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ResourcesManager;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.Display;
+import android.view.WindowManager;
+
+/**
+ * A helper class to maintain {@link android.content.res.Configuration} related methods used both
+ * in {@link android.app.Activity} and {@link WindowContext}.
+ *
+ * @hide
+ */
+public class ConfigurationHelper {
+    private ConfigurationHelper() {}
+
+    /** Ask text layout engine to free its caches if there is a locale change. */
+    public static void freeTextLayoutCachesIfNeeded(int configDiff) {
+        if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) {
+            Canvas.freeTextLayoutCaches();
+        }
+    }
+
+    /**
+     * A helper method to filter out {@link ActivityInfo#CONFIG_SCREEN_SIZE} if the
+     * {@link Configuration#diffPublicOnly(Configuration) diff} of two {@link Configuration}
+     * doesn't cross the boundary.
+     *
+     * @see SizeConfigurationBuckets#filterDiff(int, Configuration, Configuration,
+     * SizeConfigurationBuckets)
+     */
+    public static int diffPublicWithSizeBuckets(@Nullable Configuration currentConfig,
+            @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) {
+        // If current configuration is null, it is definitely different from updated Configuration.
+        if (currentConfig == null) {
+            return 0xffffffff;
+        }
+        int publicDiff = currentConfig.diffPublicOnly(newConfig);
+        return SizeConfigurationBuckets.filterDiff(publicDiff, currentConfig, newConfig, buckets);
+    }
+
+    /**
+     * Returns {@code true} if the {@link android.content.res.Resources} associated with
+     * a {@code token} needs to be updated.
+     *
+     * @param token A {@link Context#getActivityToken() activity token} or
+     * {@link Context#getWindowContextToken() window context token}
+     * @param config The original {@link Configuration}
+     * @param newConfig The updated Configuration
+     * @param displayChanged a flag to indicate there's a display change
+     * @param configChanged a flag to indicate there's a Configuration change.
+     *
+     * @see ResourcesManager#updateResourcesForActivity(IBinder, Configuration, int)
+     */
+    public static boolean shouldUpdateResources(IBinder token, @Nullable Configuration config,
+            @NonNull Configuration newConfig, @NonNull Configuration overrideConfig,
+            boolean displayChanged, @Nullable Boolean configChanged) {
+        // The configuration has not yet been initialized. We should update it.
+        if (config == null) {
+            return true;
+        }
+        // If the token associated context is moved to another display, we should update the
+        // ResourcesKey.
+        if (displayChanged) {
+            return true;
+        }
+        // If the new config is the same as the config this Activity is already running with and
+        // the override config also didn't change, then don't update the Resources
+        if (!ResourcesManager.getInstance().isSameResourcesOverrideConfig(token, overrideConfig)) {
+            return true;
+        }
+        // If there's a update on WindowConfiguration#mBounds or maxBounds, we should update the
+        // Resources to make WindowMetrics API report the updated result.
+        if (shouldUpdateWindowMetricsBounds(config, newConfig)) {
+            return true;
+        }
+        return configChanged == null ? config.diff(newConfig) != 0 : configChanged;
+    }
+
+    /**
+     * Returns {@code true} if {@code displayId} is different from {@code newDisplayId}.
+     * Note that {@link Display#INVALID_DISPLAY} means no difference.
+     */
+    public static boolean isDifferentDisplay(int displayId, int newDisplayId) {
+        return newDisplayId != INVALID_DISPLAY && displayId != newDisplayId;
+    }
+
+    // TODO(b/173090263): Remove this method after the improvement of AssetManager and ResourcesImpl
+    // constructions.
+    /**
+     * Returns {@code true} if the metrics reported by {@link android.view.WindowMetrics} APIs
+     * should be updated.
+     *
+     * @see WindowManager#getCurrentWindowMetrics()
+     * @see WindowManager#getMaximumWindowMetrics()
+     */
+    private static boolean shouldUpdateWindowMetricsBounds(@NonNull Configuration currentConfig,
+            @NonNull Configuration newConfig) {
+        final Rect currentBounds = currentConfig.windowConfiguration.getBounds();
+        final Rect newBounds = newConfig.windowConfiguration.getBounds();
+
+        final Rect currentMaxBounds = currentConfig.windowConfiguration.getMaxBounds();
+        final Rect newMaxBounds = newConfig.windowConfiguration.getMaxBounds();
+
+        return !currentBounds.equals(newBounds) || !currentMaxBounds.equals(newMaxBounds);
+    }
+}
diff --git a/core/java/android/window/DisplayAreaInfo.java b/core/java/android/window/DisplayAreaInfo.java
index 358467f..1a7aab6 100644
--- a/core/java/android/window/DisplayAreaInfo.java
+++ b/core/java/android/window/DisplayAreaInfo.java
@@ -16,6 +16,8 @@
 
 package android.window;
 
+import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
+
 import android.annotation.NonNull;
 import android.annotation.TestApi;
 import android.content.res.Configuration;
@@ -43,8 +45,17 @@
      */
     public final int displayId;
 
+    /**
+     * The feature id of this display area.
+     */
     public final int featureId;
 
+    /**
+     * The feature id of the root display area this display area is associated with.
+     * @hide
+     */
+    public int rootDisplayAreaId = FEATURE_UNDEFINED;
+
     public DisplayAreaInfo(@NonNull WindowContainerToken token, int displayId, int featureId) {
         this.token = token;
         this.displayId = displayId;
@@ -56,6 +67,7 @@
         configuration.readFromParcel(in);
         displayId = in.readInt();
         featureId = in.readInt();
+        rootDisplayAreaId = in.readInt();
     }
 
     @Override
@@ -64,6 +76,7 @@
         configuration.writeToParcel(dest, flags);
         dest.writeInt(displayId);
         dest.writeInt(featureId);
+        dest.writeInt(rootDisplayAreaId);
     }
 
     @NonNull
diff --git a/core/java/android/window/DisplayAreaOrganizer.java b/core/java/android/window/DisplayAreaOrganizer.java
index 8784399..6758a3b 100644
--- a/core/java/android/window/DisplayAreaOrganizer.java
+++ b/core/java/android/window/DisplayAreaOrganizer.java
@@ -34,6 +34,15 @@
 public class DisplayAreaOrganizer extends WindowOrganizer {
 
     /**
+     * Key to specify the {@link com.android.server.wm.RootDisplayArea} to attach a window to.
+     * It will be used by the function passed in from
+     * {@link com.android.server.wm.DisplayAreaPolicyBuilder#setSelectRootForWindowFunc(BiFunction)}
+     * to find the Root DA to attach the window.
+     * @hide
+     */
+    public static final String KEY_ROOT_DISPLAY_AREA_ID = "root_display_area_id";
+
+    /**
      * The value in display area indicating that no value has been set.
      */
     public static final int FEATURE_UNDEFINED = -1;
@@ -256,6 +265,7 @@
         }
     };
 
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
     private IDisplayAreaOrganizerController getController() {
         try {
             return getWindowOrganizerController().getDisplayAreaOrganizerController();
@@ -263,5 +273,4 @@
             return null;
         }
     }
-
 }
diff --git a/core/java/android/window/IRemoteTransitionFinishedCallback.aidl b/core/java/android/window/IRemoteTransitionFinishedCallback.aidl
index 02aa1a9..7864c24 100644
--- a/core/java/android/window/IRemoteTransitionFinishedCallback.aidl
+++ b/core/java/android/window/IRemoteTransitionFinishedCallback.aidl
@@ -16,14 +16,18 @@
 
 package android.window;
 
+import android.view.SurfaceControl;
 import android.window.WindowContainerTransaction;
 
 /**
  * Interface to be invoked by the controlling process when a remote transition has finished.
  *
  * @see IRemoteTransition
+ * @param wct An optional WindowContainerTransaction to apply before the transition finished.
+ * @param sct An optional Surface Transaction that is added to the end of the finish/cleanup
+ *            transaction. This is applied by shell.Transitions (before submitting the wct).
  * {@hide}
  */
 interface IRemoteTransitionFinishedCallback {
-    void onTransitionFinished(in WindowContainerTransaction wct);
+    void onTransitionFinished(in WindowContainerTransaction wct, in SurfaceControl.Transaction sct);
 }
diff --git a/core/java/android/window/ITaskFragmentOrganizer.aidl b/core/java/android/window/ITaskFragmentOrganizer.aidl
new file mode 100644
index 0000000..5eb432e
--- /dev/null
+++ b/core/java/android/window/ITaskFragmentOrganizer.aidl
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.window.TaskFragmentAppearedInfo;
+import android.window.TaskFragmentInfo;
+
+/** @hide */
+oneway interface ITaskFragmentOrganizer {
+    void onTaskFragmentAppeared(in TaskFragmentAppearedInfo taskFragmentAppearedInfo);
+    void onTaskFragmentInfoChanged(in TaskFragmentInfo taskFragmentInfo);
+    void onTaskFragmentVanished(in TaskFragmentInfo taskFragmentInfo);
+
+    /**
+     * Called when the parent leaf Task of organized TaskFragments is changed.
+     * When the leaf Task is changed, the organizer may want to update the TaskFragments in one
+     * transaction.
+     *
+     * For case like screen size change, it will trigger onTaskFragmentParentInfoChanged with new
+     * Task bounds, but may not trigger onTaskFragmentInfoChanged because there can be an override
+     * bounds.
+     */
+    void onTaskFragmentParentInfoChanged(in IBinder fragmentToken, in Configuration parentConfig);
+
+    /**
+     * Called when the {@link WindowContainerTransaction} created with
+     * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side.
+     *
+     * @param errorCallbackToken    Token set through {@link
+     *                              WindowContainerTransaction#setErrorCallbackToken(IBinder)}
+     * @param exceptionBundle   Bundle containing the exception. Should be created with
+     *                          {@link TaskFragmentOrganizer#putExceptionInBundle}.
+     */
+    void onTaskFragmentError(in IBinder errorCallbackToken, in Bundle exceptionBundle);
+}
diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl
new file mode 100644
index 0000000..1ad0452
--- /dev/null
+++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.view.RemoteAnimationDefinition;
+import android.window.ITaskFragmentOrganizer;
+
+/** @hide */
+interface ITaskFragmentOrganizerController {
+
+    /**
+     * Registers a TaskFragmentOrganizer to manage TaskFragments.
+     */
+    void registerOrganizer(in ITaskFragmentOrganizer organizer);
+
+    /**
+     * Unregisters a previously registered TaskFragmentOrganizer.
+     */
+    void unregisterOrganizer(in ITaskFragmentOrganizer organizer);
+
+    /**
+     * Registers remote animations per transition type for the organizer. It will override the
+     * animations if the transition only contains windows that belong to the organized
+     * TaskFragments.
+     */
+    void registerRemoteAnimations(in ITaskFragmentOrganizer organizer,
+        in RemoteAnimationDefinition definition);
+
+    /**
+     * Unregisters remote animations per transition type for the organizer.
+     */
+    void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer);
+}
diff --git a/core/java/android/window/ITaskOrganizer.aidl b/core/java/android/window/ITaskOrganizer.aidl
index 69bc1b5..fd86769 100644
--- a/core/java/android/window/ITaskOrganizer.aidl
+++ b/core/java/android/window/ITaskOrganizer.aidl
@@ -20,6 +20,7 @@
 import android.app.ActivityManager;
 import android.graphics.Rect;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.WindowContainerToken;
 
 /**
@@ -39,12 +40,9 @@
 
     /**
      * Called when the Task want to remove the starting window.
-     * @param leash A persistent leash for the top window in this task.
-     * @param frame Window frame of the top window.
-     * @param playRevealAnimation Play vanish animation.
+     * @param removalInfo The information used to remove the starting window.
      */
-    void removeStartingWindow(int taskId, in SurfaceControl leash, in Rect frame,
-            in boolean playRevealAnimation);
+    void removeStartingWindow(in StartingWindowRemovalInfo removalInfo);
 
     /**
      * Called when the Task want to copy the splash screen.
diff --git a/core/java/android/window/ITransitionMetricsReporter.aidl b/core/java/android/window/ITransitionMetricsReporter.aidl
new file mode 100644
index 0000000..00f71dc
--- /dev/null
+++ b/core/java/android/window/ITransitionMetricsReporter.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.os.IBinder;
+
+/**
+ * Implemented by WM Core to know the metrics of transition that runs on a different process.
+ * @hide
+ */
+oneway interface ITransitionMetricsReporter {
+
+    /**
+     * Called when the transition animation starts.
+     *
+     * @param startTime The time when the animation started.
+     */
+    void reportAnimationStart(IBinder transitionToken, long startTime);
+}
diff --git a/core/java/android/window/IWindowOrganizerController.aidl b/core/java/android/window/IWindowOrganizerController.aidl
index 1223d72..3c7cd02 100644
--- a/core/java/android/window/IWindowOrganizerController.aidl
+++ b/core/java/android/window/IWindowOrganizerController.aidl
@@ -19,8 +19,11 @@
 import android.view.SurfaceControl;
 
 import android.os.IBinder;
+import android.view.RemoteAnimationAdapter;
 import android.window.IDisplayAreaOrganizerController;
+import android.window.ITaskFragmentOrganizerController;
 import android.window.ITaskOrganizerController;
+import android.window.ITransitionMetricsReporter;
 import android.window.ITransitionPlayer;
 import android.window.IWindowContainerTransactionCallback;
 import android.window.WindowContainerToken;
@@ -60,6 +63,17 @@
             in @nullable WindowContainerTransaction t);
 
     /**
+     * Starts a legacy transition.
+     * @param type The transition type.
+     * @param adapter The animation to use.
+     * @param syncCallback A sync callback for the contents of `t`
+     * @param t Operations that are part of the transition.
+     * @return sync-id or -1 if this no-op'd because a transition is already running.
+     */
+    int startLegacyTransition(int type, in RemoteAnimationAdapter adapter,
+            in IWindowContainerTransactionCallback syncCallback, in WindowContainerTransaction t);
+
+    /**
      * Finishes a transition. This must be called for all created transitions.
      * @param transitionToken Which transition to finish
      * @param t Changes to make before finishing but in the same SF Transaction. Can be null.
@@ -77,9 +91,15 @@
     /** @return An interface enabling the management of display area organizers. */
     IDisplayAreaOrganizerController getDisplayAreaOrganizerController();
 
+    /** @return An interface enabling the management of task fragment organizers. */
+    ITaskFragmentOrganizerController getTaskFragmentOrganizerController();
+
     /**
      * Registers a transition player with Core. There is only one of these at a time and calling
      * this will replace the existing one if set.
      */
     void registerTransitionPlayer(in ITransitionPlayer player);
+
+    /** @return An interface enabling the transition players to report its metrics. */
+    ITransitionMetricsReporter getTransitionMetricsReporter();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/core/java/android/window/RemoteTransition.aidl
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to core/java/android/window/RemoteTransition.aidl
index b391bd9..f3c3f54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/core/java/android/window/RemoteTransition.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package android.window;
 
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
-}
+parcelable RemoteTransition;
diff --git a/core/java/android/window/RemoteTransition.java b/core/java/android/window/RemoteTransition.java
new file mode 100644
index 0000000..b243b65
--- /dev/null
+++ b/core/java/android/window/RemoteTransition.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Represents a remote transition animation and information required to run it (eg. the app thread
+ * that needs to be boosted).
+ * @hide
+ */
+@DataClass(genToString = true, genSetters = true, genAidl = true)
+public class RemoteTransition implements Parcelable {
+
+    /** The actual remote-transition interface used to run the transition animation. */
+    private @NonNull IRemoteTransition mRemoteTransition;
+
+    /** Get the IBinder associated with the underlying IRemoteTransition. */
+    public @Nullable IBinder asBinder() {
+        return mRemoteTransition.asBinder();
+    }
+
+
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/window/RemoteTransition.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    /**
+     * Creates a new RemoteTransition.
+     *
+     * @param remoteTransition
+     *   The actual remote-transition interface used to run the transition animation.
+     */
+    @DataClass.Generated.Member
+    public RemoteTransition(
+            @NonNull IRemoteTransition remoteTransition) {
+        this.mRemoteTransition = remoteTransition;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mRemoteTransition);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    /**
+     * The actual remote-transition interface used to run the transition animation.
+     */
+    @DataClass.Generated.Member
+    public @NonNull IRemoteTransition getRemoteTransition() {
+        return mRemoteTransition;
+    }
+
+    /**
+     * The actual remote-transition interface used to run the transition animation.
+     */
+    @DataClass.Generated.Member
+    public @NonNull RemoteTransition setRemoteTransition(@NonNull IRemoteTransition value) {
+        mRemoteTransition = value;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mRemoteTransition);
+        return this;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "RemoteTransition { " +
+                "remoteTransition = " + mRemoteTransition +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        dest.writeStrongInterface(mRemoteTransition);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    protected RemoteTransition(@NonNull android.os.Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        IRemoteTransition remoteTransition = IRemoteTransition.Stub.asInterface(in.readStrongBinder());
+
+        this.mRemoteTransition = remoteTransition;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mRemoteTransition);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<RemoteTransition> CREATOR
+            = new Parcelable.Creator<RemoteTransition>() {
+        @Override
+        public RemoteTransition[] newArray(int size) {
+            return new RemoteTransition[size];
+        }
+
+        @Override
+        public RemoteTransition createFromParcel(@NonNull android.os.Parcel in) {
+            return new RemoteTransition(in);
+        }
+    };
+
+    @DataClass.Generated(
+            time = 1630613039043L,
+            codegenVersion = "1.0.23",
+            sourceFile = "frameworks/base/core/java/android/window/RemoteTransition.java",
+            inputSignatures = "private @android.annotation.NonNull android.window.IRemoteTransition mRemoteTransition\npublic @android.annotation.Nullable android.os.IBinder asBinder()\nclass RemoteTransition extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/core/java/android/window/SizeConfigurationBuckets.java b/core/java/android/window/SizeConfigurationBuckets.java
index 7422f24..f474f0a 100644
--- a/core/java/android/window/SizeConfigurationBuckets.java
+++ b/core/java/android/window/SizeConfigurationBuckets.java
@@ -16,6 +16,7 @@
 
 package android.window;
 
+import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
 
@@ -25,6 +26,7 @@
 import android.os.Parcelable;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DataClass;
 
 import java.util.Arrays;
@@ -54,10 +56,24 @@
     @Nullable
     private final int[] mSmallest;
 
+    /** Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets */
+    @Nullable
+    private final int[] mScreenLayoutSize;
+
+    /**
+     * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a
+     * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and
+     * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change.
+     */
+    private final boolean mScreenLayoutLongSet;
+
     public SizeConfigurationBuckets(Configuration[] sizeConfigurations) {
         SparseIntArray horizontal = new SparseIntArray();
         SparseIntArray vertical = new SparseIntArray();
         SparseIntArray smallest = new SparseIntArray();
+        SparseIntArray screenLayoutSize = new SparseIntArray();
+        int curScreenLayoutSize;
+        boolean screenLayoutLongSet = false;
         for (int i = sizeConfigurations.length - 1; i >= 0; i--) {
             Configuration config = sizeConfigurations[i];
             if (config.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
@@ -69,23 +85,42 @@
             if (config.smallestScreenWidthDp != Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
                 smallest.put(config.smallestScreenWidthDp, 0);
             }
+            if ((curScreenLayoutSize = config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
+                    != Configuration.SCREENLAYOUT_SIZE_UNDEFINED) {
+                screenLayoutSize.put(curScreenLayoutSize, 0);
+            }
+            if (!screenLayoutLongSet && (config.screenLayout & Configuration.SCREENLAYOUT_LONG_MASK)
+                    != Configuration.SCREENLAYOUT_LONG_UNDEFINED) {
+                screenLayoutLongSet = true;
+            }
         }
         mHorizontal = horizontal.copyKeys();
         mVertical = vertical.copyKeys();
         mSmallest = smallest.copyKeys();
+        mScreenLayoutSize = screenLayoutSize.copyKeys();
+        mScreenLayoutLongSet = screenLayoutLongSet;
     }
 
     /**
      * Get the changes between two configurations but don't count changes in sizes if they don't
-     * cross boundaries that are  important to the app.
+     * cross boundaries that are important to the app.
      *
      * This is a static helper to deal with null `buckets`. When no buckets have been specified,
      * this actually filters out all 3 size-configs. This is legacy behavior.
      */
-    public static int filterDiff(int diff, Configuration oldConfig, Configuration newConfig,
-            @Nullable SizeConfigurationBuckets buckets) {
+    public static int filterDiff(int diff, @NonNull Configuration oldConfig,
+            @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) {
+        final boolean nonSizeLayoutFieldsUnchanged =
+                areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout);
         if (buckets == null) {
-            return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE);
+            // Only unflip CONFIG_SCREEN_LAYOUT if non-size-related  attributes of screen layout do
+            // not change.
+            if (nonSizeLayoutFieldsUnchanged) {
+                return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE
+                        | CONFIG_SCREEN_LAYOUT);
+            } else {
+                return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE);
+            }
         }
         if ((diff & CONFIG_SCREEN_SIZE) != 0) {
             final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp,
@@ -103,6 +138,13 @@
                 diff &= ~CONFIG_SMALLEST_SCREEN_SIZE;
             }
         }
+        if ((diff & CONFIG_SCREEN_LAYOUT) != 0 && nonSizeLayoutFieldsUnchanged) {
+            if (!buckets.crossesScreenLayoutSizeThreshold(oldConfig, newConfig)
+                    && !buckets.crossesScreenLayoutLongThreshold(oldConfig.screenLayout,
+                    newConfig.screenLayout)) {
+                diff &= ~CONFIG_SCREEN_LAYOUT;
+            }
+        }
         return diff;
     }
 
@@ -119,6 +161,61 @@
     }
 
     /**
+     * Returns whether a screen layout size threshold has been crossed.
+     */
+    @VisibleForTesting
+    public boolean crossesScreenLayoutSizeThreshold(@NonNull Configuration firstConfig,
+            @NonNull Configuration secondConfig) {
+        // If both the old and new screen layout are equal (both can be undefined), then no
+        // threshold is crossed.
+        if ((firstConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
+                == (secondConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)) {
+            return false;
+        }
+        // Any time the new layout size is smaller than the old layout size, the activity has
+        // crossed a size threshold because layout size represents the smallest possible size the
+        // activity can occupy.
+        if (!secondConfig.isLayoutSizeAtLeast(firstConfig.screenLayout
+                & Configuration.SCREENLAYOUT_SIZE_MASK)) {
+            return true;
+        }
+        // If the new layout size is at least as large as the old layout size, then check if the new
+        // layout size has crossed a threshold.
+        if (mScreenLayoutSize != null) {
+            for (int screenLayoutSize : mScreenLayoutSize) {
+                if (firstConfig.isLayoutSizeAtLeast(screenLayoutSize)
+                        != secondConfig.isLayoutSizeAtLeast(screenLayoutSize)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean crossesScreenLayoutLongThreshold(int firstScreenLayout,
+            int secondScreenLayout) {
+        final int firstScreenLayoutLongValue = firstScreenLayout
+                & Configuration.SCREENLAYOUT_LONG_MASK;
+        final int secondScreenLayoutLongValue = secondScreenLayout
+                & Configuration.SCREENLAYOUT_LONG_MASK;
+        return mScreenLayoutLongSet && firstScreenLayoutLongValue != secondScreenLayoutLongValue;
+    }
+
+    /**
+     * Returns whether non-size related screen layout attributes have changed. If true, then
+     * {@link ActivityInfo#CONFIG_SCREEN_LAYOUT} should not be filtered out in
+     * {@link SizeConfigurationBuckets#filterDiff()} because the non-size related attributes
+     * do not have a bucket range like the size-related attributes of screen layout.
+     */
+    @VisibleForTesting
+    public static boolean areNonSizeLayoutFieldsUnchanged(int oldScreenLayout,
+            int newScreenLayout) {
+        final int nonSizeRelatedFields = Configuration.SCREENLAYOUT_LAYOUTDIR_MASK
+                | Configuration.SCREENLAYOUT_ROUND_MASK | Configuration.SCREENLAYOUT_COMPAT_NEEDED;
+        return (oldScreenLayout & nonSizeRelatedFields) == (newScreenLayout & nonSizeRelatedFields);
+    }
+
+    /**
      * The purpose of this method is to decide whether the activity needs to be relaunched upon
      * changing its size. In most cases the activities don't need to be relaunched, if the resize
      * is small, all the activity content has to do is relayout itself within new bounds. There are
@@ -132,7 +229,8 @@
      * it resizes width from 620dp to 700dp, it won't be relaunched as it stays on the same side
      * of the threshold.
      */
-    private static boolean crossesSizeThreshold(int[] thresholds, int firstDp,
+    @VisibleForTesting
+    public static boolean crossesSizeThreshold(int[] thresholds, int firstDp,
             int secondDp) {
         if (thresholds == null) {
             return false;
@@ -150,12 +248,13 @@
     @Override
     public String toString() {
         return Arrays.toString(mHorizontal) + " " + Arrays.toString(mVertical) + " "
-                + Arrays.toString(mSmallest);
+                + Arrays.toString(mSmallest) + " " + Arrays.toString(mScreenLayoutSize) + " "
+                + mScreenLayoutLongSet;
     }
 
 
 
-    // Code below generated by codegen v1.0.22.
+    // Code below generated by codegen v1.0.23.
     //
     // DO NOT MODIFY!
     // CHECKSTYLE:OFF Generated code
@@ -177,15 +276,25 @@
      *   Vertical (screenHeightDp) buckets
      * @param smallest
      *   Smallest (smallestScreenWidthDp) buckets
+     * @param screenLayoutSize
+     *   Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets
+     * @param screenLayoutLongSet
+     *   Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a
+     *   value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and
+     *   SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change.
      */
     @DataClass.Generated.Member
     public SizeConfigurationBuckets(
             @Nullable int[] horizontal,
             @Nullable int[] vertical,
-            @Nullable int[] smallest) {
+            @Nullable int[] smallest,
+            @Nullable int[] screenLayoutSize,
+            boolean screenLayoutLongSet) {
         this.mHorizontal = horizontal;
         this.mVertical = vertical;
         this.mSmallest = smallest;
+        this.mScreenLayoutSize = screenLayoutSize;
+        this.mScreenLayoutLongSet = screenLayoutLongSet;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -214,6 +323,24 @@
         return mSmallest;
     }
 
+    /**
+     * Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets
+     */
+    @DataClass.Generated.Member
+    public @Nullable int[] getScreenLayoutSize() {
+        return mScreenLayoutSize;
+    }
+
+    /**
+     * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a
+     * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and
+     * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change.
+     */
+    @DataClass.Generated.Member
+    public boolean isScreenLayoutLongSet() {
+        return mScreenLayoutLongSet;
+    }
+
     @Override
     @DataClass.Generated.Member
     public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
@@ -221,13 +348,16 @@
         // void parcelFieldName(Parcel dest, int flags) { ... }
 
         byte flg = 0;
+        if (mScreenLayoutLongSet) flg |= 0x10;
         if (mHorizontal != null) flg |= 0x1;
         if (mVertical != null) flg |= 0x2;
         if (mSmallest != null) flg |= 0x4;
+        if (mScreenLayoutSize != null) flg |= 0x8;
         dest.writeByte(flg);
         if (mHorizontal != null) dest.writeIntArray(mHorizontal);
         if (mVertical != null) dest.writeIntArray(mVertical);
         if (mSmallest != null) dest.writeIntArray(mSmallest);
+        if (mScreenLayoutSize != null) dest.writeIntArray(mScreenLayoutSize);
     }
 
     @Override
@@ -242,13 +372,17 @@
         // static FieldType unparcelFieldName(Parcel in) { ... }
 
         byte flg = in.readByte();
+        boolean screenLayoutLongSet = (flg & 0x10) != 0;
         int[] horizontal = (flg & 0x1) == 0 ? null : in.createIntArray();
         int[] vertical = (flg & 0x2) == 0 ? null : in.createIntArray();
         int[] smallest = (flg & 0x4) == 0 ? null : in.createIntArray();
+        int[] screenLayoutSize = (flg & 0x8) == 0 ? null : in.createIntArray();
 
         this.mHorizontal = horizontal;
         this.mVertical = vertical;
         this.mSmallest = smallest;
+        this.mScreenLayoutSize = screenLayoutSize;
+        this.mScreenLayoutLongSet = screenLayoutLongSet;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -268,10 +402,10 @@
     };
 
     @DataClass.Generated(
-            time = 1615845864280L,
-            codegenVersion = "1.0.22",
+            time = 1628273704583L,
+            codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/window/SizeConfigurationBuckets.java",
-            inputSignatures = "private final @android.annotation.Nullable int[] mHorizontal\nprivate final @android.annotation.Nullable int[] mVertical\nprivate final @android.annotation.Nullable int[] mSmallest\npublic static  int filterDiff(int,android.content.res.Configuration,android.content.res.Configuration,android.window.SizeConfigurationBuckets)\nprivate  boolean crossesHorizontalSizeThreshold(int,int)\nprivate  boolean crossesVerticalSizeThreshold(int,int)\nprivate  boolean crossesSmallestSizeThreshold(int,int)\nprivate static  boolean crossesSizeThreshold(int[],int,int)\npublic @java.lang.Override java.lang.String toString()\nclass SizeConfigurationBuckets extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genAidl=true)")
+            inputSignatures = "private final @android.annotation.Nullable int[] mHorizontal\nprivate final @android.annotation.Nullable int[] mVertical\nprivate final @android.annotation.Nullable int[] mSmallest\nprivate final @android.annotation.Nullable int[] mScreenLayoutSize\nprivate final  boolean mScreenLayoutLongSet\npublic static  int filterDiff(int,android.content.res.Configuration,android.content.res.Configuration,android.window.SizeConfigurationBuckets)\nprivate  boolean crossesHorizontalSizeThreshold(int,int)\nprivate  boolean crossesVerticalSizeThreshold(int,int)\nprivate  boolean crossesSmallestSizeThreshold(int,int)\npublic @com.android.internal.annotations.VisibleForTesting boolean crossesScreenLayoutSizeThreshold(android.content.res.Configuration,android.content.res.Configuration)\nprivate  boolean crossesScreenLayoutLongThreshold(int,int)\npublic static @com.android.internal.annotations.VisibleForTesting boolean areNonSizeLayoutFieldsUnchanged(int,int)\npublic static @com.android.internal.annotations.VisibleForTesting boolean crossesSizeThreshold(int[],int,int)\npublic @java.lang.Override java.lang.String toString()\nclass SizeConfigurationBuckets extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genAidl=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/window/SplashScreen.java b/core/java/android/window/SplashScreen.java
index 3e00758..3354a6c 100644
--- a/core/java/android/window/SplashScreen.java
+++ b/core/java/android/window/SplashScreen.java
@@ -241,7 +241,6 @@
 
         public void handOverSplashScreenView(@NonNull IBinder token,
                 @NonNull SplashScreenView splashScreenView) {
-            transferSurface(splashScreenView);
             dispatchOnExitAnimation(token, splashScreenView);
         }
 
@@ -265,9 +264,5 @@
                 return impl != null && impl.mExitAnimationListener != null;
             }
         }
-
-        private void transferSurface(@NonNull SplashScreenView splashScreenView) {
-            splashScreenView.transferSurface();
-        }
     }
 }
diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java
index acf20d7..f748d4b 100644
--- a/core/java/android/window/SplashScreenView.java
+++ b/core/java/android/window/SplashScreenView.java
@@ -59,6 +59,7 @@
 
 import java.time.Duration;
 import java.time.Instant;
+import java.util.function.Consumer;
 
 /**
  * <p>The view which allows an activity to customize its splash screen exit animation.</p>
@@ -144,6 +145,7 @@
         private Bitmap mParceledBrandingBitmap;
         private Instant mIconAnimationStart;
         private Duration mIconAnimationDuration;
+        private Consumer<Runnable> mUiThreadInitTask;
 
         public Builder(@NonNull Context context) {
             mContext = context;
@@ -232,6 +234,15 @@
         }
 
         /**
+         * Set the Runnable that can receive the task which should be executed on UI thread.
+         * @param uiThreadInitTask
+         */
+        public Builder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) {
+            mUiThreadInitTask = uiThreadInitTask;
+            return this;
+        }
+
+        /**
          * Set the Drawable object and size for the branding view.
          */
         public Builder setBrandingDrawable(@Nullable Drawable branding, int width, int height) {
@@ -262,7 +273,11 @@
             // center icon
             if (mIconDrawable instanceof SplashScreenView.IconAnimateListener
                     || mSurfacePackage != null) {
-                view.mIconView = createSurfaceView(view);
+                if (mUiThreadInitTask != null) {
+                    mUiThreadInitTask.accept(() -> view.mIconView = createSurfaceView(view));
+                } else {
+                    view.mIconView = createSurfaceView(view);
+                }
                 view.initIconAnimation(mIconDrawable,
                         mIconAnimationDuration != null ? mIconAnimationDuration.toMillis() : 0);
                 view.mIconAnimationStart = mIconAnimationStart;
@@ -316,7 +331,9 @@
         }
 
         private SurfaceView createSurfaceView(@NonNull SplashScreenView view) {
-            final SurfaceView surfaceView = new SurfaceView(view.getContext());
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "SplashScreenView#createSurfaceView");
+            final Context viewContext = view.getContext();
+            final SurfaceView surfaceView = new SurfaceView(viewContext);
             surfaceView.setPadding(0, 0, 0, 0);
             surfaceView.setBackground(mIconBackground);
             if (mSurfacePackage == null) {
@@ -326,10 +343,10 @@
                                     + Thread.currentThread().getId());
                 }
 
-                SurfaceControlViewHost viewHost = new SurfaceControlViewHost(mContext,
-                        mContext.getDisplay(),
+                SurfaceControlViewHost viewHost = new SurfaceControlViewHost(viewContext,
+                        viewContext.getDisplay(),
                         surfaceView.getHostToken());
-                ImageView imageView = new ImageView(mContext);
+                ImageView imageView = new ImageView(viewContext);
                 imageView.setBackground(mIconDrawable);
                 viewHost.setView(imageView, mIconSize, mIconSize);
                 SurfaceControlViewHost.SurfacePackage surfacePackage = viewHost.getSurfacePackage();
@@ -360,6 +377,7 @@
 
             view.addView(surfaceView);
             view.mSurfaceView = surfaceView;
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             return surfaceView;
         }
     }
@@ -446,7 +464,10 @@
     }
 
 
-    void transferSurface() {
+    /**
+     * @hide
+     */
+    public void syncTransferSurfaceOnDraw() {
         if (mSurfacePackage == null) {
             return;
         }
@@ -456,8 +477,8 @@
                             String.format("SurfacePackage'surface reparented to %s", parent)));
             Log.d(TAG, "Transferring surface " + mSurfaceView.toString());
         }
-        mSurfaceView.setChildSurfacePackage(mSurfacePackage);
 
+        mSurfaceView.setChildSurfacePackageOnDraw(mSurfacePackage);
     }
 
     void initIconAnimation(Drawable iconDrawable, long duration) {
@@ -515,10 +536,6 @@
             restoreSystemUIColors();
             mWindow = null;
         }
-        if (mHostActivity != null) {
-            mHostActivity.setSplashScreenView(null);
-            mHostActivity = null;
-        }
         mHasRemoved = true;
     }
 
@@ -531,17 +548,14 @@
 
     private void releaseAnimationSurfaceHost() {
         if (mSurfaceHost != null && !mIsCopied) {
-            final SurfaceControlViewHost finalSurfaceHost = mSurfaceHost;
+            if (DEBUG) {
+                Log.d(TAG,
+                        "Shell removed splash screen."
+                                + " Releasing SurfaceControlViewHost on thread #"
+                                + Thread.currentThread().getId());
+            }
+            releaseIconHost(mSurfaceHost);
             mSurfaceHost = null;
-            finalSurfaceHost.getView().post(() -> {
-                if (DEBUG) {
-                    Log.d(TAG,
-                            "Shell removed splash screen."
-                                    + " Releasing SurfaceControlViewHost on thread #"
-                                    + Thread.currentThread().getId());
-                }
-                finalSurfaceHost.release();
-            });
         } else if (mSurfacePackage != null && mSurfaceHost == null) {
             mSurfacePackage = null;
             mClientCallback.sendResult(null);
@@ -549,13 +563,24 @@
     }
 
     /**
+     * Release the host which hold the SurfaceView of the icon.
+     * @hide
+     */
+    public static void releaseIconHost(SurfaceControlViewHost host) {
+        final Drawable background = host.getView().getBackground();
+        if (background instanceof SplashScreenView.IconAnimateListener) {
+            ((SplashScreenView.IconAnimateListener) background).stopAnimation();
+        }
+        host.release();
+    }
+
+    /**
      * Called when this view is attached to an activity. This also makes SystemUI colors
      * transparent so the content of splash screen view can draw fully.
      *
      * @hide
      */
     public void attachHostActivityAndSetSystemUIColors(Activity activity, Window window) {
-        activity.setSplashScreenView(this);
         mHostActivity = activity;
         mWindow = window;
         final WindowManager.LayoutParams attr = window.getAttributes();
@@ -639,6 +664,11 @@
          * @return true if this drawable object can also be animated and it can be played now.
          */
         boolean prepareAnimate(long duration, Runnable startListener);
+
+        /**
+         * Stop animation.
+         */
+        void stopAnimation();
     }
 
     /**
diff --git a/core/java/android/window/StartingWindowInfo.java b/core/java/android/window/StartingWindowInfo.java
index 8c64474..5950e9f 100644
--- a/core/java/android/window/StartingWindowInfo.java
+++ b/core/java/android/window/StartingWindowInfo.java
@@ -19,13 +19,13 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.TestApi;
 import android.app.ActivityManager;
 import android.app.TaskInfo;
 import android.content.pm.ActivityInfo;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.WindowManager;
 
 /**
@@ -33,7 +33,6 @@
  * start in the system.
  * @hide
  */
-@TestApi
 public final class StartingWindowInfo implements Parcelable {
     /**
      * Prefer nothing or not care the type of starting window.
@@ -165,7 +164,13 @@
      * TaskSnapshot.
      * @hide
      */
-    public TaskSnapshot mTaskSnapshot;
+    public TaskSnapshot taskSnapshot;
+
+    /**
+     * The requested insets visibility of the top main window.
+     * @hide
+     */
+    public final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
 
     public StartingWindowInfo() {
 
@@ -190,7 +195,8 @@
         dest.writeTypedObject(mainWindowLayoutParams, flags);
         dest.writeInt(splashScreenThemeResId);
         dest.writeBoolean(isKeyguardOccluded);
-        dest.writeTypedObject(mTaskSnapshot, flags);
+        dest.writeTypedObject(taskSnapshot, flags);
+        requestedVisibilities.writeToParcel(dest, flags);
     }
 
     void readFromParcel(@NonNull Parcel source) {
@@ -203,7 +209,8 @@
         mainWindowLayoutParams = source.readTypedObject(WindowManager.LayoutParams.CREATOR);
         splashScreenThemeResId = source.readInt();
         isKeyguardOccluded = source.readBoolean();
-        mTaskSnapshot = source.readTypedObject(TaskSnapshot.CREATOR);
+        taskSnapshot = source.readTypedObject(TaskSnapshot.CREATOR);
+        requestedVisibilities.readFromParcel(source);
     }
 
     @Override
diff --git a/core/java/android/window/StartingWindowRemovalInfo.aidl b/core/java/android/window/StartingWindowRemovalInfo.aidl
new file mode 100644
index 0000000..8e4ac04
--- /dev/null
+++ b/core/java/android/window/StartingWindowRemovalInfo.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+/** @hide */
+parcelable StartingWindowRemovalInfo;
\ No newline at end of file
diff --git a/core/java/android/window/StartingWindowRemovalInfo.java b/core/java/android/window/StartingWindowRemovalInfo.java
new file mode 100644
index 0000000..573db0d
--- /dev/null
+++ b/core/java/android/window/StartingWindowRemovalInfo.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.SurfaceControl;
+
+/**
+ * Information when removing a starting window of a particular task.
+ * @hide
+ */
+public final class StartingWindowRemovalInfo implements Parcelable {
+
+    /**
+     * The identifier of a task.
+     * @hide
+     */
+    public int taskId;
+
+    /**
+     * The animation container layer of the top activity.
+     * @hide
+     */
+    @Nullable
+    public SurfaceControl windowAnimationLeash;
+
+    /**
+     * The main window frame for the window of the top activity.
+     * @hide
+     */
+    @Nullable
+    public Rect mainFrame;
+
+    /**
+     * Whether need to play reveal animation.
+     * @hide
+     */
+    public boolean playRevealAnimation;
+
+    /**
+     * Whether need to defer removing the starting window for IME.
+     * @hide
+     */
+    public boolean deferRemoveForIme;
+
+    public StartingWindowRemovalInfo() {
+
+    }
+
+    private StartingWindowRemovalInfo(@NonNull Parcel source) {
+        readFromParcel(source);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    void readFromParcel(@NonNull Parcel source) {
+        taskId = source.readInt();
+        windowAnimationLeash = source.readTypedObject(SurfaceControl.CREATOR);
+        mainFrame = source.readTypedObject(Rect.CREATOR);
+        playRevealAnimation = source.readBoolean();
+        deferRemoveForIme = source.readBoolean();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(taskId);
+        dest.writeTypedObject(windowAnimationLeash, flags);
+        dest.writeTypedObject(mainFrame, flags);
+        dest.writeBoolean(playRevealAnimation);
+        dest.writeBoolean(deferRemoveForIme);
+    }
+
+    @Override
+    public String toString() {
+        return "StartingWindowRemovalInfo{taskId=" + taskId
+                + " frame=" + mainFrame
+                + " playRevealAnimation=" + playRevealAnimation
+                + " deferRemoveForIme=" + deferRemoveForIme + "}";
+    }
+
+    public static final @android.annotation.NonNull Creator<StartingWindowRemovalInfo> CREATOR =
+            new Creator<StartingWindowRemovalInfo>() {
+                public StartingWindowRemovalInfo createFromParcel(@NonNull Parcel source) {
+                    return new StartingWindowRemovalInfo(source);
+                }
+                public StartingWindowRemovalInfo[] newArray(int size) {
+                    return new StartingWindowRemovalInfo[size];
+                }
+            };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/core/java/android/window/TaskFragmentAppearedInfo.aidl
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to core/java/android/window/TaskFragmentAppearedInfo.aidl
index b391bd9..3729c09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/core/java/android/window/TaskFragmentAppearedInfo.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package android.window;
 
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
-}
+/**
+ * Data object for the TaskFragment info provided when a TaskFragment is presented to an organizer.
+ * @hide
+ */
+parcelable TaskFragmentAppearedInfo;
diff --git a/core/java/android/window/TaskFragmentAppearedInfo.java b/core/java/android/window/TaskFragmentAppearedInfo.java
new file mode 100644
index 0000000..89d9a95
--- /dev/null
+++ b/core/java/android/window/TaskFragmentAppearedInfo.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.SurfaceControl;
+
+/**
+ * Data object for the TaskFragment info provided when a TaskFragment is presented to an organizer.
+ * @hide
+ */
+@TestApi
+public final class TaskFragmentAppearedInfo implements Parcelable {
+
+    @NonNull
+    private final TaskFragmentInfo mTaskFragmentInfo;
+
+    @NonNull
+    private final SurfaceControl mLeash;
+
+    /** @hide */
+    public TaskFragmentAppearedInfo(
+            @NonNull TaskFragmentInfo taskFragmentInfo, @NonNull SurfaceControl leash) {
+        mTaskFragmentInfo = taskFragmentInfo;
+        mLeash = leash;
+    }
+
+    @NonNull
+    public TaskFragmentInfo getTaskFragmentInfo() {
+        return mTaskFragmentInfo;
+    }
+
+    @NonNull
+    public SurfaceControl getLeash() {
+        return mLeash;
+    }
+
+    private TaskFragmentAppearedInfo(Parcel in) {
+        mTaskFragmentInfo = in.readTypedObject(TaskFragmentInfo.CREATOR);
+        mLeash = in.readTypedObject(SurfaceControl.CREATOR);
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedObject(mTaskFragmentInfo, flags);
+        dest.writeTypedObject(mLeash, flags);
+    }
+
+    @NonNull
+    public static final Creator<TaskFragmentAppearedInfo> CREATOR =
+            new Creator<TaskFragmentAppearedInfo>() {
+                @Override
+                public TaskFragmentAppearedInfo createFromParcel(Parcel in) {
+                    return new TaskFragmentAppearedInfo(in);
+                }
+
+                @Override
+                public TaskFragmentAppearedInfo[] newArray(int size) {
+                    return new TaskFragmentAppearedInfo[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "TaskFragmentAppearedInfo{"
+                + " taskFragmentInfo=" + mTaskFragmentInfo
+                + "}";
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/core/java/android/window/TaskFragmentCreationParams.aidl
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to core/java/android/window/TaskFragmentCreationParams.aidl
index b391bd9..fde5089 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/core/java/android/window/TaskFragmentCreationParams.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package android.window;
 
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
-}
+/**
+ * Data object for options to create TaskFragment with.
+ * @hide
+ */
+parcelable TaskFragmentCreationParams;
diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java
new file mode 100644
index 0000000..81ab783
--- /dev/null
+++ b/core/java/android/window/TaskFragmentCreationParams.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.app.WindowConfiguration.WindowingMode;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data object for options to create TaskFragment with.
+ * @hide
+ */
+@TestApi
+public final class TaskFragmentCreationParams implements Parcelable {
+
+    /** The organizer that will organize this TaskFragment. */
+    @NonNull
+    private final TaskFragmentOrganizerToken mOrganizer;
+
+    /**
+     * Unique token assigned from the client organizer to identify the {@link TaskFragmentInfo} when
+     * a new TaskFragment is created with this option.
+     */
+    @NonNull
+    private final IBinder mFragmentToken;
+
+    /**
+     * Activity token used to identify the leaf Task to create the TaskFragment in. It has to belong
+     * to the same app as the root Activity of the target Task.
+     */
+    @NonNull
+    private final IBinder mOwnerToken;
+
+    /** The initial bounds of the TaskFragment. Fills parent if empty. */
+    @NonNull
+    private final Rect mInitialBounds = new Rect();
+
+    /** The initial windowing mode of the TaskFragment. Inherits from parent if not set. */
+    @WindowingMode
+    private int mWindowingMode = WINDOWING_MODE_UNDEFINED;
+
+    private TaskFragmentCreationParams(
+            @NonNull TaskFragmentOrganizerToken organizer,
+            @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) {
+        mOrganizer = organizer;
+        mFragmentToken = fragmentToken;
+        mOwnerToken = ownerToken;
+    }
+
+    @NonNull
+    public TaskFragmentOrganizerToken getOrganizer() {
+        return mOrganizer;
+    }
+
+    @NonNull
+    public IBinder getFragmentToken() {
+        return mFragmentToken;
+    }
+
+    @NonNull
+    public IBinder getOwnerToken() {
+        return mOwnerToken;
+    }
+
+    @NonNull
+    public Rect getInitialBounds() {
+        return mInitialBounds;
+    }
+
+    @WindowingMode
+    public int getWindowingMode() {
+        return mWindowingMode;
+    }
+
+    private TaskFragmentCreationParams(Parcel in) {
+        mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in);
+        mFragmentToken = in.readStrongBinder();
+        mOwnerToken = in.readStrongBinder();
+        mInitialBounds.readFromParcel(in);
+        mWindowingMode = in.readInt();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        mOrganizer.writeToParcel(dest, flags);
+        dest.writeStrongBinder(mFragmentToken);
+        dest.writeStrongBinder(mOwnerToken);
+        mInitialBounds.writeToParcel(dest, flags);
+        dest.writeInt(mWindowingMode);
+    }
+
+    @NonNull
+    public static final Creator<TaskFragmentCreationParams> CREATOR =
+            new Creator<TaskFragmentCreationParams>() {
+                @Override
+                public TaskFragmentCreationParams createFromParcel(Parcel in) {
+                    return new TaskFragmentCreationParams(in);
+                }
+
+                @Override
+                public TaskFragmentCreationParams[] newArray(int size) {
+                    return new TaskFragmentCreationParams[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "TaskFragmentCreationParams{"
+                + " organizer=" + mOrganizer
+                + " fragmentToken=" + mFragmentToken
+                + " ownerToken=" + mOwnerToken
+                + " initialBounds=" + mInitialBounds
+                + " windowingMode=" + mWindowingMode
+                + "}";
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Builder to construct the options to create TaskFragment with. */
+    public static final class Builder {
+
+        @NonNull
+        private final TaskFragmentOrganizerToken mOrganizer;
+
+        @NonNull
+        private final IBinder mFragmentToken;
+
+        @NonNull
+        private final IBinder mOwnerToken;
+
+        @NonNull
+        private final Rect mInitialBounds = new Rect();
+
+        @WindowingMode
+        private int mWindowingMode = WINDOWING_MODE_UNDEFINED;
+
+        public Builder(@NonNull TaskFragmentOrganizerToken organizer,
+                @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) {
+            mOrganizer = organizer;
+            mFragmentToken = fragmentToken;
+            mOwnerToken = ownerToken;
+        }
+
+        /** Sets the initial bounds for the TaskFragment. */
+        @NonNull
+        public Builder setInitialBounds(@NonNull Rect bounds) {
+            mInitialBounds.set(bounds);
+            return this;
+        }
+
+        /** Sets the initial windowing mode for the TaskFragment. */
+        @NonNull
+        public Builder setWindowingMode(@WindowingMode int windowingMode) {
+            mWindowingMode = windowingMode;
+            return this;
+        }
+
+        /** Constructs the options to create TaskFragment with. */
+        @NonNull
+        public TaskFragmentCreationParams build() {
+            final TaskFragmentCreationParams result = new TaskFragmentCreationParams(
+                    mOrganizer, mFragmentToken, mOwnerToken);
+            result.mInitialBounds.set(mInitialBounds);
+            result.mWindowingMode = mWindowingMode;
+            return result;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/core/java/android/window/TaskFragmentInfo.aidl
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to core/java/android/window/TaskFragmentInfo.aidl
index b391bd9..461a736 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/core/java/android/window/TaskFragmentInfo.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package android.window;
 
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
-}
+/**
+ * Stores information about a particular TaskFragment.
+ * @hide
+ */
+parcelable TaskFragmentInfo;
diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java
new file mode 100644
index 0000000..165dcdf
--- /dev/null
+++ b/core/java/android/window/TaskFragmentInfo.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import static android.app.WindowConfiguration.WindowingMode;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Stores information about a particular TaskFragment.
+ * @hide
+ */
+@TestApi
+public final class TaskFragmentInfo implements Parcelable {
+
+    /**
+     * Client assigned unique token in {@link TaskFragmentCreationParams#getFragmentToken()} to
+     * create this TaskFragment with.
+     */
+    @NonNull
+    private final IBinder mFragmentToken;
+
+    @NonNull
+    private final WindowContainerToken mToken;
+
+    @NonNull
+    private final Configuration mConfiguration = new Configuration();
+
+    /** Whether the TaskFragment contains any child Window Container. */
+    private final boolean mIsEmpty;
+
+    /** The number of the running activities in the TaskFragment. */
+    private final int mRunningActivityCount;
+
+    /** Whether this TaskFragment is visible on the window hierarchy. */
+    private final boolean mIsVisible;
+
+    /**
+     * List of Activity tokens that are children of this TaskFragment. It only contains Activities
+     * that belong to the organizer process for security.
+     */
+    @NonNull
+    private final List<IBinder> mActivities = new ArrayList<>();
+
+    /** Relative position of the fragment's top left corner in the parent container. */
+    private final Point mPositionInParent;
+
+    /**
+     * Whether the last running activity in the TaskFragment was finished due to clearing task while
+     * launching an activity in the host Task.
+     */
+    private final boolean mIsTaskClearedForReuse;
+
+    /** @hide */
+    public TaskFragmentInfo(
+            @NonNull IBinder fragmentToken, @NonNull WindowContainerToken token,
+            @NonNull Configuration configuration, boolean isEmpty, int runningActivityCount,
+            boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent,
+            boolean isTaskClearedForReuse) {
+        mFragmentToken = requireNonNull(fragmentToken);
+        mToken = requireNonNull(token);
+        mConfiguration.setTo(configuration);
+        mIsEmpty = isEmpty;
+        mRunningActivityCount = runningActivityCount;
+        mIsVisible = isVisible;
+        mActivities.addAll(activities);
+        mPositionInParent = requireNonNull(positionInParent);
+        mIsTaskClearedForReuse = isTaskClearedForReuse;
+    }
+
+    @NonNull
+    public IBinder getFragmentToken() {
+        return mFragmentToken;
+    }
+
+    @NonNull
+    public WindowContainerToken getToken() {
+        return mToken;
+    }
+
+    @NonNull
+    public Configuration getConfiguration() {
+        return mConfiguration;
+    }
+
+    public boolean isEmpty() {
+        return mIsEmpty;
+    }
+
+    public boolean hasRunningActivity() {
+        return mRunningActivityCount > 0;
+    }
+
+    public int getRunningActivityCount() {
+        return mRunningActivityCount;
+    }
+
+    public boolean isVisible() {
+        return mIsVisible;
+    }
+
+    @NonNull
+    public List<IBinder> getActivities() {
+        return mActivities;
+    }
+
+    /** Returns the relative position of the fragment's top left corner in the parent container. */
+    @NonNull
+    public Point getPositionInParent() {
+        return mPositionInParent;
+    }
+
+    public boolean isTaskClearedForReuse() {
+        return mIsTaskClearedForReuse;
+    }
+
+    @WindowingMode
+    public int getWindowingMode() {
+        return mConfiguration.windowConfiguration.getWindowingMode();
+    }
+
+    /**
+     * Returns {@code true} if the parameters that are important for task fragment organizers are
+     * equal between this {@link TaskFragmentInfo} and {@param that}.
+     */
+    public boolean equalsForTaskFragmentOrganizer(@Nullable TaskFragmentInfo that) {
+        if (that == null) {
+            return false;
+        }
+
+        return mFragmentToken.equals(that.mFragmentToken)
+                && mToken.equals(that.mToken)
+                && mIsEmpty == that.mIsEmpty
+                && mRunningActivityCount == that.mRunningActivityCount
+                && mIsVisible == that.mIsVisible
+                && getWindowingMode() == that.getWindowingMode()
+                && mActivities.equals(that.mActivities)
+                && mPositionInParent.equals(that.mPositionInParent)
+                && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse;
+    }
+
+    private TaskFragmentInfo(Parcel in) {
+        mFragmentToken = in.readStrongBinder();
+        mToken = in.readTypedObject(WindowContainerToken.CREATOR);
+        mConfiguration.readFromParcel(in);
+        mIsEmpty = in.readBoolean();
+        mRunningActivityCount = in.readInt();
+        mIsVisible = in.readBoolean();
+        in.readBinderList(mActivities);
+        mPositionInParent = requireNonNull(in.readTypedObject(Point.CREATOR));
+        mIsTaskClearedForReuse = in.readBoolean();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mFragmentToken);
+        dest.writeTypedObject(mToken, flags);
+        mConfiguration.writeToParcel(dest, flags);
+        dest.writeBoolean(mIsEmpty);
+        dest.writeInt(mRunningActivityCount);
+        dest.writeBoolean(mIsVisible);
+        dest.writeBinderList(mActivities);
+        dest.writeTypedObject(mPositionInParent, flags);
+        dest.writeBoolean(mIsTaskClearedForReuse);
+    }
+
+    @NonNull
+    public static final Creator<TaskFragmentInfo> CREATOR =
+            new Creator<TaskFragmentInfo>() {
+                @Override
+                public TaskFragmentInfo createFromParcel(Parcel in) {
+                    return new TaskFragmentInfo(in);
+                }
+
+                @Override
+                public TaskFragmentInfo[] newArray(int size) {
+                    return new TaskFragmentInfo[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "TaskFragmentInfo{"
+                + " fragmentToken=" + mFragmentToken
+                + " token=" + mToken
+                + " isEmpty=" + mIsEmpty
+                + " runningActivityCount=" + mRunningActivityCount
+                + " isVisible=" + mIsVisible
+                + " positionInParent=" + mPositionInParent
+                + " isTaskClearedForReuse=" + mIsTaskClearedForReuse
+                + "}";
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
new file mode 100644
index 0000000..337c5a1
--- /dev/null
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.RemoteAnimationDefinition;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface for WindowManager to delegate control of {@code TaskFragment}.
+ * @hide
+ */
+@TestApi
+public class TaskFragmentOrganizer extends WindowOrganizer {
+
+    /**
+     * Key to the exception in {@link Bundle} in {@link ITaskFragmentOrganizer#onTaskFragmentError}.
+     */
+    private static final String KEY_ERROR_CALLBACK_EXCEPTION = "fragment_exception";
+
+    /**
+     * Creates a {@link Bundle} with an exception that can be passed to
+     * {@link ITaskFragmentOrganizer#onTaskFragmentError}.
+     * @hide
+     */
+    public static Bundle putExceptionInBundle(@NonNull Throwable exception) {
+        final Bundle exceptionBundle = new Bundle();
+        exceptionBundle.putSerializable(KEY_ERROR_CALLBACK_EXCEPTION, exception);
+        return exceptionBundle;
+    }
+
+    /**
+     * Callbacks from WM Core are posted on this executor.
+     */
+    private final Executor mExecutor;
+
+    public TaskFragmentOrganizer(@NonNull Executor executor) {
+        mExecutor = executor;
+    }
+
+    /**
+     * Gets the executor to run callbacks on.
+     */
+    @NonNull
+    public Executor getExecutor() {
+        return mExecutor;
+    }
+
+    /**
+     * Registers a TaskFragmentOrganizer to manage TaskFragments.
+     */
+    @CallSuper
+    public void registerOrganizer() {
+        try {
+            getController().registerOrganizer(mInterface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregisters a previously registered TaskFragmentOrganizer.
+     */
+    @CallSuper
+    public void unregisterOrganizer() {
+        try {
+            getController().unregisterOrganizer(mInterface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers remote animations per transition type for the organizer. It will override the
+     * animations if the transition only contains windows that belong to the organized
+     * TaskFragments.
+     * @hide
+     */
+    @CallSuper
+    public void registerRemoteAnimations(@NonNull RemoteAnimationDefinition definition) {
+        try {
+            getController().registerRemoteAnimations(mInterface, definition);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregisters remote animations per transition type for the organizer.
+     * @hide
+     */
+    @CallSuper
+    public void unregisterRemoteAnimations() {
+        try {
+            getController().unregisterRemoteAnimations(mInterface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** Called when a TaskFragment is created and organized by this organizer. */
+    public void onTaskFragmentAppeared(
+            @NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) {}
+
+    /** Called when the status of an organized TaskFragment is changed. */
+    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {}
+
+    /** Called when an organized TaskFragment is removed. */
+    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {}
+
+    /**
+     * Called when the parent leaf Task of organized TaskFragments is changed.
+     * When the leaf Task is changed, the organizer may want to update the TaskFragments in one
+     * transaction.
+     *
+     * For case like screen size change, it will trigger onTaskFragmentParentInfoChanged with new
+     * Task bounds, but may not trigger onTaskFragmentInfoChanged because there can be an override
+     * bounds.
+     */
+    public void onTaskFragmentParentInfoChanged(
+            @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) {}
+
+    /**
+     * Called when the {@link WindowContainerTransaction} created with
+     * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side.
+     *
+     * @param errorCallbackToken    token set in
+     *                             {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)}
+     * @param exception             exception from the server side.
+     */
+    public void onTaskFragmentError(
+            @NonNull IBinder errorCallbackToken, @NonNull Throwable exception) {}
+
+    @Override
+    public void applyTransaction(@NonNull WindowContainerTransaction t) {
+        t.setTaskFragmentOrganizer(mInterface);
+        super.applyTransaction(t);
+    }
+
+    // Suppress the lint because it is not a registration method.
+    @SuppressWarnings("ExecutorRegistration")
+    @Override
+    public int applySyncTransaction(@NonNull WindowContainerTransaction t,
+            @NonNull WindowContainerTransactionCallback callback) {
+        t.setTaskFragmentOrganizer(mInterface);
+        return super.applySyncTransaction(t, callback);
+    }
+
+    private final ITaskFragmentOrganizer mInterface = new ITaskFragmentOrganizer.Stub() {
+        @Override
+        public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentInfo) {
+            mExecutor.execute(
+                    () -> TaskFragmentOrganizer.this.onTaskFragmentAppeared(taskFragmentInfo));
+        }
+
+        @Override
+        public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+            mExecutor.execute(
+                    () -> TaskFragmentOrganizer.this.onTaskFragmentInfoChanged(taskFragmentInfo));
+        }
+
+        @Override
+        public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+            mExecutor.execute(
+                    () -> TaskFragmentOrganizer.this.onTaskFragmentVanished(taskFragmentInfo));
+        }
+
+        @Override
+        public void onTaskFragmentParentInfoChanged(
+                @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) {
+            mExecutor.execute(
+                    () -> TaskFragmentOrganizer.this.onTaskFragmentParentInfoChanged(
+                            fragmentToken, parentConfig));
+        }
+
+        @Override
+        public void onTaskFragmentError(
+                @NonNull IBinder errorCallbackToken, @NonNull Bundle exceptionBundle) {
+            mExecutor.execute(() -> TaskFragmentOrganizer.this.onTaskFragmentError(
+                    errorCallbackToken,
+                    (Throwable) exceptionBundle.getSerializable(KEY_ERROR_CALLBACK_EXCEPTION)));
+        }
+    };
+
+    private final TaskFragmentOrganizerToken mToken = new TaskFragmentOrganizerToken(mInterface);
+
+    @NonNull
+    public TaskFragmentOrganizerToken getOrganizerToken() {
+        return mToken;
+    }
+
+    private ITaskFragmentOrganizerController getController() {
+        try {
+            return getWindowOrganizerController().getTaskFragmentOrganizerController();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+}
diff --git a/core/java/android/window/TaskFragmentOrganizerToken.java b/core/java/android/window/TaskFragmentOrganizerToken.java
new file mode 100644
index 0000000..d5216a6
--- /dev/null
+++ b/core/java/android/window/TaskFragmentOrganizerToken.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Interface to communicate between window manager and {@link TaskFragmentOrganizer}.
+ * <p>
+ * Window manager will dispatch TaskFragment information updates via this interface.
+ * It is necessary because {@link ITaskFragmentOrganizer} aidl interface can not be used as a
+ * {@link TestApi}.
+ * @hide
+ */
+@TestApi
+public final class TaskFragmentOrganizerToken implements Parcelable {
+    private final ITaskFragmentOrganizer mRealToken;
+
+    TaskFragmentOrganizerToken(ITaskFragmentOrganizer realToken) {
+        mRealToken = realToken;
+    }
+
+    /** @hide */
+    public IBinder asBinder() {
+        return mRealToken.asBinder();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeStrongInterface(mRealToken);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<TaskFragmentOrganizerToken> CREATOR =
+            new Creator<TaskFragmentOrganizerToken>() {
+                @Override
+                public TaskFragmentOrganizerToken createFromParcel(Parcel in) {
+                    final ITaskFragmentOrganizer realToken =
+                            ITaskFragmentOrganizer.Stub.asInterface(in.readStrongBinder());
+                    // The TaskFragmentOrganizerToken may be null for TaskOrganizer or
+                    // DisplayAreaOrganizer.
+                    if (realToken == null) {
+                        return null;
+                    }
+                    return new TaskFragmentOrganizerToken(realToken);
+                }
+
+                @Override
+                public TaskFragmentOrganizerToken[] newArray(int size) {
+                    return new TaskFragmentOrganizerToken[size];
+                }
+            };
+
+    @Override
+    public int hashCode() {
+        return mRealToken.asBinder().hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "TaskFragmentOrganizerToken{" + mRealToken + "}";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof TaskFragmentOrganizerToken)) {
+            return false;
+        }
+        return mRealToken.asBinder() == ((TaskFragmentOrganizerToken) obj).asBinder();
+    }
+}
diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java
index d8723a8..27c7d315 100644
--- a/core/java/android/window/TaskOrganizer.java
+++ b/core/java/android/window/TaskOrganizer.java
@@ -24,7 +24,6 @@
 import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.app.ActivityManager;
-import android.graphics.Rect;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.view.SurfaceControl;
@@ -94,6 +93,7 @@
      * @param info The information about the Task that's available
      * @param appToken Token of the application being started.
      *        context to for resources
+     * @hide
      */
     @BinderThread
     public void addStartingWindow(@NonNull StartingWindowInfo info,
@@ -101,14 +101,11 @@
 
     /**
      * Called when the Task want to remove the starting window.
-     * @param leash A persistent leash for the top window in this task. Release it once exit
-     *              animation has finished.
-     * @param frame Window frame of the top window.
-     * @param playRevealAnimation Play vanish animation.
+     * @param removalInfo The information used to remove the starting window.
+     * @hide
      */
     @BinderThread
-    public void removeStartingWindow(int taskId, @Nullable SurfaceControl leash,
-            @Nullable Rect frame, boolean playRevealAnimation) {}
+    public void removeStartingWindow(@NonNull StartingWindowRemovalInfo removalInfo) {}
 
     /**
      * Called when the Task want to copy the splash screen.
@@ -226,6 +223,7 @@
         }
     }
 
+
     /**
      * Restarts the top activity in the given task by killing its process if it is visible.
      * @hide
@@ -256,10 +254,8 @@
         }
 
         @Override
-        public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) {
-            mExecutor.execute(() -> TaskOrganizer.this.removeStartingWindow(taskId, leash, frame,
-                    playRevealAnimation));
+        public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
+            mExecutor.execute(() -> TaskOrganizer.this.removeStartingWindow(removalInfo));
         }
 
         @Override
@@ -298,6 +294,7 @@
         }
     };
 
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
     private ITaskOrganizerController getController() {
         try {
             return getWindowOrganizerController().getTaskOrganizerController();
diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java
index 141f47b..db15145 100644
--- a/core/java/android/window/TransitionFilter.java
+++ b/core/java/android/window/TransitionFilter.java
@@ -17,12 +17,17 @@
 package android.window;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.view.WindowManager.TransitionType;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.app.WindowConfiguration;
+import android.content.ComponentName;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.view.WindowManager;
 
 /**
  * A parcelable filter that can be used for rerouting transitions to a remote. This is a local
@@ -33,11 +38,29 @@
  */
 public final class TransitionFilter implements Parcelable {
 
+    /** The associated requirement doesn't care about the z-order. */
+    public static final int CONTAINER_ORDER_ANY = 0;
+    /** The associated requirement only matches the top-most (z-order) container. */
+    public static final int CONTAINER_ORDER_TOP = 1;
+
+    /** @hide */
+    @IntDef(prefix = { "CONTAINER_ORDER_" }, value = {
+            CONTAINER_ORDER_ANY,
+            CONTAINER_ORDER_TOP,
+    })
+    public @interface ContainerOrder {}
+
     /**
      * When non-null: this is a list of transition types that this filter applies to. This filter
      * will fail for transitions that aren't one of these types.
      */
-    @Nullable public int[] mTypeSet = null;
+    @Nullable public @TransitionType int[] mTypeSet = null;
+
+    /** All flags must be set on a transition. */
+    public @WindowManager.TransitionFlags int mFlags = 0;
+
+    /** All flags must NOT be set on a transition. */
+    public @WindowManager.TransitionFlags int mNotFlags = 0;
 
     /**
      * A list of required changes. To pass, a transition must meet all requirements.
@@ -49,6 +72,8 @@
 
     private TransitionFilter(Parcel in) {
         mTypeSet = in.createIntArray();
+        mFlags = in.readInt();
+        mNotFlags = in.readInt();
         mRequirements = in.createTypedArray(Requirement.CREATOR);
     }
 
@@ -65,10 +90,19 @@
             }
             if (!typePass) return false;
         }
+        if ((info.getFlags() & mFlags) != mFlags) {
+            return false;
+        }
+        if ((info.getFlags() & mNotFlags) != 0) {
+            return false;
+        }
         // Make sure info meets all of the requirements.
         if (mRequirements != null) {
             for (int i = 0; i < mRequirements.length; ++i) {
-                if (!mRequirements[i].matches(info)) return false;
+                final boolean matches = mRequirements[i].matches(info);
+                if (matches == mRequirements[i].mNot) {
+                    return false;
+                }
             }
         }
         return true;
@@ -78,6 +112,8 @@
     /** @hide */
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeIntArray(mTypeSet);
+        dest.writeInt(mFlags);
+        dest.writeInt(mNotFlags);
         dest.writeTypedArray(mRequirements, flags);
     }
 
@@ -107,10 +143,12 @@
         sb.append("{types=[");
         if (mTypeSet != null) {
             for (int i = 0; i < mTypeSet.length; ++i) {
-                sb.append((i == 0 ? "" : ",") + mTypeSet[i]);
+                sb.append((i == 0 ? "" : ",") + WindowManager.transitTypeToString(mTypeSet[i]));
             }
         }
-        sb.append("] checks=[");
+        sb.append("] flags=0x" + Integer.toHexString(mFlags));
+        sb.append("] notFlags=0x" + Integer.toHexString(mNotFlags));
+        sb.append(" checks=[");
         if (mRequirements != null) {
             for (int i = 0; i < mRequirements.length; ++i) {
                 sb.append((i == 0 ? "" : ",") + mRequirements[i]);
@@ -125,30 +163,56 @@
      */
     public static final class Requirement implements Parcelable {
         public int mActivityType = ACTIVITY_TYPE_UNDEFINED;
+
+        /** This only matches if the change is independent of its parents. */
+        public boolean mMustBeIndependent = true;
+
+        /** If this matches, the parent filter will fail */
+        public boolean mNot = false;
+
         public int[] mModes = null;
 
+        /** Matches only if all the flags here are set on the change. */
+        public @TransitionInfo.ChangeFlags int mFlags = 0;
+
+        /** If this needs to be a task. */
+        public boolean mMustBeTask = false;
+
+        public @ContainerOrder int mOrder = CONTAINER_ORDER_ANY;
+        public ComponentName mTopActivity;
+
         public Requirement() {
         }
 
         private Requirement(Parcel in) {
             mActivityType = in.readInt();
+            mMustBeIndependent = in.readBoolean();
+            mNot = in.readBoolean();
             mModes = in.createIntArray();
+            mFlags = in.readInt();
+            mMustBeTask = in.readBoolean();
+            mOrder = in.readInt();
+            mTopActivity = in.readTypedObject(ComponentName.CREATOR);
         }
 
         /** Go through changes and find if at-least one change matches this filter */
         boolean matches(@NonNull TransitionInfo info) {
             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                 final TransitionInfo.Change change = info.getChanges().get(i);
-                if (!TransitionInfo.isIndependent(change, info)) {
+                if (mMustBeIndependent && !TransitionInfo.isIndependent(change, info)) {
                     // Only look at independent animating windows.
                     continue;
                 }
+                if (mOrder == CONTAINER_ORDER_TOP && i > 0) {
+                    continue;
+                }
                 if (mActivityType != ACTIVITY_TYPE_UNDEFINED) {
                     if (change.getTaskInfo() == null
                             || change.getTaskInfo().getActivityType() != mActivityType) {
                         continue;
                     }
                 }
+                if (!matchesTopActivity(change.getTaskInfo())) continue;
                 if (mModes != null) {
                     boolean pass = false;
                     for (int m = 0; m < mModes.length; ++m) {
@@ -159,24 +223,44 @@
                     }
                     if (!pass) continue;
                 }
+                if ((change.getFlags() & mFlags) != mFlags) {
+                    continue;
+                }
+                if (mMustBeTask && change.getTaskInfo() == null) {
+                    continue;
+                }
                 return true;
             }
             return false;
         }
 
+        private boolean matchesTopActivity(ActivityManager.RunningTaskInfo info) {
+            if (mTopActivity == null) return true;
+            if (info == null) return false;
+            final ComponentName component = info.topActivity;
+            return mTopActivity.equals(component);
+        }
+
         /** Check if the request matches this filter. It may generate false positives */
         boolean matches(@NonNull TransitionRequestInfo request) {
-            // Can't check modes since the transition hasn't been built at this point.
+            // Can't check modes/order since the transition hasn't been built at this point.
             if (mActivityType == ACTIVITY_TYPE_UNDEFINED) return true;
             return request.getTriggerTask() != null
-                    && request.getTriggerTask().getActivityType() == mActivityType;
+                    && request.getTriggerTask().getActivityType() == mActivityType
+                    && matchesTopActivity(request.getTriggerTask());
         }
 
         @Override
         /** @hide */
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             dest.writeInt(mActivityType);
+            dest.writeBoolean(mMustBeIndependent);
+            dest.writeBoolean(mNot);
             dest.writeIntArray(mModes);
+            dest.writeInt(mFlags);
+            dest.writeBoolean(mMustBeTask);
+            dest.writeInt(mOrder);
+            dest.writeTypedObject(mTopActivity, flags);
         }
 
         @NonNull
@@ -202,14 +286,31 @@
         @Override
         public String toString() {
             StringBuilder out = new StringBuilder();
-            out.append("{atype=" + WindowConfiguration.activityTypeToString(mActivityType));
+            out.append('{');
+            if (mNot) out.append("NOT ");
+            out.append("atype=" + WindowConfiguration.activityTypeToString(mActivityType));
+            out.append(" independent=" + mMustBeIndependent);
             out.append(" modes=[");
             if (mModes != null) {
                 for (int i = 0; i < mModes.length; ++i) {
                     out.append((i == 0 ? "" : ",") + TransitionInfo.modeToString(mModes[i]));
                 }
             }
-            return out.append("]}").toString();
+            out.append("]").toString();
+            out.append(" flags=" + TransitionInfo.flagsToString(mFlags));
+            out.append(" mustBeTask=" + mMustBeTask);
+            out.append(" order=" + containerOrderToString(mOrder));
+            out.append(" topActivity=").append(mTopActivity);
+            out.append("}");
+            return out.toString();
         }
     }
+
+    private static String containerOrderToString(int order) {
+        switch (order) {
+            case CONTAINER_ORDER_ANY: return "ANY";
+            case CONTAINER_ORDER_TOP: return "TOP";
+        }
+        return "UNKNOWN(" + order + ")";
+    }
 }
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 23b8ee4..7208930 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -16,13 +16,23 @@
 
 package android.window;
 
+import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
+import static android.app.ActivityOptions.ANIM_CUSTOM;
+import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
+import static android.app.ActivityOptions.ANIM_SCALE_UP;
+import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
+import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.WindowManager.TransitionFlags;
+import static android.view.WindowManager.TransitionType;
 import static android.view.WindowManager.transitTypeToString;
 
 import android.annotation.IntDef;
@@ -31,11 +41,11 @@
 import android.app.ActivityManager;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.Surface;
 import android.view.SurfaceControl;
-import android.view.WindowManager;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -80,8 +90,22 @@
     /** The container has voice session. */
     public static final int FLAG_IS_VOICE_INTERACTION = 1 << 4;
 
+    /** The container is the display. */
+    public static final int FLAG_IS_DISPLAY = 1 << 5;
+
+    /** The container can show on top of lock screen. */
+    public static final int FLAG_OCCLUDES_KEYGUARD = 1 << 6;
+
+    /**
+     * Only for IS_DISPLAY containers. Is set if the display has system alert windows. This is
+     * used to prevent seamless rotation.
+     * TODO(b/194540864): Once we can include all windows in transition, then replace this with
+     *         something like FLAG_IS_SYSTEM_ALERT instead. Then we can do mixed rotations.
+     */
+    public static final int FLAG_DISPLAY_HAS_ALERT_WINDOWS = 1 << 7;
+
     /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
-    public static final int FLAG_FIRST_CUSTOM = 1 << 5;
+    public static final int FLAG_FIRST_CUSTOM = 1 << 8;
 
     /** @hide */
     @IntDef(prefix = { "FLAG_" }, value = {
@@ -91,20 +115,24 @@
             FLAG_TRANSLUCENT,
             FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT,
             FLAG_IS_VOICE_INTERACTION,
+            FLAG_IS_DISPLAY,
+            FLAG_OCCLUDES_KEYGUARD,
+            FLAG_DISPLAY_HAS_ALERT_WINDOWS,
             FLAG_FIRST_CUSTOM
     })
     public @interface ChangeFlags {}
 
-    private final @WindowManager.TransitionOldType int mType;
-    private final @WindowManager.TransitionFlags int mFlags;
+    private final @TransitionType int mType;
+    private final @TransitionFlags int mFlags;
     private final ArrayList<Change> mChanges = new ArrayList<>();
 
     private SurfaceControl mRootLeash;
     private final Point mRootOffset = new Point();
 
+    private AnimationOptions mOptions;
+
     /** @hide */
-    public TransitionInfo(@WindowManager.TransitionOldType int type,
-            @WindowManager.TransitionFlags int flags) {
+    public TransitionInfo(@TransitionType int type, @TransitionFlags int flags) {
         mType = type;
         mFlags = flags;
     }
@@ -112,10 +140,11 @@
     private TransitionInfo(Parcel in) {
         mType = in.readInt();
         mFlags = in.readInt();
-        in.readList(mChanges, null /* classLoader */);
+        in.readTypedList(mChanges, Change.CREATOR);
         mRootLeash = new SurfaceControl();
         mRootLeash.readFromParcel(in);
         mRootOffset.readFromParcel(in);
+        mOptions = in.readTypedObject(AnimationOptions.CREATOR);
     }
 
     @Override
@@ -123,9 +152,10 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeInt(mType);
         dest.writeInt(mFlags);
-        dest.writeList(mChanges);
+        dest.writeTypedList(mChanges);
         mRootLeash.writeToParcel(dest, flags);
         mRootOffset.writeToParcel(dest, flags);
+        dest.writeTypedObject(mOptions, flags);
     }
 
     @NonNull
@@ -154,7 +184,11 @@
         mRootOffset.set(offsetLeft, offsetTop);
     }
 
-    public int getType() {
+    public void setAnimationOptions(AnimationOptions options) {
+        mOptions = options;
+    }
+
+    public @TransitionType int getType() {
         return mType;
     }
 
@@ -182,6 +216,14 @@
         return mRootOffset;
     }
 
+    public AnimationOptions getAnimationOptions() {
+        return mOptions;
+    }
+
+    /**
+     * @return the list of {@link Change}s in this transition. The list is sorted top-to-bottom
+     *         in Z (meaning index 0 is the top-most container).
+     */
     @NonNull
     public List<Change> getChanges() {
         return mChanges;
@@ -208,10 +250,17 @@
         mChanges.add(change);
     }
 
+    /**
+     * Whether this transition includes keyguard going away.
+     */
+    public boolean isKeyguardGoingAway() {
+        return (mFlags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
-        sb.append("{t=" + transitTypeToString(mType) + " f=" + Integer.toHexString(mFlags)
+        sb.append("{t=" + transitTypeToString(mType) + " f=0x" + Integer.toHexString(mFlags)
                 + " ro=" + mRootOffset + " c=[");
         for (int i = 0; i < mChanges.size(); ++i) {
             if (i > 0) {
@@ -257,6 +306,15 @@
         if ((flags & FLAG_IS_VOICE_INTERACTION) != 0) {
             sb.append((sb.length() == 0 ? "" : "|") + "IS_VOICE_INTERACTION");
         }
+        if ((flags & FLAG_IS_DISPLAY) != 0) {
+            sb.append((sb.length() == 0 ? "" : "|") + "IS_DISPLAY");
+        }
+        if ((flags & FLAG_OCCLUDES_KEYGUARD) != 0) {
+            sb.append((sb.length() == 0 ? "" : "|") + "OCCLUDES_KEYGUARD");
+        }
+        if ((flags & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) {
+            sb.append((sb.length() == 0 ? "" : "|") + "DISPLAY_HAS_ALERT_WINDOWS");
+        }
         if ((flags & FLAG_FIRST_CUSTOM) != 0) {
             sb.append((sb.length() == 0 ? "" : "|") + "FIRST_CUSTOM");
         }
@@ -302,8 +360,10 @@
         private final Rect mEndAbsBounds = new Rect();
         private final Point mEndRelOffset = new Point();
         private ActivityManager.RunningTaskInfo mTaskInfo = null;
+        private boolean mAllowEnterPip;
         private int mStartRotation = ROTATION_UNDEFINED;
         private int mEndRotation = ROTATION_UNDEFINED;
+        private int mRotationAnimation = ROTATION_ANIMATION_UNSPECIFIED;
 
         public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) {
             mContainer = container;
@@ -321,8 +381,10 @@
             mEndAbsBounds.readFromParcel(in);
             mEndRelOffset.readFromParcel(in);
             mTaskInfo = in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR);
+            mAllowEnterPip = in.readBoolean();
             mStartRotation = in.readInt();
             mEndRotation = in.readInt();
+            mRotationAnimation = in.readInt();
         }
 
         /** Sets the parent of this change's container. The parent must be a participant or null. */
@@ -363,12 +425,25 @@
             mTaskInfo = taskInfo;
         }
 
+        /** Sets the allowEnterPip flag which represents AppOpsManager check on PiP permission */
+        public void setAllowEnterPip(boolean allowEnterPip) {
+            mAllowEnterPip = allowEnterPip;
+        }
+
         /** Sets the start and end rotation of this container. */
         public void setRotation(@Surface.Rotation int start, @Surface.Rotation int end) {
             mStartRotation = start;
             mEndRotation = end;
         }
 
+        /**
+         * Sets the app-requested animation type for rotation. Will be one of the
+         * ROTATION_ANIMATION_ values in {@link android.view.WindowManager.LayoutParams};
+         */
+        public void setRotationAnimation(int anim) {
+            mRotationAnimation = anim;
+        }
+
         /** @return the container that is changing. May be null if non-remotable (eg. activity) */
         @Nullable
         public WindowContainerToken getContainer() {
@@ -432,6 +507,10 @@
             return mTaskInfo;
         }
 
+        public boolean getAllowEnterPip() {
+            return mAllowEnterPip;
+        }
+
         public int getStartRotation() {
             return mStartRotation;
         }
@@ -440,6 +519,11 @@
             return mEndRotation;
         }
 
+        /** @return the rotation animation. */
+        public int getRotationAnimation() {
+            return mRotationAnimation;
+        }
+
         /** @hide */
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -452,8 +536,10 @@
             mEndAbsBounds.writeToParcel(dest, flags);
             mEndRelOffset.writeToParcel(dest, flags);
             dest.writeTypedObject(mTaskInfo, flags);
+            dest.writeBoolean(mAllowEnterPip);
             dest.writeInt(mStartRotation);
             dest.writeInt(mEndRotation);
+            dest.writeInt(mRotationAnimation);
         }
 
         @NonNull
@@ -481,7 +567,149 @@
             return "{" + mContainer + "(" + mParent + ") leash=" + mLeash
                     + " m=" + modeToString(mMode) + " f=" + flagsToString(mFlags) + " sb="
                     + mStartAbsBounds + " eb=" + mEndAbsBounds + " eo=" + mEndRelOffset + " r="
-                    + mStartRotation + "->" + mEndRotation + "}";
+                    + mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation + "}";
+        }
+    }
+
+    /** Represents animation options during a transition */
+    public static final class AnimationOptions implements Parcelable {
+
+        private int mType;
+        private int mEnterResId;
+        private int mExitResId;
+        private boolean mOverrideTaskTransition;
+        private String mPackageName;
+        private final Rect mTransitionBounds = new Rect();
+        private HardwareBuffer mThumbnail;
+
+        private AnimationOptions(int type) {
+            mType = type;
+        }
+
+        public AnimationOptions(Parcel in) {
+            mType = in.readInt();
+            mEnterResId = in.readInt();
+            mExitResId = in.readInt();
+            mOverrideTaskTransition = in.readBoolean();
+            mPackageName = in.readString();
+            mTransitionBounds.readFromParcel(in);
+            mThumbnail = in.readTypedObject(HardwareBuffer.CREATOR);
+        }
+
+        public static AnimationOptions makeCustomAnimOptions(String packageName, int enterResId,
+                int exitResId, boolean overrideTaskTransition) {
+            AnimationOptions options = new AnimationOptions(ANIM_CUSTOM);
+            options.mPackageName = packageName;
+            options.mEnterResId = enterResId;
+            options.mExitResId = exitResId;
+            options.mOverrideTaskTransition = overrideTaskTransition;
+            return options;
+        }
+
+        public static AnimationOptions makeClipRevealAnimOptions(int startX, int startY, int width,
+                int height) {
+            AnimationOptions options = new AnimationOptions(ANIM_CLIP_REVEAL);
+            options.mTransitionBounds.set(startX, startY, startX + width, startY + height);
+            return options;
+        }
+
+        public static AnimationOptions makeScaleUpAnimOptions(int startX, int startY, int width,
+                int height) {
+            AnimationOptions options = new AnimationOptions(ANIM_SCALE_UP);
+            options.mTransitionBounds.set(startX, startY, startX + width, startY + height);
+            return options;
+        }
+
+        public static AnimationOptions makeThumnbnailAnimOptions(HardwareBuffer srcThumb,
+                int startX, int startY, boolean scaleUp) {
+            AnimationOptions options = new AnimationOptions(
+                    scaleUp ? ANIM_THUMBNAIL_SCALE_UP : ANIM_THUMBNAIL_SCALE_DOWN);
+            options.mTransitionBounds.set(startX, startY, startX, startY);
+            options.mThumbnail = srcThumb;
+            return options;
+        }
+
+        public static AnimationOptions makeCrossProfileAnimOptions() {
+            AnimationOptions options = new AnimationOptions(ANIM_OPEN_CROSS_PROFILE_APPS);
+            return options;
+        }
+
+        public int getType() {
+            return mType;
+        }
+
+        public int getEnterResId() {
+            return mEnterResId;
+        }
+
+        public int getExitResId() {
+            return mExitResId;
+        }
+
+        public boolean getOverrideTaskTransition() {
+            return mOverrideTaskTransition;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        public Rect getTransitionBounds() {
+            return mTransitionBounds;
+        }
+
+        public HardwareBuffer getThumbnail() {
+            return mThumbnail;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mType);
+            dest.writeInt(mEnterResId);
+            dest.writeInt(mExitResId);
+            dest.writeBoolean(mOverrideTaskTransition);
+            dest.writeString(mPackageName);
+            mTransitionBounds.writeToParcel(dest, flags);
+            dest.writeTypedObject(mThumbnail, flags);
+        }
+
+        @NonNull
+        public static final Creator<AnimationOptions> CREATOR =
+                new Creator<AnimationOptions>() {
+                    @Override
+                    public AnimationOptions createFromParcel(Parcel in) {
+                        return new AnimationOptions(in);
+                    }
+
+                    @Override
+                    public AnimationOptions[] newArray(int size) {
+                        return new AnimationOptions[size];
+                    }
+                };
+
+        /** @hide */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @NonNull
+        private static String typeToString(int mode) {
+            switch(mode) {
+                case ANIM_CUSTOM: return "ANIM_CUSTOM";
+                case ANIM_CLIP_REVEAL: return "ANIM_CLIP_REVEAL";
+                case ANIM_SCALE_UP: return "ANIM_SCALE_UP";
+                case ANIM_THUMBNAIL_SCALE_UP: return "ANIM_THUMBNAIL_SCALE_UP";
+                case ANIM_THUMBNAIL_SCALE_DOWN: return "ANIM_THUMBNAIL_SCALE_DOWN";
+                case ANIM_OPEN_CROSS_PROFILE_APPS: return "ANIM_OPEN_CROSS_PROFILE_APPS";
+                default: return "<unknown:" + mode + ">";
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "{ AnimationOtions type= " + typeToString(mType) + " package=" + mPackageName
+                    + " override=" + mOverrideTaskTransition + " b=" + mTransitionBounds + "}";
         }
     }
 }
diff --git a/core/java/android/window/TransitionMetrics.java b/core/java/android/window/TransitionMetrics.java
new file mode 100644
index 0000000..9a93c1a
--- /dev/null
+++ b/core/java/android/window/TransitionMetrics.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Singleton;
+
+/**
+ * A helper class for who plays transition animation can report its metrics easily.
+ * @hide
+ */
+public class TransitionMetrics {
+
+    private final ITransitionMetricsReporter mTransitionMetricsReporter;
+
+    private TransitionMetrics(ITransitionMetricsReporter reporter) {
+        mTransitionMetricsReporter = reporter;
+    }
+
+    /** Reports the current timestamp as when the transition animation starts. */
+    public void reportAnimationStart(IBinder transitionToken) {
+        try {
+            mTransitionMetricsReporter.reportAnimationStart(transitionToken,
+                    SystemClock.elapsedRealtime());
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /** Gets the singleton instance of TransitionMetrics. */
+    public static TransitionMetrics getInstance() {
+        return sTransitionMetrics.get();
+    }
+
+    private static final Singleton<TransitionMetrics> sTransitionMetrics = new Singleton<>() {
+        @Override
+        protected TransitionMetrics create() {
+            return new TransitionMetrics(WindowOrganizer.getTransitionMetricsReporter());
+        }
+    };
+}
diff --git a/core/java/android/window/TransitionRequestInfo.java b/core/java/android/window/TransitionRequestInfo.java
index cc493ab..f770731 100644
--- a/core/java/android/window/TransitionRequestInfo.java
+++ b/core/java/android/window/TransitionRequestInfo.java
@@ -40,11 +40,11 @@
     private @Nullable ActivityManager.RunningTaskInfo mTriggerTask;
 
     /** If non-null, a remote-transition associated with the source of this transition. */
-    private @Nullable IRemoteTransition mRemoteTransition;
+    private @Nullable RemoteTransition mRemoteTransition;
 
 
 
-    // Code below generated by codegen v1.0.22.
+    // Code below generated by codegen v1.0.23.
     //
     // DO NOT MODIFY!
     // CHECKSTYLE:OFF Generated code
@@ -72,7 +72,7 @@
     public TransitionRequestInfo(
             @WindowManager.TransitionType int type,
             @Nullable ActivityManager.RunningTaskInfo triggerTask,
-            @Nullable IRemoteTransition remoteTransition) {
+            @Nullable RemoteTransition remoteTransition) {
         this.mType = type;
         com.android.internal.util.AnnotationValidations.validate(
                 WindowManager.TransitionType.class, null, mType);
@@ -103,7 +103,7 @@
      * If non-null, a remote-transition associated with the source of this transition.
      */
     @DataClass.Generated.Member
-    public @Nullable IRemoteTransition getRemoteTransition() {
+    public @Nullable RemoteTransition getRemoteTransition() {
         return mRemoteTransition;
     }
 
@@ -121,7 +121,7 @@
      * If non-null, a remote-transition associated with the source of this transition.
      */
     @DataClass.Generated.Member
-    public @android.annotation.NonNull TransitionRequestInfo setRemoteTransition(@android.annotation.NonNull IRemoteTransition value) {
+    public @android.annotation.NonNull TransitionRequestInfo setRemoteTransition(@android.annotation.NonNull RemoteTransition value) {
         mRemoteTransition = value;
         return this;
     }
@@ -151,7 +151,7 @@
         dest.writeByte(flg);
         dest.writeInt(mType);
         if (mTriggerTask != null) dest.writeTypedObject(mTriggerTask, flags);
-        if (mRemoteTransition != null) dest.writeStrongInterface(mRemoteTransition);
+        if (mRemoteTransition != null) dest.writeTypedObject(mRemoteTransition, flags);
     }
 
     @Override
@@ -168,7 +168,7 @@
         byte flg = in.readByte();
         int type = in.readInt();
         ActivityManager.RunningTaskInfo triggerTask = (flg & 0x2) == 0 ? null : (ActivityManager.RunningTaskInfo) in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR);
-        IRemoteTransition remoteTransition = (flg & 0x4) == 0 ? null : IRemoteTransition.Stub.asInterface(in.readStrongBinder());
+        RemoteTransition remoteTransition = (flg & 0x4) == 0 ? null : (RemoteTransition) in.readTypedObject(RemoteTransition.CREATOR);
 
         this.mType = type;
         com.android.internal.util.AnnotationValidations.validate(
@@ -194,10 +194,10 @@
     };
 
     @DataClass.Generated(
-            time = 1610060387917L,
-            codegenVersion = "1.0.22",
+            time = 1629321632222L,
+            codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/window/TransitionRequestInfo.java",
-            inputSignatures = "private final @android.view.WindowManager.TransitionType int mType\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mTriggerTask\nprivate @android.annotation.Nullable android.window.IRemoteTransition mRemoteTransition\nclass TransitionRequestInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)")
+            inputSignatures = "private final @android.view.WindowManager.TransitionType int mType\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mTriggerTask\nprivate @android.annotation.Nullable android.window.RemoteTransition mRemoteTransition\nclass TransitionRequestInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index c0af572..bbf8138 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -19,7 +19,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
+import android.app.PendingIntent;
 import android.app.WindowConfiguration;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -34,6 +36,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Represents a collection of operations on some WindowContainers that should be applied all at
@@ -48,11 +51,19 @@
     // Flat list because re-order operations are order-dependent
     private final ArrayList<HierarchyOp> mHierarchyOps = new ArrayList<>();
 
+    @Nullable
+    private IBinder mErrorCallbackToken;
+
+    @Nullable
+    private ITaskFragmentOrganizer mTaskFragmentOrganizer;
+
     public WindowContainerTransaction() {}
 
     private WindowContainerTransaction(Parcel in) {
         in.readMap(mChanges, null /* loader */);
         in.readList(mHierarchyOps, null /* loader */);
+        mErrorCallbackToken = in.readStrongBinder();
+        mTaskFragmentOrganizer = ITaskFragmentOrganizer.Stub.asInterface(in.readStrongBinder());
     }
 
     private Change getOrCreateChange(IBinder token) {
@@ -284,6 +295,36 @@
     }
 
     /**
+     * Reparent's all children tasks or the top task of {@param currentParent} in the specified
+     * {@param windowingMode} and {@param activityType} to {@param newParent} in their current
+     * z-order.
+     *
+     * @param currentParent of the tasks to perform the operation no.
+     *                      {@code null} will perform the operation on the display.
+     * @param newParent for the tasks. {@code null} will perform the operation on the display.
+     * @param windowingModes of the tasks to reparent.
+     * @param activityTypes of the tasks to reparent.
+     * @param onTop When {@code true}, the child goes to the top of parent; otherwise it goes to
+     *              the bottom.
+     * @param reparentTopOnly When {@code true}, only reparent the top task which fit windowingModes
+     *                        and activityTypes.
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction reparentTasks(@Nullable WindowContainerToken currentParent,
+            @Nullable WindowContainerToken newParent, @Nullable int[] windowingModes,
+            @Nullable int[] activityTypes, boolean onTop, boolean reparentTopOnly) {
+        mHierarchyOps.add(HierarchyOp.createForChildrenTasksReparent(
+                currentParent != null ? currentParent.asBinder() : null,
+                newParent != null ? newParent.asBinder() : null,
+                windowingModes,
+                activityTypes,
+                onTop,
+                reparentTopOnly));
+        return this;
+    }
+
+    /**
      * Reparent's all children tasks of {@param currentParent} in the specified
      * {@param windowingMode} and {@param activityType} to {@param newParent} in their current
      * z-order.
@@ -300,13 +341,8 @@
     public WindowContainerTransaction reparentTasks(@Nullable WindowContainerToken currentParent,
             @Nullable WindowContainerToken newParent, @Nullable int[] windowingModes,
             @Nullable int[] activityTypes, boolean onTop) {
-        mHierarchyOps.add(HierarchyOp.createForChildrenTasksReparent(
-                currentParent != null ? currentParent.asBinder() : null,
-                newParent != null ? newParent.asBinder() : null,
-                windowingModes,
-                activityTypes,
-                onTop));
-        return this;
+        return reparentTasks(currentParent, newParent, windowingModes, activityTypes, onTop,
+                false /* reparentTopOnly */);
     }
 
     /**
@@ -325,7 +361,8 @@
 
     /**
      * Sets to containers adjacent to each other. Containers below two visible adjacent roots will
-     * be made invisible. This currently only applies to Task containers created by organizer.
+     * be made invisible. This currently only applies to TaskFragment containers created by
+     * organizer.
      * @param root1 the first root.
      * @param root2 the second root.
      */
@@ -378,6 +415,177 @@
     }
 
     /**
+     * Sends a pending intent in sync.
+     * @param sender The PendingIntent sender.
+     * @param intent The fillIn intent to patch over the sender's base intent.
+     * @param options bundle containing ActivityOptions for the task's top activity.
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction sendPendingIntent(PendingIntent sender, Intent intent,
+            @Nullable Bundle options) {
+        mHierarchyOps.add(new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT)
+                .setLaunchOptions(options)
+                .setPendingIntent(sender)
+                .setActivityIntent(intent)
+                .build());
+        return this;
+    }
+
+    /**
+     * Creates a new TaskFragment with the given options.
+     * @param taskFragmentOptions the options used to create the TaskFragment.
+     */
+    @NonNull
+    public WindowContainerTransaction createTaskFragment(
+            @NonNull TaskFragmentCreationParams taskFragmentOptions) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT)
+                        .setTaskFragmentCreationOptions(taskFragmentOptions)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
+     * Deletes an existing TaskFragment. Any remaining activities below it will be destroyed.
+     * @param taskFragment  the TaskFragment to be removed.
+     */
+    @NonNull
+    public WindowContainerTransaction deleteTaskFragment(
+            @NonNull WindowContainerToken taskFragment) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT)
+                        .setContainer(taskFragment.asBinder())
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
+     * Starts an activity in the TaskFragment.
+     * @param fragmentToken client assigned unique token to create TaskFragment with specified in
+     *                      {@link TaskFragmentCreationParams#getFragmentToken()}.
+     * @param callerToken  the activity token that initialized the activity launch.
+     * @param activityIntent    intent to start the activity.
+     * @param activityOptions    ActivityOptions to start the activity with.
+     * @see android.content.Context#startActivity(Intent, Bundle).
+     */
+    @NonNull
+    public WindowContainerTransaction startActivityInTaskFragment(
+            @NonNull IBinder fragmentToken, @NonNull IBinder callerToken,
+            @NonNull Intent activityIntent, @Nullable Bundle activityOptions) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(
+                        HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT)
+                        .setContainer(fragmentToken)
+                        .setReparentContainer(callerToken)
+                        .setActivityIntent(activityIntent)
+                        .setLaunchOptions(activityOptions)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
+     * Moves an activity into the TaskFragment.
+     * @param fragmentToken client assigned unique token to create TaskFragment with specified in
+     *                      {@link TaskFragmentCreationParams#getFragmentToken()}.
+     * @param activityToken activity to be reparented.
+     */
+    @NonNull
+    public WindowContainerTransaction reparentActivityToTaskFragment(
+            @NonNull IBinder fragmentToken, @NonNull IBinder activityToken) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(
+                        HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT)
+                        .setReparentContainer(fragmentToken)
+                        .setContainer(activityToken)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
+     * Reparents all children of one TaskFragment to another.
+     * @param oldParent children of this TaskFragment will be reparented.
+     * @param newParent the new parent TaskFragment to move the children to. If {@code null}, the
+     *                  children will be moved to the leaf Task.
+     */
+    @NonNull
+    public WindowContainerTransaction reparentChildren(
+            @NonNull WindowContainerToken oldParent,
+            @Nullable WindowContainerToken newParent) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN)
+                        .setContainer(oldParent.asBinder())
+                        .setReparentContainer(newParent != null ? newParent.asBinder() : null)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
+     * Sets to TaskFragments adjacent to each other. Containers below two visible adjacent
+     * TaskFragments will be made invisible. This is similar to
+     * {@link #setAdjacentRoots(WindowContainerToken, WindowContainerToken)}, but can be used with
+     * fragmentTokens when that TaskFragments haven't been created (but will be created in the same
+     * {@link WindowContainerTransaction}).
+     * To reset it, pass {@code null} for {@code fragmentToken2}.
+     * @param fragmentToken1    client assigned unique token to create TaskFragment with specified
+     *                          in {@link TaskFragmentCreationParams#getFragmentToken()}.
+     * @param fragmentToken2    client assigned unique token to create TaskFragment with specified
+     *                          in {@link TaskFragmentCreationParams#getFragmentToken()}. If it is
+     *                          {@code null}, the transaction will reset the adjacent TaskFragment.
+     */
+    @NonNull
+    public WindowContainerTransaction setAdjacentTaskFragments(
+            @NonNull IBinder fragmentToken1, @Nullable IBinder fragmentToken2,
+            @Nullable TaskFragmentAdjacentParams params) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS)
+                        .setContainer(fragmentToken1)
+                        .setReparentContainer(fragmentToken2)
+                        .setLaunchOptions(params != null ? params.toBundle() : null)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
+     * When this {@link WindowContainerTransaction} failed to finish on the server side, it will
+     * trigger callback with this {@param errorCallbackToken}.
+     * @param errorCallbackToken    client provided token that will be passed back as parameter in
+     *                              the callback if there is an error on the server side.
+     * @see ITaskFragmentOrganizer#onTaskFragmentError
+     */
+    @NonNull
+    public WindowContainerTransaction setErrorCallbackToken(@NonNull IBinder errorCallbackToken) {
+        if (mErrorCallbackToken != null) {
+            throw new IllegalStateException("Can't set multiple error token for one transaction.");
+        }
+        mErrorCallbackToken = errorCallbackToken;
+        return this;
+    }
+
+    /**
+     * Sets the {@link TaskFragmentOrganizer} that applies this {@link WindowContainerTransaction}.
+     * When this is set, the server side will not check for the permission of
+     * {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS}, but will ensure this WCT only
+     * contains operations that are allowed for this organizer, such as modifying TaskFragments that
+     * are organized by this organizer.
+     * @hide
+     */
+    @NonNull
+    WindowContainerTransaction setTaskFragmentOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
+        if (mTaskFragmentOrganizer != null) {
+            throw new IllegalStateException("Can't set multiple organizers for one transaction.");
+        }
+        mTaskFragmentOrganizer = organizer;
+        return this;
+    }
+
+    /**
      * Merges another WCT into this one.
      * @param transfer When true, this will transfer everything from other potentially leaving
      *                 other in an unusable state. When false, other is left alone, but
@@ -398,6 +606,23 @@
             mHierarchyOps.add(transfer ? other.mHierarchyOps.get(i)
                     : new HierarchyOp(other.mHierarchyOps.get(i)));
         }
+        if (mErrorCallbackToken != null && other.mErrorCallbackToken != null && mErrorCallbackToken
+                != other.mErrorCallbackToken) {
+            throw new IllegalArgumentException("Can't merge two WCTs with different error token");
+        }
+        final IBinder taskFragmentOrganizerAsBinder = mTaskFragmentOrganizer != null
+                ? mTaskFragmentOrganizer.asBinder()
+                : null;
+        final IBinder otherTaskFragmentOrganizerAsBinder = other.mTaskFragmentOrganizer != null
+                ? other.mTaskFragmentOrganizer.asBinder()
+                : null;
+        if (!Objects.equals(taskFragmentOrganizerAsBinder, otherTaskFragmentOrganizerAsBinder)) {
+            throw new IllegalArgumentException(
+                    "Can't merge two WCTs from different TaskFragmentOrganizers");
+        }
+        mErrorCallbackToken = mErrorCallbackToken != null
+                ? mErrorCallbackToken
+                : other.mErrorCallbackToken;
     }
 
     /** @hide */
@@ -415,10 +640,26 @@
         return mHierarchyOps;
     }
 
+    /** @hide */
+    @Nullable
+    public IBinder getErrorCallbackToken() {
+        return mErrorCallbackToken;
+    }
+
+    /** @hide */
+    @Nullable
+    public ITaskFragmentOrganizer getTaskFragmentOrganizer() {
+        return mTaskFragmentOrganizer;
+    }
+
     @Override
     @NonNull
     public String toString() {
-        return "WindowContainerTransaction { changes = " + mChanges + " hops = " + mHierarchyOps
+        return "WindowContainerTransaction {"
+                + " changes = " + mChanges
+                + " hops = " + mHierarchyOps
+                + " errorCallbackToken=" + mErrorCallbackToken
+                + " taskFragmentOrganizer=" + mTaskFragmentOrganizer
                 + " }";
     }
 
@@ -427,6 +668,8 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeMap(mChanges);
         dest.writeList(mHierarchyOps);
+        dest.writeStrongBinder(mErrorCallbackToken);
+        dest.writeStrongInterface(mTaskFragmentOrganizer);
     }
 
     @Override
@@ -705,6 +948,13 @@
         public static final int HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS = 4;
         public static final int HIERARCHY_OP_TYPE_LAUNCH_TASK = 5;
         public static final int HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT = 6;
+        public static final int HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT = 7;
+        public static final int HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT = 8;
+        public static final int HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT = 9;
+        public static final int HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT = 10;
+        public static final int HIERARCHY_OP_TYPE_REPARENT_CHILDREN = 11;
+        public static final int HIERARCHY_OP_TYPE_PENDING_INTENT = 12;
+        public static final int HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS = 13;
 
         // The following key(s) are for use with mLaunchOptions:
         // When launching a task (eg. from recents), this is the taskId to be launched.
@@ -713,75 +963,105 @@
         private final int mType;
 
         // Container we are performing the operation on.
-        private final IBinder mContainer;
+        @Nullable
+        private IBinder mContainer;
 
         // If this is same as mContainer, then only change position, don't reparent.
-        private final IBinder mReparent;
+        @Nullable
+        private IBinder mReparent;
 
         // Moves/reparents to top of parent when {@code true}, otherwise moves/reparents to bottom.
-        private final boolean mToTop;
+        private boolean mToTop;
 
-        final private int[]  mWindowingModes;
-        final private int[] mActivityTypes;
+        private boolean mReparentTopOnly;
 
-        private final Bundle mLaunchOptions;
+        @Nullable
+        private int[]  mWindowingModes;
+
+        @Nullable
+        private int[] mActivityTypes;
+
+        @Nullable
+        private Bundle mLaunchOptions;
+
+        @Nullable
+        private Intent mActivityIntent;
+
+        // Used as options for WindowContainerTransaction#createTaskFragment().
+        @Nullable
+        private TaskFragmentCreationParams mTaskFragmentCreationOptions;
+
+        @Nullable
+        private PendingIntent mPendingIntent;
 
         public static HierarchyOp createForReparent(
                 @NonNull IBinder container, @Nullable IBinder reparent, boolean toTop) {
-            return new HierarchyOp(HIERARCHY_OP_TYPE_REPARENT,
-                    container, reparent, null, null, toTop, null);
+            return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_REPARENT)
+                    .setContainer(container)
+                    .setReparentContainer(reparent)
+                    .setToTop(toTop)
+                    .build();
         }
 
         public static HierarchyOp createForReorder(@NonNull IBinder container, boolean toTop) {
-            return new HierarchyOp(HIERARCHY_OP_TYPE_REORDER,
-                    container, container, null, null, toTop, null);
+            return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_REORDER)
+                    .setContainer(container)
+                    .setReparentContainer(container)
+                    .setToTop(toTop)
+                    .build();
         }
 
         public static HierarchyOp createForChildrenTasksReparent(IBinder currentParent,
-                IBinder newParent, int[] windowingModes, int[] activityTypes, boolean onTop) {
-            return new HierarchyOp(HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT,
-                    currentParent, newParent, windowingModes, activityTypes, onTop, null);
+                IBinder newParent, int[] windowingModes, int[] activityTypes, boolean onTop,
+                boolean reparentTopOnly) {
+            return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT)
+                    .setContainer(currentParent)
+                    .setReparentContainer(newParent)
+                    .setWindowingModes(windowingModes)
+                    .setActivityTypes(activityTypes)
+                    .setToTop(onTop)
+                    .setReparentTopOnly(reparentTopOnly)
+                    .build();
         }
 
         public static HierarchyOp createForSetLaunchRoot(IBinder container,
                 int[] windowingModes, int[] activityTypes) {
-            return new HierarchyOp(HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT,
-                    container, null, windowingModes, activityTypes, false, null);
+            return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT)
+                    .setContainer(container)
+                    .setWindowingModes(windowingModes)
+                    .setActivityTypes(activityTypes)
+                    .build();
         }
 
         public static HierarchyOp createForAdjacentRoots(IBinder root1, IBinder root2) {
-            return new HierarchyOp(HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS,
-                    root1, root2, null, null, false, null);
+            return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS)
+                    .setContainer(root1)
+                    .setReparentContainer(root2)
+                    .build();
         }
 
         /** Create a hierarchy op for launching a task. */
         public static HierarchyOp createForTaskLaunch(int taskId, @Nullable Bundle options) {
             final Bundle fullOptions = options == null ? new Bundle() : options;
             fullOptions.putInt(LAUNCH_KEY_TASK_ID, taskId);
-            return new HierarchyOp(HIERARCHY_OP_TYPE_LAUNCH_TASK, null, null, null, null, true,
-                    fullOptions);
+            return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_LAUNCH_TASK)
+                    .setToTop(true)
+                    .setLaunchOptions(fullOptions)
+                    .build();
         }
 
         /** Create a hierarchy op for setting launch adjacent flag root. */
         public static HierarchyOp createForSetLaunchAdjacentFlagRoot(IBinder container,
                 boolean clearRoot) {
-            return new HierarchyOp(HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT, container, null,
-                    null, null, clearRoot, null);
+            return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT)
+                    .setContainer(container)
+                    .setToTop(clearRoot)
+                    .build();
         }
 
-
-        private HierarchyOp(int type, @Nullable IBinder container, @Nullable IBinder reparent,
-                int[] windowingModes, int[] activityTypes, boolean toTop,
-                @Nullable Bundle launchOptions) {
+        /** Only creates through {@link Builder}. */
+        private HierarchyOp(int type) {
             mType = type;
-            mContainer = container;
-            mReparent = reparent;
-            mWindowingModes = windowingModes != null ?
-                    Arrays.copyOf(windowingModes, windowingModes.length) : null;
-            mActivityTypes = activityTypes != null ?
-                    Arrays.copyOf(activityTypes, activityTypes.length) : null;
-            mToTop = toTop;
-            mLaunchOptions = launchOptions;
         }
 
         public HierarchyOp(@NonNull HierarchyOp copy) {
@@ -789,9 +1069,13 @@
             mContainer = copy.mContainer;
             mReparent = copy.mReparent;
             mToTop = copy.mToTop;
+            mReparentTopOnly = copy.mReparentTopOnly;
             mWindowingModes = copy.mWindowingModes;
             mActivityTypes = copy.mActivityTypes;
             mLaunchOptions = copy.mLaunchOptions;
+            mActivityIntent = copy.mActivityIntent;
+            mTaskFragmentCreationOptions = copy.mTaskFragmentCreationOptions;
+            mPendingIntent = copy.mPendingIntent;
         }
 
         protected HierarchyOp(Parcel in) {
@@ -799,9 +1083,13 @@
             mContainer = in.readStrongBinder();
             mReparent = in.readStrongBinder();
             mToTop = in.readBoolean();
+            mReparentTopOnly = in.readBoolean();
             mWindowingModes = in.createIntArray();
             mActivityTypes = in.createIntArray();
             mLaunchOptions = in.readBundle();
+            mActivityIntent = in.readTypedObject(Intent.CREATOR);
+            mTaskFragmentCreationOptions = in.readTypedObject(TaskFragmentCreationParams.CREATOR);
+            mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
         }
 
         public int getType() {
@@ -827,10 +1115,19 @@
             return mReparent;
         }
 
+        @NonNull
+        public IBinder getCallingActivity() {
+            return mReparent;
+        }
+
         public boolean getToTop() {
             return mToTop;
         }
 
+        public boolean getReparentTopOnly() {
+            return mReparentTopOnly;
+        }
+
         public int[] getWindowingModes() {
             return mWindowingModes;
         }
@@ -844,17 +1141,33 @@
             return mLaunchOptions;
         }
 
+        @Nullable
+        public Intent getActivityIntent() {
+            return mActivityIntent;
+        }
+
+        @Nullable
+        public TaskFragmentCreationParams getTaskFragmentCreationOptions() {
+            return mTaskFragmentCreationOptions;
+        }
+
+        @Nullable
+        public PendingIntent getPendingIntent() {
+            return mPendingIntent;
+        }
+
         @Override
         public String toString() {
             switch (mType) {
                 case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT:
                     return "{ChildrenTasksReparent: from=" + mContainer + " to=" + mReparent
-                            + " mToTop=" + mToTop + " mWindowingMode=" + mWindowingModes
-                            + " mActivityType=" + mActivityTypes + "}";
+                            + " mToTop=" + mToTop + " mReparentTopOnly=" + mReparentTopOnly
+                            + " mWindowingMode=" + Arrays.toString(mWindowingModes)
+                            + " mActivityType=" + Arrays.toString(mActivityTypes) + "}";
                 case HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT:
                     return "{SetLaunchRoot: container=" + mContainer
-                            + " mWindowingMode=" + mWindowingModes
-                            + " mActivityType=" + mActivityTypes + "}";
+                            + " mWindowingMode=" + Arrays.toString(mWindowingModes)
+                            + " mActivityType=" + Arrays.toString(mActivityTypes) + "}";
                 case HIERARCHY_OP_TYPE_REPARENT:
                     return "{reparent: " + mContainer + " to " + (mToTop ? "top of " : "bottom of ")
                             + mReparent + "}";
@@ -868,10 +1181,27 @@
                 case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT:
                     return "{SetAdjacentFlagRoot: container=" + mContainer + " clearRoot=" + mToTop
                             + "}";
+                case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT:
+                    return "{CreateTaskFragment: options=" + mTaskFragmentCreationOptions + "}";
+                case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT:
+                    return "{DeleteTaskFragment: taskFragment=" + mContainer + "}";
+                case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
+                    return "{StartActivityInTaskFragment: fragmentToken=" + mContainer + " intent="
+                            + mActivityIntent + " options=" + mLaunchOptions + "}";
+                case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT:
+                    return "{ReparentActivityToTaskFragment: fragmentToken=" + mReparent
+                            + " activity=" + mContainer + "}";
+                case HIERARCHY_OP_TYPE_REPARENT_CHILDREN:
+                    return "{ReparentChildren: oldParent=" + mContainer + " newParent=" + mReparent
+                            + "}";
+                case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS:
+                    return "{SetAdjacentTaskFragments: container=" + mContainer
+                            + " adjacentContainer=" + mReparent + "}";
                 default:
                     return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent
-                            + " mToTop=" + mToTop + " mWindowingMode=" + mWindowingModes
-                            + " mActivityType=" + mActivityTypes + "}";
+                            + " mToTop=" + mToTop
+                            + " mWindowingMode=" + Arrays.toString(mWindowingModes)
+                            + " mActivityType=" + Arrays.toString(mActivityTypes) + "}";
             }
         }
 
@@ -881,9 +1211,13 @@
             dest.writeStrongBinder(mContainer);
             dest.writeStrongBinder(mReparent);
             dest.writeBoolean(mToTop);
+            dest.writeBoolean(mReparentTopOnly);
             dest.writeIntArray(mWindowingModes);
             dest.writeIntArray(mActivityTypes);
             dest.writeBundle(mLaunchOptions);
+            dest.writeTypedObject(mActivityIntent, flags);
+            dest.writeTypedObject(mTaskFragmentCreationOptions, flags);
+            dest.writeTypedObject(mPendingIntent, flags);
         }
 
         @Override
@@ -902,5 +1236,174 @@
                 return new HierarchyOp[size];
             }
         };
+
+        private static class Builder {
+
+            private final int mType;
+
+            @Nullable
+            private IBinder mContainer;
+
+            @Nullable
+            private IBinder mReparent;
+
+            private boolean mToTop;
+
+            private boolean mReparentTopOnly;
+
+            @Nullable
+            private int[]  mWindowingModes;
+
+            @Nullable
+            private int[] mActivityTypes;
+
+            @Nullable
+            private Bundle mLaunchOptions;
+
+            @Nullable
+            private Intent mActivityIntent;
+
+            @Nullable
+            private TaskFragmentCreationParams mTaskFragmentCreationOptions;
+
+            @Nullable
+            private PendingIntent mPendingIntent;
+
+            Builder(int type) {
+                mType = type;
+            }
+
+            Builder setContainer(@Nullable IBinder container) {
+                mContainer = container;
+                return this;
+            }
+
+            Builder setReparentContainer(@Nullable IBinder reparentContainer) {
+                mReparent = reparentContainer;
+                return this;
+            }
+
+            Builder setToTop(boolean toTop) {
+                mToTop = toTop;
+                return this;
+            }
+
+            Builder setReparentTopOnly(boolean reparentTopOnly) {
+                mReparentTopOnly = reparentTopOnly;
+                return this;
+            }
+
+            Builder setWindowingModes(@Nullable int[] windowingModes) {
+                mWindowingModes = windowingModes;
+                return this;
+            }
+
+            Builder setActivityTypes(@Nullable int[] activityTypes) {
+                mActivityTypes = activityTypes;
+                return this;
+            }
+
+            Builder setLaunchOptions(@Nullable Bundle launchOptions) {
+                mLaunchOptions = launchOptions;
+                return this;
+            }
+
+            Builder setActivityIntent(@Nullable Intent activityIntent) {
+                mActivityIntent = activityIntent;
+                return this;
+            }
+
+            Builder setPendingIntent(@Nullable PendingIntent sender) {
+                mPendingIntent = sender;
+                return this;
+            }
+
+            Builder setTaskFragmentCreationOptions(
+                    @Nullable TaskFragmentCreationParams taskFragmentCreationOptions) {
+                mTaskFragmentCreationOptions = taskFragmentCreationOptions;
+                return this;
+            }
+
+            HierarchyOp build() {
+                final HierarchyOp hierarchyOp = new HierarchyOp(mType);
+                hierarchyOp.mContainer = mContainer;
+                hierarchyOp.mReparent = mReparent;
+                hierarchyOp.mWindowingModes = mWindowingModes != null
+                        ? Arrays.copyOf(mWindowingModes, mWindowingModes.length)
+                        : null;
+                hierarchyOp.mActivityTypes = mActivityTypes != null
+                        ? Arrays.copyOf(mActivityTypes, mActivityTypes.length)
+                        : null;
+                hierarchyOp.mToTop = mToTop;
+                hierarchyOp.mReparentTopOnly = mReparentTopOnly;
+                hierarchyOp.mLaunchOptions = mLaunchOptions;
+                hierarchyOp.mActivityIntent = mActivityIntent;
+                hierarchyOp.mPendingIntent = mPendingIntent;
+                hierarchyOp.mTaskFragmentCreationOptions = mTaskFragmentCreationOptions;
+
+                return hierarchyOp;
+            }
+        }
+    }
+
+    /**
+     * Helper class for building an options Bundle that can be used to set adjacent rules of
+     * TaskFragments.
+     */
+    public static class TaskFragmentAdjacentParams {
+        private static final String DELAY_PRIMARY_LAST_ACTIVITY_REMOVAL =
+                "android:transaction.adjacent.option.delay_primary_removal";
+        private static final String DELAY_SECONDARY_LAST_ACTIVITY_REMOVAL =
+                "android:transaction.adjacent.option.delay_secondary_removal";
+
+        private boolean mDelayPrimaryLastActivityRemoval;
+        private boolean mDelaySecondaryLastActivityRemoval;
+
+        public TaskFragmentAdjacentParams() {
+        }
+
+        public TaskFragmentAdjacentParams(@NonNull Bundle bundle) {
+            mDelayPrimaryLastActivityRemoval = bundle.getBoolean(
+                    DELAY_PRIMARY_LAST_ACTIVITY_REMOVAL);
+            mDelaySecondaryLastActivityRemoval = bundle.getBoolean(
+                    DELAY_SECONDARY_LAST_ACTIVITY_REMOVAL);
+        }
+
+        /** @see #shouldDelayPrimaryLastActivityRemoval() */
+        public void setShouldDelayPrimaryLastActivityRemoval(boolean delay) {
+            mDelayPrimaryLastActivityRemoval = delay;
+        }
+
+        /** @see #shouldDelaySecondaryLastActivityRemoval() */
+        public void setShouldDelaySecondaryLastActivityRemoval(boolean delay) {
+            mDelaySecondaryLastActivityRemoval = delay;
+        }
+
+        /**
+         * Whether to delay the last activity of the primary adjacent TaskFragment being immediately
+         * removed while finishing.
+         * <p>
+         * It is usually set to {@code true} to give organizer an opportunity to perform other
+         * actions or animations. An example is to finish together with the adjacent TaskFragment.
+         * </p>
+         */
+        public boolean shouldDelayPrimaryLastActivityRemoval() {
+            return mDelayPrimaryLastActivityRemoval;
+        }
+
+        /**
+         * Similar to {@link #shouldDelayPrimaryLastActivityRemoval()}, but for the secondary
+         * TaskFragment.
+         */
+        public boolean shouldDelaySecondaryLastActivityRemoval() {
+            return mDelaySecondaryLastActivityRemoval;
+        }
+
+        Bundle toBundle() {
+            final Bundle b = new Bundle();
+            b.putBoolean(DELAY_PRIMARY_LAST_ACTIVITY_REMOVAL, mDelayPrimaryLastActivityRemoval);
+            b.putBoolean(DELAY_SECONDARY_LAST_ACTIVITY_REMOVAL, mDelaySecondaryLastActivityRemoval);
+            return b;
+        }
     }
 }
diff --git a/core/java/android/window/WindowContext.java b/core/java/android/window/WindowContext.java
index 6d0a6bd..cfccb71 100644
--- a/core/java/android/window/WindowContext.java
+++ b/core/java/android/window/WindowContext.java
@@ -26,6 +26,7 @@
 import android.content.ContextWrapper;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.view.Display;
 import android.view.WindowManager;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -42,23 +43,33 @@
  * @hide
  */
 @UiContext
-public class WindowContext extends ContextWrapper {
+public class WindowContext extends ContextWrapper implements WindowProvider {
     private final WindowManager mWindowManager;
-    private final @WindowManager.LayoutParams.WindowType int mType;
-    private final @Nullable Bundle mOptions;
+    @WindowManager.LayoutParams.WindowType
+    private final int mType;
+    @Nullable
+    private final Bundle mOptions;
     private final ComponentCallbacksController mCallbacksController =
             new ComponentCallbacksController();
     private final WindowContextController mController;
 
     /**
-     * Default constructor. Will generate a {@link WindowTokenClient} and attach this context to
-     * the token.
+     * Default implementation of {@link WindowContext}
+     * <p>
+     * Note that the users should call {@link Context#createWindowContext(Display, int, Bundle)}
+     * to create a {@link WindowContext} instead of using this constructor
+     * </p><p>
+     * Example usage:
+     * <pre class="prettyprint">
+     * Bundle options = new Bundle();
+     * options.put(KEY_ROOT_DISPLAY_AREA_ID, displayAreaInfo.rootDisplayAreaId);
+     * Context windowContext = context.createWindowContext(display, windowType, options);
+     * </pre></p>
      *
-     * @param base Base {@link Context} for this new instance.
-     * @param type Window type to be used with this context.
+     * @param base    Base {@link Context} for this new instance.
+     * @param type    Window type to be used with this context.
      * @param options A bundle used to pass window-related options.
-     *
-     * @hide
+     * @see DisplayAreaInfo#rootDisplayAreaId
      */
     public WindowContext(@NonNull Context base, int type, @Nullable Bundle options) {
         super(base);
@@ -104,10 +115,13 @@
 
     @Override
     public void destroy() {
-        mCallbacksController.clearCallbacks();
-        // Called to the base ContextImpl to do final clean-up.
-        getBaseContext().destroy();
-        Reference.reachabilityFence(this);
+        try {
+            mCallbacksController.clearCallbacks();
+            // Called to the base ContextImpl to do final clean-up.
+            getBaseContext().destroy();
+        } finally {
+            Reference.reachabilityFence(this);
+        }
     }
 
     @Override
@@ -124,4 +138,15 @@
     void dispatchConfigurationChanged(@NonNull Configuration newConfig) {
         mCallbacksController.dispatchConfigurationChanged(newConfig);
     }
+
+    @Override
+    public int getWindowType() {
+        return mType;
+    }
+
+    @Nullable
+    @Override
+    public Bundle getWindowContextOptions() {
+        return mOptions;
+    }
 }
diff --git a/core/java/android/window/WindowContextController.java b/core/java/android/window/WindowContextController.java
index 505b450..5aa6233 100644
--- a/core/java/android/window/WindowContextController.java
+++ b/core/java/android/window/WindowContextController.java
@@ -87,7 +87,8 @@
                 mAttachedToDisplayArea = true;
                 // Send the DisplayArea's configuration to WindowContext directly instead of
                 // waiting for dispatching from WMS.
-                mToken.onConfigurationChanged(configuration, displayId);
+                mToken.onConfigurationChanged(configuration, displayId,
+                        false /* shouldReportConfigChange */);
             }
         }  catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
diff --git a/core/java/android/window/WindowInfosListener.java b/core/java/android/window/WindowInfosListener.java
new file mode 100644
index 0000000..4376e3e
--- /dev/null
+++ b/core/java/android/window/WindowInfosListener.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.view.InputWindowHandle;
+
+import libcore.util.NativeAllocationRegistry;
+
+/**
+ * Listener for getting {@link InputWindowHandle} updates from SurfaceFlinger.
+ * @hide
+ */
+public abstract class WindowInfosListener {
+    private final long mNativeListener;
+
+    public WindowInfosListener() {
+        NativeAllocationRegistry registry = NativeAllocationRegistry.createMalloced(
+                WindowInfosListener.class.getClassLoader(), nativeGetFinalizer());
+
+        mNativeListener = nativeCreate(this);
+        registry.registerNativeAllocation(this, mNativeListener);
+    }
+
+    /**
+     * Called when WindowInfos in SurfaceFlinger have changed.
+     * @param windowHandles Reverse Z ordered array of window information that was on screen,
+     *                      where the first value is the topmost window.
+     */
+    public abstract void onWindowInfosChanged(InputWindowHandle[] windowHandles);
+
+    /**
+     * Register the WindowInfosListener.
+     */
+    public void register() {
+        nativeRegister(mNativeListener);
+    }
+
+    /**
+     * Unregisters the WindowInfosListener.
+     */
+    public void unregister() {
+        nativeUnregister(mNativeListener);
+    }
+
+    private static native long nativeCreate(WindowInfosListener thiz);
+    private static native void nativeRegister(long ptr);
+    private static native void nativeUnregister(long ptr);
+    private static native long nativeGetFinalizer();
+}
diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java
index 544d422..4ea5ea5 100644
--- a/core/java/android/window/WindowOrganizer.java
+++ b/core/java/android/window/WindowOrganizer.java
@@ -25,6 +25,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Singleton;
+import android.view.RemoteAnimationAdapter;
 
 /**
  * Base class for organizing specific types of windows like Tasks and DisplayAreas
@@ -36,9 +37,16 @@
 
     /**
      * Apply multiple WindowContainer operations at once.
+     *
+     * Note that using this API requires the caller to hold
+     * {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS}, unless the caller is using
+     * {@link TaskFragmentOrganizer}, in which case it is allowed to change TaskFragment that is
+     * created by itself.
+     *
      * @param t The transaction to apply.
      */
-    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+    @RequiresPermission(value = android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
+            conditional = true)
     public void applyTransaction(@NonNull WindowContainerTransaction t) {
         try {
             if (!t.isEmpty()) {
@@ -51,6 +59,12 @@
 
     /**
      * Apply multiple WindowContainer operations at once.
+     *
+     * Note that using this API requires the caller to hold
+     * {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS}, unless the caller is using
+     * {@link TaskFragmentOrganizer}, in which case it is allowed to change TaskFragment that is
+     * created by itself.
+     *
      * @param t The transaction to apply.
      * @param callback This transaction will use the synchronization scheme described in
      *        BLASTSyncEngine.java. The SurfaceControl transaction containing the effects of this
@@ -58,7 +72,8 @@
      * @return An ID for the sync operation which will later be passed to transactionReady callback.
      *         This lets the caller differentiate overlapping sync operations.
      */
-    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+    @RequiresPermission(value = android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
+            conditional = true)
     public int applySyncTransaction(@NonNull WindowContainerTransaction t,
             @NonNull WindowContainerTransactionCallback callback) {
         try {
@@ -111,6 +126,27 @@
     }
 
     /**
+     * Start a legacy transition.
+     * @param type The type of the transition. This is ignored if a transitionToken is provided.
+     * @param adapter An existing transition to start. If null, a new transition is created.
+     * @param t The set of window operations that are part of this transition.
+     * @return true on success, false if a transition was already running.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+    @NonNull
+    public int startLegacyTransition(int type, @NonNull RemoteAnimationAdapter adapter,
+            @NonNull WindowContainerTransactionCallback syncCallback,
+            @NonNull WindowContainerTransaction t) {
+        try {
+            return getWindowOrganizerController().startLegacyTransition(
+                    type, adapter, syncCallback.mInterface, t);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Register an ITransitionPlayer to handle transition animations.
      * @hide
      */
@@ -123,8 +159,19 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
-    IWindowOrganizerController getWindowOrganizerController() {
+    /**
+     * @see TransitionMetrics
+     * @hide
+     */
+    public static ITransitionMetricsReporter getTransitionMetricsReporter() {
+        try {
+            return getWindowOrganizerController().getTransitionMetricsReporter();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    static IWindowOrganizerController getWindowOrganizerController() {
         return IWindowOrganizerControllerSingleton.get();
     }
 
diff --git a/core/java/android/window/WindowProvider.java b/core/java/android/window/WindowProvider.java
new file mode 100644
index 0000000..b078b93
--- /dev/null
+++ b/core/java/android/window/WindowProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.window;
+
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.view.WindowManager.LayoutParams.WindowType;
+
+/**
+ * An interface to provide a non-activity window.
+ * Examples are {@link WindowContext} and {@link WindowProviderService}.
+ *
+ * @hide
+ */
+public interface WindowProvider {
+    /** @hide */
+    String KEY_IS_WINDOW_PROVIDER_SERVICE = "android.windowContext.isWindowProviderService";
+
+    /** Gets the window type of this provider */
+    @WindowType
+    int getWindowType();
+
+    /** Gets the launch options of this provider */
+    @Nullable
+    Bundle getWindowContextOptions();
+}
diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java
index b8619fb..f8484d1 100644
--- a/core/java/android/window/WindowProviderService.java
+++ b/core/java/android/window/WindowProviderService.java
@@ -36,27 +36,45 @@
 import android.view.WindowManager.LayoutParams.WindowType;
 import android.view.WindowManagerImpl;
 
-// TODO(b/159767464): handle #onConfigurationChanged(Configuration)
 /**
  * A {@link Service} responsible for showing a non-activity window, such as software keyboards or
  * accessibility overlay windows. This {@link Service} has similar behavior to
  * {@link WindowContext}, but is represented as {@link Service}.
  *
  * @see android.inputmethodservice.InputMethodService
- * @see android.accessibilityservice.AccessibilityService
  *
  * @hide
  */
 @TestApi
 @UiContext
-public abstract class WindowProviderService extends Service {
+public abstract class WindowProviderService extends Service implements WindowProvider {
 
+    private final Bundle mOptions;
     private final WindowTokenClient mWindowToken = new WindowTokenClient();
     private final WindowContextController mController = new WindowContextController(mWindowToken);
     private WindowManager mWindowManager;
+    private boolean mInitialized;
 
     /**
-     * Returns the type of this {@link WindowProviderService}.
+     * Returns {@code true} if the {@code windowContextOptions} declares that it is a
+     * {@link WindowProviderService}.
+     *
+     * @hide
+     */
+    public static boolean isWindowProviderService(@Nullable Bundle windowContextOptions) {
+        if (windowContextOptions == null) {
+            return false;
+        }
+        return (windowContextOptions.getBoolean(KEY_IS_WINDOW_PROVIDER_SERVICE, false));
+    }
+
+    public WindowProviderService() {
+        mOptions = new Bundle();
+        mOptions.putBoolean(KEY_IS_WINDOW_PROVIDER_SERVICE, true);
+    }
+
+    /**
+     * Returns the window type of this {@link WindowProviderService}.
      * Each inheriting class must implement this method to provide the type of the window. It is
      * used similar to {@code type} of {@link Context#createWindowContext(int, Bundle)}
      *
@@ -68,15 +86,24 @@
     @SuppressLint("OnNameExpected")
     // Suppress the lint because it is not a callback and users should provide window type
     // so we cannot make it final.
-    public abstract @WindowType int getWindowType();
+    @WindowType
+    @Override
+    public abstract int getWindowType();
 
     /**
      * Returns the option of this {@link WindowProviderService}.
-     * Default is {@code null}. The inheriting class can implement this method to provide the
-     * customization {@code option} of the window. It is used similar to {@code options} of
-     * {@link Context#createWindowContext(int, Bundle)}
-     *
-     * @see Context#createWindowContext(int, Bundle)
+     * <p>
+     * The inheriting class can implement this method to provide the customization {@code option} of
+     * the window, but must be based on this method's returned value.
+     * It is used similar to {@code options} of {@link Context#createWindowContext(int, Bundle)}
+     * </p>
+     * <pre class="prettyprint">
+     * public Bundle getWindowContextOptions() {
+     *     final Bundle options = super.getWindowContextOptions();
+     *     options.put(KEY_ROOT_DISPLAY_AREA_ID, displayAreaInfo.rootDisplayAreaId);
+     *     return options;
+     * }
+     * </pre>
      *
      * @hide
      */
@@ -85,8 +112,24 @@
     // Suppress the lint because it is not a callback and users may override this API to provide
     // launch option. Also, the return value of this API is null by default.
     @Nullable
+    @CallSuper
+    @Override
     public Bundle getWindowContextOptions() {
-        return null;
+        return mOptions;
+    }
+
+    /**
+     * Returns the display ID to launch this {@link WindowProviderService}.
+     *
+     * @hide
+     */
+    @TestApi
+    @SuppressLint({"OnNameExpected"})
+    // Suppress the lint because it is not a callback and users may override this API to provide
+    // display.
+    @NonNull
+    public int getInitialDisplayId() {
+        return DEFAULT_DISPLAY;
     }
 
     /**
@@ -104,19 +147,22 @@
     public final Context createServiceBaseContext(ActivityThread mainThread,
             LoadedApk packageInfo) {
         final Context context = super.createServiceBaseContext(mainThread, packageInfo);
-        // Always associate with the default display at initialization.
-        final Display defaultDisplay = context.getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
-        return context.createTokenContext(mWindowToken, defaultDisplay);
+        final Display display = context.getSystemService(DisplayManager.class)
+                .getDisplay(getInitialDisplayId());
+        return context.createTokenContext(mWindowToken, display);
     }
 
-    @CallSuper
+    /** @hide */
     @Override
-    public void onCreate() {
-        super.onCreate();
-        mWindowToken.attachContext(this);
-        mController.attachToDisplayArea(getWindowType(), getDisplayId(), getWindowContextOptions());
-        mWindowManager = WindowManagerImpl.createWindowContextWindowManager(this);
+    protected void attachBaseContext(Context newBase) {
+        super.attachBaseContext(newBase);
+        if (!mInitialized) {
+            mWindowToken.attachContext(this);
+            mController.attachToDisplayArea(getWindowType(), getDisplayId(),
+                    getWindowContextOptions());
+            mWindowManager = WindowManagerImpl.createWindowContextWindowManager(this);
+            mInitialized = true;
+        }
     }
 
     @SuppressLint("OnNameExpected")
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index 4dcd2e7..f3e3859 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -15,14 +15,23 @@
  */
 package android.window;
 
+import static android.window.ConfigurationHelper.diffPublicWithSizeBuckets;
+import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
+import static android.window.ConfigurationHelper.isDifferentDisplay;
+import static android.window.ConfigurationHelper.shouldUpdateResources;
+
 import android.annotation.NonNull;
 import android.app.ActivityThread;
 import android.app.IWindowToken;
 import android.app.ResourcesManager;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.inputmethodservice.AbstractInputMethodService;
+import android.os.Build;
 import android.os.Bundle;
+import android.os.Debug;
 import android.os.IBinder;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -40,6 +49,8 @@
  * @hide
  */
 public class WindowTokenClient extends IWindowToken.Stub {
+    private static final String TAG = WindowTokenClient.class.getSimpleName();
+
     /**
      * Attached {@link Context} for this window token to update configuration and resources.
      * Initialized by {@link #attachContext(Context)}.
@@ -48,6 +59,10 @@
 
     private final ResourcesManager mResourcesManager = ResourcesManager.getInstance();
 
+    private final Configuration mConfiguration = new Configuration();
+
+    private boolean mShouldDumpConfigForIme;
+
     /**
      * Attaches {@code context} to this {@link WindowTokenClient}. Each {@link WindowTokenClient}
      * can only attach one {@link Context}.
@@ -63,6 +78,9 @@
             throw new IllegalStateException("Context is already attached.");
         }
         mContextRef = new WeakReference<>(context);
+        mConfiguration.setTo(context.getResources().getConfiguration());
+        mShouldDumpConfigForIme = Build.IS_DEBUGGABLE
+                && context instanceof AbstractInputMethodService;
     }
 
     /**
@@ -71,24 +89,76 @@
      * @param newConfig the updated {@link Configuration}
      * @param newDisplayId the updated {@link android.view.Display} ID
      */
-    @VisibleForTesting
     @Override
     public void onConfigurationChanged(Configuration newConfig, int newDisplayId) {
+        onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */);
+    }
+
+    // TODO(b/192048581): rewrite this method based on WindowContext and WindowProviderService
+    //  are inherited from WindowProvider.
+    /**
+     * Called when {@link Configuration} updates from the server side receive.
+     *
+     * Similar to {@link #onConfigurationChanged(Configuration, int)}, but adds a flag to control
+     * whether to dispatch configuration update or not.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void onConfigurationChanged(Configuration newConfig, int newDisplayId,
+            boolean shouldReportConfigChange) {
         final Context context = mContextRef.get();
         if (context == null) {
             return;
         }
-        final int currentDisplayId = context.getDisplayId();
-        final boolean displayChanged = newDisplayId != currentDisplayId;
-        final Configuration config = context.getResources().getConfiguration();
-        final boolean configChanged = config.diff(newConfig) != 0;
-        if (displayChanged || configChanged) {
+        final boolean displayChanged = isDifferentDisplay(context.getDisplayId(), newDisplayId);
+        final boolean shouldUpdateResources = shouldUpdateResources(this, mConfiguration,
+                newConfig, newConfig /* overrideConfig */, displayChanged,
+                null /* configChanged */);
+
+        if (!shouldUpdateResources && mShouldDumpConfigForIme) {
+            Log.d(TAG, "Configuration not dispatch to IME because configuration is up"
+                    + " to date. Current config=" + context.getResources().getConfiguration()
+                    + ", reported config=" + mConfiguration
+                    + ", updated config=" + newConfig);
+        }
+
+        if (shouldUpdateResources) {
             // TODO(ag/9789103): update resource manager logic to track non-activity tokens
             mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId);
-            if (context instanceof WindowContext) {
+
+            if (shouldReportConfigChange && context instanceof WindowContext) {
+                final WindowContext windowContext = (WindowContext) context;
                 ActivityThread.currentActivityThread().getHandler().post(
-                        () -> ((WindowContext) context).dispatchConfigurationChanged(newConfig));
+                        () -> windowContext.dispatchConfigurationChanged(newConfig));
             }
+
+            // Dispatch onConfigurationChanged only if there's a significant public change to
+            // make it compatible with the original behavior.
+            final Configuration[] sizeConfigurations = context.getResources()
+                    .getSizeConfigurations();
+            final SizeConfigurationBuckets buckets = sizeConfigurations != null
+                    ? new SizeConfigurationBuckets(sizeConfigurations) : null;
+            final int diff = diffPublicWithSizeBuckets(mConfiguration, newConfig, buckets);
+
+            if (shouldReportConfigChange && diff != 0
+                    && context instanceof WindowProviderService) {
+                final WindowProviderService windowProviderService = (WindowProviderService) context;
+                ActivityThread.currentActivityThread().getHandler().post(
+                        () -> windowProviderService.onConfigurationChanged(newConfig));
+            }
+            freeTextLayoutCachesIfNeeded(diff);
+            if (mShouldDumpConfigForIme) {
+                if (!shouldReportConfigChange) {
+                    Log.d(TAG, "Only apply configuration update to Resources because "
+                            + "shouldReportConfigChange is false.\n" + Debug.getCallers(5));
+                } else if (diff == 0) {
+                    Log.d(TAG, "Configuration not dispatch to IME because configuration has no "
+                            + " public difference with updated config. "
+                            + " Current config=" + context.getResources().getConfiguration()
+                            + ", reported config=" + mConfiguration
+                            + ", updated config=" + newConfig);
+                }
+            }
+            mConfiguration.setTo(newConfig);
         }
         if (displayChanged) {
             context.updateDisplay(newDisplayId);
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 0b92b93..874e3f4 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -47,6 +47,8 @@
 public class AccessibilityShortcutChooserActivity extends Activity {
     @ShortcutType
     private final int mShortcutType = ACCESSIBILITY_SHORTCUT_KEY;
+    private static final String KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE =
+            "accessibility_shortcut_menu_mode";
     private final List<AccessibilityTarget> mTargets = new ArrayList<>();
     private AlertDialog mMenuDialog;
     private AlertDialog mPermissionDialog;
@@ -66,14 +68,30 @@
         mMenuDialog = createMenuDialog();
         mMenuDialog.setOnShowListener(dialog -> updateDialogListeners());
         mMenuDialog.show();
+
+        if (savedInstanceState != null) {
+            final int restoreShortcutMenuMode =
+                    savedInstanceState.getInt(KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE,
+                            ShortcutMenuMode.LAUNCH);
+            if (restoreShortcutMenuMode == ShortcutMenuMode.EDIT) {
+                onEditButtonClicked();
+            }
+        }
     }
 
     @Override
     protected void onDestroy() {
+        mMenuDialog.setOnDismissListener(null);
         mMenuDialog.dismiss();
         super.onDestroy();
     }
 
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE, mTargetAdapter.getShortcutMenuMode());
+    }
+
     private void onTargetSelected(AdapterView<?> parent, View view, int position, long id) {
         final AccessibilityTarget target = mTargets.get(position);
         target.onSelected();
diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java
index c57afbc..179ac8b 100644
--- a/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java
+++ b/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java
@@ -17,6 +17,7 @@
 package com.android.internal.accessibility.util;
 
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
+import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
@@ -30,6 +31,7 @@
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_BUTTON;
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_BUTTON_LONG_PRESS;
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_FLOATING_MENU;
+import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_GESTURE;
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__TRIPLE_TAP;
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__UNKNOWN_TYPE;
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__VOLUME_KEY;
@@ -152,19 +154,29 @@
                 convertToLoggingMagnificationMode(mode));
     }
 
-    private static boolean isFloatingMenuEnabled(Context context) {
+    private static boolean isAccessibilityFloatingMenuEnabled(Context context) {
         return Settings.Secure.getInt(context.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1)
                 == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
     }
 
+    private static boolean isAccessibilityGestureEnabled(Context context) {
+        return Settings.Secure.getInt(context.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1)
+                == ACCESSIBILITY_BUTTON_MODE_GESTURE;
+    }
+
     private static int convertToLoggingShortcutType(Context context,
             @ShortcutType int shortcutType) {
         switch (shortcutType) {
             case ACCESSIBILITY_BUTTON:
-                return isFloatingMenuEnabled(context)
-                        ? ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_FLOATING_MENU
-                        : ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_BUTTON;
+                if (isAccessibilityFloatingMenuEnabled(context)) {
+                    return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_FLOATING_MENU;
+                } else if (isAccessibilityGestureEnabled(context)) {
+                    return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_GESTURE;
+                } else {
+                    return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_BUTTON;
+                }
             case ACCESSIBILITY_SHORTCUT_KEY:
                 return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__VOLUME_KEY;
         }
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index c8a4425..9985262 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -272,4 +272,14 @@
     void triggerHardwareRecognitionEventForTest(
             in SoundTrigger.KeyphraseRecognitionEvent event,
             in IHotwordRecognitionStatusCallback callback);
+
+    /**
+     * Starts to listen the status of visible activity.
+     */
+    void startListeningVisibleActivityChanged(in IBinder token);
+
+    /**
+     * Stops to listen the status of visible activity.
+     */
+    void stopListeningVisibleActivityChanged(in IBinder token);
 }
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index d12c870..c863292 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -45,6 +45,7 @@
 import android.view.ViewRootImpl;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor.Configuration;
 import com.android.internal.jank.InteractionJankMonitor.Session;
 import com.android.internal.util.FrameworkStatsLog;
 
@@ -70,6 +71,7 @@
     static final int REASON_CANCEL_NORMAL = 16;
     static final int REASON_CANCEL_NOT_BEGUN = 17;
     static final int REASON_CANCEL_SAME_VSYNC = 18;
+    static final int REASON_CANCEL_TIMEOUT = 19;
 
     /** @hide */
     @IntDef({
@@ -98,6 +100,9 @@
     private final Handler mHandler;
     private final ChoreographerWrapper mChoreographer;
 
+    @VisibleForTesting
+    public final boolean mSurfaceOnly;
+
     private long mBeginVsyncId = INVALID_ID;
     private long mEndVsyncId = INVALID_ID;
     private boolean mMetricsFinalized;
@@ -138,71 +143,85 @@
     }
 
     public FrameTracker(@NonNull Session session, @NonNull Handler handler,
-            @NonNull ThreadedRendererWrapper renderer, @NonNull ViewRootWrapper viewRootWrapper,
+            @Nullable ThreadedRendererWrapper renderer, @Nullable ViewRootWrapper viewRootWrapper,
             @NonNull SurfaceControlWrapper surfaceControlWrapper,
             @NonNull ChoreographerWrapper choreographer,
-            @NonNull FrameMetricsWrapper metrics, int traceThresholdMissedFrames,
-            int traceThresholdFrameTimeMillis, @Nullable FrameTrackerListener listener) {
+            @Nullable FrameMetricsWrapper metrics,
+            int traceThresholdMissedFrames, int traceThresholdFrameTimeMillis,
+            @Nullable FrameTrackerListener listener, @NonNull Configuration config) {
+        mSurfaceOnly = config.isSurfaceOnly();
         mSession = session;
-        mRendererWrapper = renderer;
-        mMetricsWrapper = metrics;
-        mViewRoot = viewRootWrapper;
+        mHandler = handler;
         mChoreographer = choreographer;
         mSurfaceControlWrapper = surfaceControlWrapper;
-        mHandler = handler;
-        mObserver = new HardwareRendererObserver(
-                this, mMetricsWrapper.getTiming(), handler, false /*waitForPresentTime*/);
+
+        // HWUI instrumentation init.
+        mRendererWrapper = mSurfaceOnly ? null : renderer;
+        mMetricsWrapper = mSurfaceOnly ? null : metrics;
+        mViewRoot = mSurfaceOnly ? null : viewRootWrapper;
+        mObserver = mSurfaceOnly
+                ? null
+                : new HardwareRendererObserver(this, mMetricsWrapper.getTiming(),
+                        handler, /* waitForPresentTime= */ false);
+
         mTraceThresholdMissedFrames = traceThresholdMissedFrames;
         mTraceThresholdFrameTimeMillis = traceThresholdFrameTimeMillis;
         mListener = listener;
 
-        // If the surface isn't valid yet, wait until it's created.
-        if (viewRootWrapper.getSurfaceControl().isValid()) {
-            mSurfaceControl = viewRootWrapper.getSurfaceControl();
-        }
-        mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
-            @Override
-            public void surfaceCreated(SurfaceControl.Transaction t) {
-                synchronized (FrameTracker.this) {
-                    if (mSurfaceControl == null) {
-                        mSurfaceControl = viewRootWrapper.getSurfaceControl();
-                        if (mBeginVsyncId != INVALID_ID) {
-                            mSurfaceControlWrapper.addJankStatsListener(
-                                    FrameTracker.this, mSurfaceControl);
-                            postTraceStartMarker();
+        if (mSurfaceOnly) {
+            mSurfaceControl = config.getSurfaceControl();
+            mSurfaceChangedCallback = null;
+        } else {
+            // HWUI instrumentation init.
+            // If the surface isn't valid yet, wait until it's created.
+            if (mViewRoot.getSurfaceControl().isValid()) {
+                mSurfaceControl = mViewRoot.getSurfaceControl();
+            }
+
+            mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
+                @Override
+                public void surfaceCreated(SurfaceControl.Transaction t) {
+                    synchronized (FrameTracker.this) {
+                        if (mSurfaceControl == null) {
+                            mSurfaceControl = mViewRoot.getSurfaceControl();
+                            if (mBeginVsyncId != INVALID_ID) {
+                                mSurfaceControlWrapper.addJankStatsListener(
+                                        FrameTracker.this, mSurfaceControl);
+                                postTraceStartMarker();
+                            }
                         }
                     }
                 }
-            }
 
-            @Override
-            public void surfaceReplaced(SurfaceControl.Transaction t) {
-            }
+                @Override
+                public void surfaceReplaced(SurfaceControl.Transaction t) {
+                }
 
-            @Override
-            public void surfaceDestroyed() {
+                @Override
+                public void surfaceDestroyed() {
 
-                // Wait a while to give the system a chance for the remaining frames to arrive, then
-                // force finish the session.
-                mHandler.postDelayed(() -> {
-                    synchronized (FrameTracker.this) {
-                        if (DEBUG) {
-                            Log.d(TAG, "surfaceDestroyed: " + mSession.getName()
-                                    + ", finalized=" + mMetricsFinalized
-                                    + ", info=" + mJankInfos.size()
-                                    + ", vsync=" + mBeginVsyncId + "-" + mEndVsyncId);
+                    // Wait a while to give the system a chance for the remaining
+                    // frames to arrive, then force finish the session.
+                    mHandler.postDelayed(() -> {
+                        synchronized (FrameTracker.this) {
+                            if (DEBUG) {
+                                Log.d(TAG, "surfaceDestroyed: " + mSession.getName()
+                                        + ", finalized=" + mMetricsFinalized
+                                        + ", info=" + mJankInfos.size()
+                                        + ", vsync=" + mBeginVsyncId + "-" + mEndVsyncId);
+                            }
+                            if (!mMetricsFinalized) {
+                                end(REASON_END_SURFACE_DESTROYED);
+                                finish(mJankInfos.size() - 1);
+                            }
                         }
-                        if (!mMetricsFinalized) {
-                            end(REASON_END_SURFACE_DESTROYED);
-                            finish(mJankInfos.size() - 1);
-                        }
-                    }
-                }, 50);
-            }
-        };
-
-        // This callback has a reference to FrameTracker, remember to remove it to avoid leakage.
-        viewRootWrapper.addSurfaceChangedCallback(mSurfaceChangedCallback);
+                    }, 50);
+                }
+            };
+            // This callback has a reference to FrameTracker,
+            // remember to remove it to avoid leakage.
+            mViewRoot.addSurfaceChangedCallback(mSurfaceChangedCallback);
+        }
     }
 
     /**
@@ -210,16 +229,16 @@
      */
     public synchronized void begin() {
         mBeginVsyncId = mChoreographer.getVsyncId() + 1;
-        if (mSurfaceControl != null) {
-            postTraceStartMarker();
-        }
-        mRendererWrapper.addObserver(mObserver);
         if (DEBUG) {
             Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
         }
         if (mSurfaceControl != null) {
+            postTraceStartMarker();
             mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
         }
+        if (!mSurfaceOnly) {
+            mRendererWrapper.addObserver(mObserver);
+        }
         if (mListener != null) {
             mListener.onCujEvents(mSession, ACTION_SESSION_BEGIN);
         }
@@ -265,6 +284,7 @@
             if (mListener != null) {
                 mListener.onCujEvents(mSession, ACTION_SESSION_END);
             }
+
             // We don't remove observer here,
             // will remove it when all the frame metrics in this duration are called back.
             // See onFrameMetricsAvailable for the logic of removing the observer.
@@ -281,11 +301,12 @@
      * Cancel the trace session of the CUJ.
      */
     public synchronized void cancel(@Reasons int reason) {
+        mCancelled = true;
+
         // We don't need to end the trace section if it never begun.
         if (mTracingStarted) {
             Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
         }
-        mCancelled = true;
 
         // Always remove the observers in cancel call to avoid leakage.
         removeObservers();
@@ -385,7 +406,7 @@
         for (int i = mJankInfos.size() - 1; i >= 0; i--) {
             JankInfo info = mJankInfos.valueAt(i);
             if (info.frameVsyncId >= mEndVsyncId) {
-                if (info.hwuiCallbackFired && info.surfaceControlCallbackFired) {
+                if (isLastIndexCandidate(info)) {
                     lastIndex = i;
                 }
             } else {
@@ -403,6 +424,12 @@
         finish(indexOnOrAfterEnd);
     }
 
+    private boolean isLastIndexCandidate(JankInfo info) {
+        return mSurfaceOnly
+                ? info.surfaceControlCallbackFired
+                : info.hwuiCallbackFired && info.surfaceControlCallbackFired;
+    }
+
     private void finish(int indexOnOrAfterEnd) {
         mHandler.removeCallbacks(mWaitForFinishTimedOut);
         mWaitForFinishTimedOut = null;
@@ -419,7 +446,8 @@
 
         for (int i = 0; i <= indexOnOrAfterEnd; i++) {
             JankInfo info = mJankInfos.valueAt(i);
-            if (info.isFirstFrame) {
+            final boolean isFirstDrawn = !mSurfaceOnly && info.isFirstFrame;
+            if (isFirstDrawn) {
                 continue;
             }
             if (info.surfaceControlCallbackFired) {
@@ -444,11 +472,11 @@
                 }
                 // TODO (b/174755489): Early latch currently gets fired way too often, so we have
                 // to ignore it for now.
-                if (!info.hwuiCallbackFired) {
+                if (!mSurfaceOnly && !info.hwuiCallbackFired) {
                     Log.w(TAG, "Missing HWUI jank callback for vsyncId: " + info.frameVsyncId);
                 }
             }
-            if (info.hwuiCallbackFired) {
+            if (!mSurfaceOnly && info.hwuiCallbackFired) {
                 maxFrameTimeNanos = Math.max(info.totalDurationNanos, maxFrameTimeNanos);
                 if (!info.surfaceControlCallbackFired) {
                     Log.w(TAG, "Missing SF jank callback for vsyncId: " + info.frameVsyncId);
@@ -471,7 +499,7 @@
         // Trigger perfetto if necessary.
         boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
                 && missedFramesCount >= mTraceThresholdMissedFrames;
-        boolean overFrameTimeThreshold = mTraceThresholdFrameTimeMillis != -1
+        boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
                 && maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
         if (overMissedFramesThreshold || overFrameTimeThreshold) {
             triggerPerfetto();
@@ -482,7 +510,7 @@
                     mSession.getStatsdInteractionType(),
                     totalFramesCount,
                     missedFramesCount,
-                    maxFrameTimeNanos,
+                    maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */
                     missedSfFramesCount,
                     missedAppFramesCount);
             if (mListener != null) {
@@ -505,10 +533,13 @@
      */
     @VisibleForTesting
     public void removeObservers() {
-        mRendererWrapper.removeObserver(mObserver);
         mSurfaceControlWrapper.removeJankStatsListener(this);
-        if (mSurfaceChangedCallback != null) {
-            mViewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
+        if (!mSurfaceOnly) {
+            // HWUI part.
+            mRendererWrapper.removeObserver(mObserver);
+            if (mSurfaceChangedCallback != null) {
+                mViewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 610cd73..f8eb95c 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -18,11 +18,11 @@
 
 import static android.content.Intent.FLAG_RECEIVER_REGISTERED_ONLY;
 
-import static com.android.internal.jank.FrameTracker.ChoreographerWrapper;
 import static com.android.internal.jank.FrameTracker.REASON_CANCEL_NORMAL;
 import static com.android.internal.jank.FrameTracker.REASON_CANCEL_NOT_BEGUN;
+import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT;
 import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
-import static com.android.internal.jank.FrameTracker.SurfaceControlWrapper;
+import static com.android.internal.jank.FrameTracker.REASON_END_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_ALL_APPS_SCROLL;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_PIP;
@@ -41,6 +41,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_TRANSITION_TO_AOD;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_UNLOCK_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__NOTIFICATION_SHADE_SWIPE;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON;
@@ -58,6 +59,8 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_ROW_SWIPE;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_SCROLL_FLING;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -72,11 +75,15 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Choreographer;
+import android.view.SurfaceControl;
 import android.view.View;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.FrameTracker.ChoreographerWrapper;
 import com.android.internal.jank.FrameTracker.FrameMetricsWrapper;
 import com.android.internal.jank.FrameTracker.FrameTrackerListener;
+import com.android.internal.jank.FrameTracker.Reasons;
+import com.android.internal.jank.FrameTracker.SurfaceControlWrapper;
 import com.android.internal.jank.FrameTracker.ThreadedRendererWrapper;
 import com.android.internal.jank.FrameTracker.ViewRootWrapper;
 import com.android.internal.util.PerfettoTrigger;
@@ -163,6 +170,9 @@
     public static final int CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE = 32;
     public static final int CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON = 33;
     public static final int CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP = 34;
+    public static final int CUJ_PIP_TRANSITION = 35;
+    public static final int CUJ_WALLPAPER_TRANSITION = 36;
+    public static final int CUJ_USER_SWITCH = 37;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -206,6 +216,9 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH_FROM_QS_TILE,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -213,10 +226,10 @@
     private final DeviceConfig.OnPropertiesChangedListener mPropertiesChangedListener =
             this::updateProperties;
 
-    private FrameMetricsWrapper mMetrics;
-    private SparseArray<FrameTracker> mRunningTrackers;
-    private SparseArray<Runnable> mTimeoutActions;
-    private HandlerThread mWorker;
+    private final FrameMetricsWrapper mMetrics;
+    private final SparseArray<FrameTracker> mRunningTrackers;
+    private final SparseArray<Runnable> mTimeoutActions;
+    private final HandlerThread mWorker;
 
     private boolean mEnabled = DEFAULT_ENABLED;
     private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
@@ -260,6 +273,9 @@
             CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
             CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON,
             CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
+            CUJ_PIP_TRANSITION,
+            CUJ_WALLPAPER_TRANSITION,
+            CUJ_USER_SWITCH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -310,24 +326,31 @@
     }
 
     /**
-     * Create a {@link FrameTracker} instance.
+     * Creates a {@link FrameTracker} instance.
      *
+     * @param config the config used in instrumenting
      * @param session the session associates with this tracker
      * @return instance of the FrameTracker
      */
     @VisibleForTesting
-    public FrameTracker createFrameTracker(Configuration conf, Session session) {
-        final View v = conf.mView;
-        final Context c = v.getContext().getApplicationContext();
-        final ThreadedRendererWrapper r = new ThreadedRendererWrapper(v.getThreadedRenderer());
-        final ViewRootWrapper vr = new ViewRootWrapper(v.getViewRootImpl());
-        final SurfaceControlWrapper sc = new SurfaceControlWrapper();
-        final ChoreographerWrapper cg = new ChoreographerWrapper(Choreographer.getInstance());
+    public FrameTracker createFrameTracker(Configuration config, Session session) {
+        final View view = config.mView;
+        final ThreadedRendererWrapper threadedRenderer =
+                view == null ? null : new ThreadedRendererWrapper(view.getThreadedRenderer());
+        final ViewRootWrapper viewRoot =
+                view == null ? null : new ViewRootWrapper(view.getViewRootImpl());
+
+        final SurfaceControlWrapper surfaceControl = new SurfaceControlWrapper();
+        final ChoreographerWrapper choreographer =
+                new ChoreographerWrapper(Choreographer.getInstance());
 
         synchronized (this) {
-            FrameTrackerListener eventsListener = (s, act) -> handleCujEvents(c, act, s);
-            return new FrameTracker(session, mWorker.getThreadHandler(), r, vr, sc, cg, mMetrics,
-                    mTraceThresholdMissedFrames, mTraceThresholdFrameTimeMillis, eventsListener);
+            FrameTrackerListener eventsListener =
+                    (s, act) -> handleCujEvents(config.getContext(), act, s);
+            return new FrameTracker(session, mWorker.getThreadHandler(),
+                    threadedRenderer, viewRoot, surfaceControl, choreographer, mMetrics,
+                    mTraceThresholdMissedFrames, mTraceThresholdFrameTimeMillis,
+                    eventsListener, config);
         }
     }
 
@@ -376,7 +399,7 @@
     }
 
     /**
-     * Begin a trace session.
+     * Begins a trace session.
      *
      * @param v an attached view.
      * @param cujType the specific {@link InteractionJankMonitor.CujType}.
@@ -385,8 +408,7 @@
     public boolean begin(View v, @CujType int cujType) {
         try {
             return beginInternal(
-                    new Configuration.Builder(cujType)
-                            .setView(v)
+                    Configuration.Builder.withView(cujType, v)
                             .build());
         } catch (IllegalArgumentException ex) {
             Log.d(TAG, "Build configuration failed!", ex);
@@ -395,7 +417,7 @@
     }
 
     /**
-     * Begin a trace session.
+     * Begins a trace session.
      *
      * @param builder the builder of the configurations for instrumenting the CUJ.
      * @return boolean true if the tracker is started successfully, false otherwise.
@@ -431,48 +453,60 @@
             tracker.begin();
 
             // Cancel the trace if we don't get an end() call in specified duration.
-            Runnable timeoutAction = () -> cancel(cujType);
-            mTimeoutActions.put(cujType, timeoutAction);
-            mWorker.getThreadHandler().postDelayed(timeoutAction, conf.mTimeout);
+            scheduleTimeoutAction(
+                    cujType, conf.mTimeout, () -> cancel(cujType, REASON_CANCEL_TIMEOUT));
             return true;
         }
     }
 
     /**
-     * End a trace session.
+     * Schedules a timeout action.
+     * @param cuj cuj type
+     * @param timeout duration to timeout
+     * @param action action once timeout
+     */
+    @VisibleForTesting
+    public void scheduleTimeoutAction(@CujType int cuj, long timeout, Runnable action) {
+        mTimeoutActions.put(cuj, action);
+        mWorker.getThreadHandler().postDelayed(action, timeout);
+    }
+
+    /**
+     * Ends a trace session.
      *
      * @param cujType the specific {@link InteractionJankMonitor.CujType}.
      * @return boolean true if the tracker is ended successfully, false otherwise.
      */
     public boolean end(@CujType int cujType) {
-        //TODO (163505250): This should be no-op if not in droid food rom.
         synchronized (this) {
-
             // remove the timeout action first.
             removeTimeout(cujType);
             FrameTracker tracker = getTracker(cujType);
             // Skip this call since we haven't started a trace yet.
             if (tracker == null) return false;
-            tracker.end(FrameTracker.REASON_END_NORMAL);
+            tracker.end(REASON_END_NORMAL);
             removeTracker(cujType);
             return true;
         }
     }
 
     /**
-     * Cancel the trace session.
+     * Cancels the trace session.
      *
      * @return boolean true if the tracker is cancelled successfully, false otherwise.
      */
     public boolean cancel(@CujType int cujType) {
-        //TODO (163505250): This should be no-op if not in droid food rom.
+        return cancel(cujType, REASON_CANCEL_NORMAL);
+    }
+
+    boolean cancel(@CujType int cujType, @Reasons int reason) {
         synchronized (this) {
             // remove the timeout action first.
             removeTimeout(cujType);
             FrameTracker tracker = getTracker(cujType);
             // Skip this call since we haven't started a trace yet.
             if (tracker == null) return false;
-            tracker.cancel(FrameTracker.REASON_CANCEL_NORMAL);
+            tracker.cancel(reason);
             removeTracker(cujType);
             return true;
         }
@@ -509,7 +543,7 @@
     }
 
     /**
-     * Trigger the perfetto daemon to collect and upload data.
+     * Triggers the perfetto daemon to collect and upload data.
      */
     @VisibleForTesting
     public void trigger(Session session) {
@@ -608,6 +642,12 @@
                 return "SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON";
             case CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP:
                 return "STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP";
+            case CUJ_PIP_TRANSITION:
+                return "PIP_TRANSITION";
+            case CUJ_WALLPAPER_TRANSITION:
+                return "WALLPAPER_TRANSITION";
+            case CUJ_USER_SWITCH:
+                return "USER_SWITCH";
         }
         return "UNKNOWN";
     }
@@ -618,32 +658,64 @@
      */
     public static class Configuration {
         private final View mView;
+        private final Context mContext;
         private final long mTimeout;
         private final String mTag;
+        private final boolean mSurfaceOnly;
+        private final SurfaceControl mSurfaceControl;
         private final @CujType int mCujType;
 
         /**
-         * A builder for building Configuration. <br/>
+         * A builder for building Configuration. {@link #setView(View)} is essential
+         * if {@link #setSurfaceOnly(boolean)} is not set, otherwise both
+         * {@link #setSurfaceControl(SurfaceControl)} and {@link #setContext(Context)}
+         * are necessary<br/>
          * <b>It may refer to an attached view, don't use static reference for any purpose.</b>
          */
         public static class Builder {
             private View mAttrView = null;
+            private Context mAttrContext = null;
             private long mAttrTimeout = DEFAULT_TIMEOUT_MS;
             private String mAttrTag = "";
+            private boolean mAttrSurfaceOnly;
+            private SurfaceControl mAttrSurfaceControl;
             private @CujType int mAttrCujType;
 
             /**
+             * Creates a builder which instruments only surface.
              * @param cuj The enum defined in {@link InteractionJankMonitor.CujType}.
+             * @param context context
+             * @param surfaceControl surface control
+             * @return builder
              */
-            public Builder(@CujType int cuj) {
+            public static Builder withSurface(@CujType int cuj, @NonNull Context context,
+                    @NonNull SurfaceControl surfaceControl) {
+                return new Builder(cuj)
+                        .setContext(context)
+                        .setSurfaceControl(surfaceControl)
+                        .setSurfaceOnly(true);
+            }
+
+            /**
+             * Creates a builder which instruments both surface and view.
+             * @param cuj The enum defined in {@link InteractionJankMonitor.CujType}.
+             * @param view view
+             * @return builder
+             */
+            public static Builder withView(@CujType int cuj, @NonNull View view) {
+                return new Builder(cuj).setView(view);
+            }
+
+            private Builder(@CujType int cuj) {
                 mAttrCujType = cuj;
             }
 
             /**
+             * Specifies a view, must be set if {@link #setSurfaceOnly(boolean)} is set to false.
              * @param view an attached view
              * @return builder
              */
-            public Builder setView(@NonNull View view) {
+            private Builder setView(@NonNull View view) {
                 mAttrView = view;
                 return this;
             }
@@ -669,20 +741,56 @@
             }
 
             /**
-             * Build the {@link Configuration} instance
+             * Indicates if only instrument with surface,
+             * if true, must also setup with {@link #setContext(Context)}
+             * and {@link #setSurfaceControl(SurfaceControl)}.
+             * @param surfaceOnly true if only instrument with surface, false otherwise
+             * @return builder Surface only builder.
+             */
+            private Builder setSurfaceOnly(boolean surfaceOnly) {
+                mAttrSurfaceOnly = surfaceOnly;
+                return this;
+            }
+
+            /**
+             * Specifies a context, must set if {@link #setSurfaceOnly(boolean)} is set.
+             */
+            private Builder setContext(Context context) {
+                mAttrContext = context;
+                return this;
+            }
+
+            /**
+             * Specifies a surface control, must be set if {@link #setSurfaceOnly(boolean)} is set.
+             */
+            private Builder setSurfaceControl(SurfaceControl surfaceControl) {
+                mAttrSurfaceControl = surfaceControl;
+                return this;
+            }
+
+            /**
+             * Builds the {@link Configuration} instance
              * @return the instance of {@link Configuration}
              * @throws IllegalArgumentException if any invalid attribute is set
              */
             public Configuration build() throws IllegalArgumentException {
-                return new Configuration(mAttrCujType, mAttrView, mAttrTag, mAttrTimeout);
+                return new Configuration(
+                        mAttrCujType, mAttrView, mAttrTag, mAttrTimeout,
+                        mAttrSurfaceOnly, mAttrContext, mAttrSurfaceControl);
             }
         }
 
-        private Configuration(@CujType int cuj, View view, String tag, long timeout) {
+        private Configuration(@CujType int cuj, View view, String tag, long timeout,
+                boolean surfaceOnly, Context context, SurfaceControl surfaceControl) {
             mCujType = cuj;
             mTag = tag;
             mTimeout = timeout;
             mView = view;
+            mSurfaceOnly = surfaceOnly;
+            mContext = context != null
+                    ? context
+                    : (view != null ? view.getContext().getApplicationContext() : null);
+            mSurfaceControl = surfaceControl;
             validate();
         }
 
@@ -698,14 +806,47 @@
                 shouldThrow = true;
                 msg.append("Invalid timeout value; ");
             }
-            if (mView == null || !mView.isAttachedToWindow()) {
-                shouldThrow = true;
-                msg.append("Null view or view is not attached yet; ");
+            if (mSurfaceOnly) {
+                if (mContext == null) {
+                    shouldThrow = true;
+                    msg.append("Must pass in a context if only instrument surface; ");
+                }
+                if (mSurfaceControl == null || !mSurfaceControl.isValid()) {
+                    shouldThrow = true;
+                    msg.append("Must pass in a valid surface control if only instrument surface; ");
+                }
+            } else {
+                if (mView == null || !mView.isAttachedToWindow()) {
+                    shouldThrow = true;
+                    msg.append("Null view or unattached view while instrumenting view; ");
+                }
             }
             if (shouldThrow) {
                 throw new IllegalArgumentException(msg.toString());
             }
         }
+
+        /**
+         * @return true if only instrumenting surface, false otherwise
+         */
+        public boolean isSurfaceOnly() {
+            return mSurfaceOnly;
+        }
+
+        /**
+         * @return the surafce control which is instrumenting
+         */
+        public SurfaceControl getSurfaceControl() {
+            return mSurfaceControl;
+        }
+
+        View getView() {
+            return mView;
+        }
+
+        Context getContext() {
+            return mContext;
+        }
     }
 
     /**
@@ -715,8 +856,8 @@
         @CujType
         private final int mCujType;
         private final long mTimeStamp;
-        @FrameTracker.Reasons
-        private int mReason = FrameTracker.REASON_END_UNKNOWN;
+        @Reasons
+        private int mReason = REASON_END_UNKNOWN;
         private final boolean mShouldNotify;
         private final String mName;
 
@@ -756,15 +897,15 @@
             return mTimeStamp;
         }
 
-        public void setReason(@FrameTracker.Reasons int reason) {
+        public void setReason(@Reasons int reason) {
             mReason = reason;
         }
 
-        public int getReason() {
+        public @Reasons int getReason() {
             return mReason;
         }
 
-        /** Determine if should notify the receivers of cuj events */
+        /** Determines if should notify the receivers of cuj events */
         public boolean shouldNotify() {
             return mShouldNotify;
         }
diff --git a/core/java/com/android/internal/notification/NotificationAccessConfirmationActivityContract.java b/core/java/com/android/internal/notification/NotificationAccessConfirmationActivityContract.java
index 4ce6f60..3eb9804 100644
--- a/core/java/com/android/internal/notification/NotificationAccessConfirmationActivityContract.java
+++ b/core/java/com/android/internal/notification/NotificationAccessConfirmationActivityContract.java
@@ -17,19 +17,27 @@
 package com.android.internal.notification;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 
+import com.android.internal.R;
+
+/**
+ * This class provides methods to create intents for NotificationAccessConfirmationActivity.
+ */
 public final class NotificationAccessConfirmationActivityContract {
-    private static final ComponentName COMPONENT_NAME = new ComponentName(
-            "com.android.settings",
-            "com.android.settings.notification.NotificationAccessConfirmationActivity");
     public static final String EXTRA_USER_ID = "user_id";
     public static final String EXTRA_COMPONENT_NAME = "component_name";
     public static final String EXTRA_PACKAGE_TITLE = "package_title";
 
-    public static Intent launcherIntent(int userId, ComponentName component, String packageTitle) {
+    /**
+     * Creates a launcher intent for NotificationAccessConfirmationActivity.
+     */
+    public static Intent launcherIntent(Context context, int userId, ComponentName component,
+            String packageTitle) {
         return new Intent()
-                .setComponent(COMPONENT_NAME)
+                .setComponent(ComponentName.unflattenFromString(context.getString(
+                        R.string.config_notificationAccessConfirmationActivity)))
                 .putExtra(EXTRA_USER_ID, userId)
                 .putExtra(EXTRA_COMPONENT_NAME, component)
                 .putExtra(EXTRA_PACKAGE_TITLE, packageTitle);
diff --git a/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java b/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java
index 0307268..9443070 100644
--- a/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java
+++ b/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT;
+
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
@@ -29,11 +31,15 @@
  * Estimates power consumed by the ambient display
  */
 public class AmbientDisplayPowerCalculator extends PowerCalculator {
-    private final UsageBasedPowerEstimator mPowerEstimator;
+    private final UsageBasedPowerEstimator[] mPowerEstimators;
 
     public AmbientDisplayPowerCalculator(PowerProfile powerProfile) {
-        mPowerEstimator = new UsageBasedPowerEstimator(
-                powerProfile.getAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY));
+        final int numDisplays = powerProfile.getNumDisplays();
+        mPowerEstimators = new UsageBasedPowerEstimator[numDisplays];
+        for (int display = 0; display < numDisplays; display++) {
+            mPowerEstimators[display] = new UsageBasedPowerEstimator(
+                    powerProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, display));
+        }
     }
 
     /**
@@ -47,8 +53,8 @@
         final int powerModel = getPowerModel(measuredEnergyUC, query);
         final long durationMs = calculateDuration(batteryStats, rawRealtimeUs,
                 BatteryStats.STATS_SINCE_CHARGED);
-        final double powerMah = getMeasuredOrEstimatedPower(powerModel,
-                measuredEnergyUC, mPowerEstimator, durationMs);
+        final double powerMah = calculateTotalPower(powerModel, batteryStats, rawRealtimeUs,
+                measuredEnergyUC);
         builder.getAggregateBatteryConsumerBuilder(
                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
                 .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY, durationMs)
@@ -68,9 +74,8 @@
         final long measuredEnergyUC = batteryStats.getScreenDozeMeasuredBatteryConsumptionUC();
         final long durationMs = calculateDuration(batteryStats, rawRealtimeUs, statsType);
         final int powerModel = getPowerModel(measuredEnergyUC);
-        final double powerMah = getMeasuredOrEstimatedPower(powerModel,
-                batteryStats.getScreenDozeMeasuredBatteryConsumptionUC(),
-                mPowerEstimator, durationMs);
+        final double powerMah = calculateTotalPower(powerModel, batteryStats, rawRealtimeUs,
+                measuredEnergyUC);
         if (powerMah > 0) {
             BatterySipper bs = new BatterySipper(BatterySipper.DrainType.AMBIENT_DISPLAY, null, 0);
             bs.usagePowerMah = powerMah;
@@ -83,4 +88,26 @@
     private long calculateDuration(BatteryStats batteryStats, long rawRealtimeUs, int statsType) {
         return batteryStats.getScreenDozeTime(rawRealtimeUs, statsType) / 1000;
     }
+
+    private double calculateTotalPower(@BatteryConsumer.PowerModel int powerModel,
+            BatteryStats batteryStats, long rawRealtimeUs, long consumptionUC) {
+        switch (powerModel) {
+            case BatteryConsumer.POWER_MODEL_MEASURED_ENERGY:
+                return uCtoMah(consumptionUC);
+            case BatteryConsumer.POWER_MODEL_POWER_PROFILE:
+            default:
+                return calculateEstimatedPower(batteryStats, rawRealtimeUs);
+        }
+    }
+
+    private double calculateEstimatedPower(BatteryStats batteryStats, long rawRealtimeUs) {
+        final int numDisplays = mPowerEstimators.length;
+        double power = 0;
+        for (int display = 0; display < numDisplays; display++) {
+            final long dozeTime = batteryStats.getDisplayScreenDozeTime(display, rawRealtimeUs)
+                    / 1000;
+            power += mPowerEstimators[display].calculatePower(dozeTime);
+        }
+        return power;
+    }
 }
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index a817119..169eff0 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -690,7 +690,7 @@
          * Schedule a sync because of a screen state change.
          */
         Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
-                boolean onBatteryScreenOff, int screenState);
+                boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates);
         Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis);
         void cancelCpuSyncDueToWakelockChange();
         Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis);
@@ -851,17 +851,91 @@
     public boolean mRecordAllHistory;
     boolean mNoAutoReset;
 
+    /**
+     * Overall screen state. For multidisplay devices, this represents the current highest screen
+     * state of the displays.
+     */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     protected int mScreenState = Display.STATE_UNKNOWN;
+    /**
+     * Overall screen on timer. For multidisplay devices, this represents the time spent with at
+     * least one display in the screen on state.
+     */
     StopwatchTimer mScreenOnTimer;
+    /**
+     * Overall screen doze timer. For multidisplay devices, this represents the time spent with
+     * screen doze being the highest screen state.
+     */
     StopwatchTimer mScreenDozeTimer;
-
+    /**
+     * Overall screen brightness bin. For multidisplay devices, this represents the current
+     * brightest screen.
+     */
     int mScreenBrightnessBin = -1;
+    /**
+     * Overall screen brightness timers. For multidisplay devices, the {@link mScreenBrightnessBin}
+     * timer will be active at any given time
+     */
     final StopwatchTimer[] mScreenBrightnessTimer =
             new StopwatchTimer[NUM_SCREEN_BRIGHTNESS_BINS];
 
     boolean mPretendScreenOff;
 
+    private static class DisplayBatteryStats {
+        /**
+         * Per display screen state.
+         */
+        public int screenState = Display.STATE_UNKNOWN;
+        /**
+         * Per display screen on timers.
+         */
+        public StopwatchTimer screenOnTimer;
+        /**
+         * Per display screen doze timers.
+         */
+        public StopwatchTimer screenDozeTimer;
+        /**
+         * Per display screen brightness bins.
+         */
+        public int screenBrightnessBin = -1;
+        /**
+         * Per display screen brightness timers.
+         */
+        public StopwatchTimer[] screenBrightnessTimers =
+                new StopwatchTimer[NUM_SCREEN_BRIGHTNESS_BINS];
+        /**
+         * Per display screen state the last time {@link #updateDisplayMeasuredEnergyStatsLocked}
+         * was called.
+         */
+        public int screenStateAtLastEnergyMeasurement = Display.STATE_UNKNOWN;
+
+        DisplayBatteryStats(Clocks clocks, TimeBase timeBase) {
+            screenOnTimer = new StopwatchTimer(clocks, null, -1, null,
+                    timeBase);
+            screenDozeTimer = new StopwatchTimer(clocks, null, -1, null,
+                    timeBase);
+            for (int i = 0; i < NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+                screenBrightnessTimers[i] = new StopwatchTimer(clocks, null, -100 - i, null,
+                        timeBase);
+            }
+        }
+
+        /**
+         * Reset display timers.
+         */
+        public void reset(long elapsedRealtimeUs) {
+            screenOnTimer.reset(false, elapsedRealtimeUs);
+            screenDozeTimer.reset(false, elapsedRealtimeUs);
+            for (int i = 0; i < NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+                screenBrightnessTimers[i].reset(false, elapsedRealtimeUs);
+            }
+        }
+    }
+
+    DisplayBatteryStats[] mPerDisplayBatteryStats;
+
+    private int mDisplayMismatchWtfCount = 0;
+
     boolean mInteractive;
     StopwatchTimer mInteractiveTimer;
 
@@ -1006,8 +1080,6 @@
     @GuardedBy("this")
     @VisibleForTesting
     protected @Nullable MeasuredEnergyStats mGlobalMeasuredEnergyStats;
-    /** Last known screen state. Needed for apportioning display energy. */
-    int mScreenStateAtLastEnergyMeasurement = Display.STATE_UNKNOWN;
     /** Bluetooth Power calculator for attributing measured bluetooth charge consumption to uids */
     @Nullable BluetoothPowerCalculator mBluetoothPowerCalculator = null;
     /** Cpu Power calculator for attributing measured cpu charge consumption to uids */
@@ -4308,8 +4380,10 @@
     public void setPretendScreenOff(boolean pretendScreenOff) {
         if (mPretendScreenOff != pretendScreenOff) {
             mPretendScreenOff = pretendScreenOff;
-            noteScreenStateLocked(pretendScreenOff ? Display.STATE_OFF : Display.STATE_ON,
-                    mClocks.elapsedRealtime(), mClocks.uptimeMillis(), mClocks.currentTimeMillis());
+            final int primaryScreenState = mPerDisplayBatteryStats[0].screenState;
+            noteScreenStateLocked(0, primaryScreenState,
+                    mClocks.elapsedRealtime(), mClocks.uptimeMillis(),
+                    mClocks.currentTimeMillis());
         }
     }
 
@@ -4907,29 +4981,158 @@
     }
 
     @GuardedBy("this")
-    public void noteScreenStateLocked(int state) {
-        noteScreenStateLocked(state, mClocks.elapsedRealtime(), mClocks.uptimeMillis(),
+    public void noteScreenStateLocked(int display, int state) {
+        noteScreenStateLocked(display, state, mClocks.elapsedRealtime(), mClocks.uptimeMillis(),
                 mClocks.currentTimeMillis());
     }
 
     @GuardedBy("this")
-    public void noteScreenStateLocked(int state,
+    public void noteScreenStateLocked(int display, int displayState,
             long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
-        state = mPretendScreenOff ? Display.STATE_OFF : state;
-
         // Battery stats relies on there being 4 states. To accommodate this, new states beyond the
         // original 4 are mapped to one of the originals.
-        if (state > MAX_TRACKED_SCREEN_STATE) {
-            switch (state) {
-                case Display.STATE_VR:
-                    state = Display.STATE_ON;
+        if (displayState > MAX_TRACKED_SCREEN_STATE) {
+            if (Display.isOnState(displayState)) {
+                displayState = Display.STATE_ON;
+            } else if (Display.isDozeState(displayState)) {
+                if (Display.isSuspendedState(displayState)) {
+                    displayState = Display.STATE_DOZE_SUSPEND;
+                } else {
+                    displayState = Display.STATE_DOZE;
+                }
+            } else if (Display.isOffState(displayState)) {
+                displayState = Display.STATE_OFF;
+            } else {
+                Slog.wtf(TAG, "Unknown screen state (not mapped): " + displayState);
+                displayState = Display.STATE_UNKNOWN;
+            }
+        }
+        // As of this point, displayState should be mapped to one of:
+        //  - Display.STATE_ON,
+        //  - Display.STATE_DOZE
+        //  - Display.STATE_DOZE_SUSPEND
+        //  - Display.STATE_OFF
+        //  - Display.STATE_UNKNOWN
+
+        int state;
+        int overallBin = mScreenBrightnessBin;
+        int externalUpdateFlag = 0;
+        boolean shouldScheduleSync = false;
+        final int numDisplay = mPerDisplayBatteryStats.length;
+        if (display < 0 || display >= numDisplay) {
+            Slog.wtf(TAG, "Unexpected note screen state for display " + display + " (only "
+                    + mPerDisplayBatteryStats.length + " displays exist...)");
+            return;
+        }
+        final DisplayBatteryStats displayStats = mPerDisplayBatteryStats[display];
+        final int oldDisplayState = displayStats.screenState;
+
+        if (oldDisplayState == displayState) {
+            // Nothing changed
+            state = mScreenState;
+        } else {
+            displayStats.screenState = displayState;
+
+            // Stop timer for previous display state.
+            switch (oldDisplayState) {
+                case Display.STATE_ON:
+                    displayStats.screenOnTimer.stopRunningLocked(elapsedRealtimeMs);
+                    final int bin = displayStats.screenBrightnessBin;
+                    if (bin >= 0) {
+                        displayStats.screenBrightnessTimers[bin].stopRunningLocked(
+                                elapsedRealtimeMs);
+                    }
+                    overallBin = evaluateOverallScreenBrightnessBinLocked();
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_DOZE:
+                    // Transition from doze to doze suspend can be ignored.
+                    if (displayState == Display.STATE_DOZE_SUSPEND) break;
+                    displayStats.screenDozeTimer.stopRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_DOZE_SUSPEND:
+                    // Transition from doze suspend to doze can be ignored.
+                    if (displayState == Display.STATE_DOZE) break;
+                    displayStats.screenDozeTimer.stopRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_OFF: // fallthrough
+                case Display.STATE_UNKNOWN:
+                    // Not tracked by timers.
                     break;
                 default:
-                    Slog.wtf(TAG, "Unknown screen state (not mapped): " + state);
+                    Slog.wtf(TAG,
+                            "Attempted to stop timer for unexpected display state " + display);
+            }
+
+            // Start timer for new display state.
+            switch (displayState) {
+                case Display.STATE_ON:
+                    displayStats.screenOnTimer.startRunningLocked(elapsedRealtimeMs);
+                    final int bin = displayStats.screenBrightnessBin;
+                    if (bin >= 0) {
+                        displayStats.screenBrightnessTimers[bin].startRunningLocked(
+                                elapsedRealtimeMs);
+                    }
+                    overallBin = evaluateOverallScreenBrightnessBinLocked();
+                    shouldScheduleSync = true;
                     break;
+                case Display.STATE_DOZE:
+                    // Transition from doze suspend to doze can be ignored.
+                    if (oldDisplayState == Display.STATE_DOZE_SUSPEND) break;
+                    displayStats.screenDozeTimer.startRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_DOZE_SUSPEND:
+                    // Transition from doze to doze suspend can be ignored.
+                    if (oldDisplayState == Display.STATE_DOZE) break;
+                    displayStats.screenDozeTimer.startRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_OFF: // fallthrough
+                case Display.STATE_UNKNOWN:
+                    // Not tracked by timers.
+                    break;
+                default:
+                    Slog.wtf(TAG,
+                            "Attempted to start timer for unexpected display state " + displayState
+                                    + " for display " + display);
+            }
+
+            if (shouldScheduleSync
+                    && mGlobalMeasuredEnergyStats != null
+                    && mGlobalMeasuredEnergyStats.isStandardBucketSupported(
+                    MeasuredEnergyStats.POWER_BUCKET_SCREEN_ON)) {
+                // Display measured energy stats is available. Prepare to schedule an
+                // external sync.
+                externalUpdateFlag |= ExternalStatsSync.UPDATE_DISPLAY;
+            }
+
+            // Reevaluate most important display screen state.
+            state = Display.STATE_UNKNOWN;
+            for (int i = 0; i < numDisplay; i++) {
+                final int tempState = mPerDisplayBatteryStats[i].screenState;
+                if (tempState == Display.STATE_ON
+                        || state == Display.STATE_ON) {
+                    state = Display.STATE_ON;
+                } else if (tempState == Display.STATE_DOZE
+                        || state == Display.STATE_DOZE) {
+                    state = Display.STATE_DOZE;
+                } else if (tempState == Display.STATE_DOZE_SUSPEND
+                        || state == Display.STATE_DOZE_SUSPEND) {
+                    state = Display.STATE_DOZE_SUSPEND;
+                } else if (tempState == Display.STATE_OFF
+                        || state == Display.STATE_OFF) {
+                    state = Display.STATE_OFF;
+                }
             }
         }
 
+        final boolean batteryRunning = mOnBatteryTimeBase.isRunning();
+        final boolean batteryScreenOffRunning = mOnBatteryScreenOffTimeBase.isRunning();
+
+        state = mPretendScreenOff ? Display.STATE_OFF : state;
         if (mScreenState != state) {
             recordDailyStatsIfNeededLocked(true, currentTimeMs);
             final int oldState = mScreenState;
@@ -4983,11 +5186,11 @@
                         + Display.stateToString(state));
                 addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
             }
-            // TODO: (Probably overkill) Have mGlobalMeasuredEnergyStats store supported flags and
-            //       only update DISPLAY if it is. Currently overkill since CPU is scheduled anyway.
-            final int updateFlag = ExternalStatsSync.UPDATE_CPU | ExternalStatsSync.UPDATE_DISPLAY;
-            mExternalSync.scheduleSyncDueToScreenStateChange(updateFlag,
-                    mOnBatteryTimeBase.isRunning(), mOnBatteryScreenOffTimeBase.isRunning(), state);
+
+            // Per screen state Cpu stats needed. Prepare to schedule an external sync.
+            externalUpdateFlag |= ExternalStatsSync.UPDATE_CPU;
+            shouldScheduleSync = true;
+
             if (Display.isOnState(state)) {
                 updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), state,
                         uptimeMs * 1000, elapsedRealtimeMs * 1000);
@@ -5005,33 +5208,116 @@
                 updateDischargeScreenLevelsLocked(oldState, state);
             }
         }
+
+        // Changing display states might have changed the screen used to determine the overall
+        // brightness.
+        maybeUpdateOverallScreenBrightness(overallBin, elapsedRealtimeMs, uptimeMs);
+
+        if (shouldScheduleSync) {
+            final int numDisplays = mPerDisplayBatteryStats.length;
+            final int[] displayStates = new int[numDisplays];
+            for (int i = 0; i < numDisplays; i++) {
+                displayStates[i] = mPerDisplayBatteryStats[i].screenState;
+            }
+            mExternalSync.scheduleSyncDueToScreenStateChange(externalUpdateFlag,
+                    batteryRunning, batteryScreenOffRunning, state, displayStates);
+        }
     }
 
     @UnsupportedAppUsage
     public void noteScreenBrightnessLocked(int brightness) {
-        noteScreenBrightnessLocked(brightness, mClocks.elapsedRealtime(), mClocks.uptimeMillis());
+        noteScreenBrightnessLocked(0, brightness);
     }
 
-    public void noteScreenBrightnessLocked(int brightness, long elapsedRealtimeMs, long uptimeMs) {
+    /**
+     * Note screen brightness change for a display.
+     */
+    public void noteScreenBrightnessLocked(int display, int brightness) {
+        noteScreenBrightnessLocked(display, brightness, mClocks.elapsedRealtime(),
+                mClocks.uptimeMillis());
+    }
+
+
+    /**
+     * Note screen brightness change for a display.
+     */
+    public void noteScreenBrightnessLocked(int display, int brightness, long elapsedRealtimeMs,
+            long uptimeMs) {
         // Bin the brightness.
         int bin = brightness / (256/NUM_SCREEN_BRIGHTNESS_BINS);
         if (bin < 0) bin = 0;
         else if (bin >= NUM_SCREEN_BRIGHTNESS_BINS) bin = NUM_SCREEN_BRIGHTNESS_BINS-1;
-        if (mScreenBrightnessBin != bin) {
-            mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_BRIGHTNESS_MASK)
-                    | (bin << HistoryItem.STATE_BRIGHTNESS_SHIFT);
-            if (DEBUG_HISTORY) Slog.v(TAG, "Screen brightness " + bin + " to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+
+        final int overallBin;
+
+        final int numDisplays = mPerDisplayBatteryStats.length;
+        if (display < 0 || display >= numDisplays) {
+            Slog.wtf(TAG, "Unexpected note screen brightness for display " + display + " (only "
+                    + mPerDisplayBatteryStats.length + " displays exist...)");
+            return;
+        }
+
+        final DisplayBatteryStats displayStats = mPerDisplayBatteryStats[display];
+        final int oldBin = displayStats.screenBrightnessBin;
+        if (oldBin == bin) {
+            // Nothing changed
+            overallBin = mScreenBrightnessBin;
+        } else {
+            displayStats.screenBrightnessBin = bin;
+            if (displayStats.screenState == Display.STATE_ON) {
+                if (oldBin >= 0) {
+                    displayStats.screenBrightnessTimers[oldBin].stopRunningLocked(
+                            elapsedRealtimeMs);
+                }
+                displayStats.screenBrightnessTimers[bin].startRunningLocked(
+                        elapsedRealtimeMs);
+            }
+            overallBin = evaluateOverallScreenBrightnessBinLocked();
+        }
+
+        maybeUpdateOverallScreenBrightness(overallBin, elapsedRealtimeMs, uptimeMs);
+    }
+
+    private int evaluateOverallScreenBrightnessBinLocked() {
+        int overallBin = -1;
+        final int numDisplays = getDisplayCount();
+        for (int display = 0; display < numDisplays; display++) {
+            final int displayBrightnessBin;
+            if (mPerDisplayBatteryStats[display].screenState == Display.STATE_ON) {
+                displayBrightnessBin = mPerDisplayBatteryStats[display].screenBrightnessBin;
+            } else {
+                displayBrightnessBin = -1;
+            }
+            if (displayBrightnessBin > overallBin) {
+                overallBin = displayBrightnessBin;
+            }
+        }
+        return overallBin;
+    }
+
+    private void maybeUpdateOverallScreenBrightness(int overallBin, long elapsedRealtimeMs,
+            long uptimeMs) {
+        if (mScreenBrightnessBin != overallBin) {
+            if (overallBin >= 0) {
+                mHistoryCur.states = (mHistoryCur.states & ~HistoryItem.STATE_BRIGHTNESS_MASK)
+                        | (overallBin << HistoryItem.STATE_BRIGHTNESS_SHIFT);
+                if (DEBUG_HISTORY) {
+                    Slog.v(TAG, "Screen brightness " + overallBin + " to: "
+                            + Integer.toHexString(mHistoryCur.states));
+                }
+                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            }
             if (mScreenState == Display.STATE_ON) {
                 if (mScreenBrightnessBin >= 0) {
                     mScreenBrightnessTimer[mScreenBrightnessBin]
                             .stopRunningLocked(elapsedRealtimeMs);
                 }
-                mScreenBrightnessTimer[bin]
-                        .startRunningLocked(elapsedRealtimeMs);
+                if (overallBin >= 0) {
+                    mScreenBrightnessTimer[overallBin]
+                            .startRunningLocked(elapsedRealtimeMs);
+                }
             }
-            mScreenBrightnessBin = bin;
+            mScreenBrightnessBin = overallBin;
         }
     }
 
@@ -6693,6 +6979,31 @@
         return mScreenBrightnessTimer[brightnessBin];
     }
 
+    @Override
+    public int getDisplayCount() {
+        return mPerDisplayBatteryStats.length;
+    }
+
+    @Override
+    public long getDisplayScreenOnTime(int display, long elapsedRealtimeUs) {
+        return mPerDisplayBatteryStats[display].screenOnTimer.getTotalTimeLocked(elapsedRealtimeUs,
+                STATS_SINCE_CHARGED);
+    }
+
+    @Override
+    public long getDisplayScreenDozeTime(int display, long elapsedRealtimeUs) {
+        return mPerDisplayBatteryStats[display].screenDozeTimer.getTotalTimeLocked(
+                elapsedRealtimeUs, STATS_SINCE_CHARGED);
+    }
+
+    @Override
+    public long getDisplayScreenBrightnessTime(int display, int brightnessBin,
+            long elapsedRealtimeUs) {
+        final DisplayBatteryStats displayStats = mPerDisplayBatteryStats[display];
+        return displayStats.screenBrightnessTimers[brightnessBin].getTotalTimeLocked(
+                elapsedRealtimeUs, STATS_SINCE_CHARGED);
+    }
+
     @Override public long getInteractiveTime(long elapsedRealtimeUs, int which) {
         return mInteractiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
     }
@@ -10694,6 +11005,10 @@
             mScreenBrightnessTimer[i] = new StopwatchTimer(mClocks, null, -100-i, null,
                     mOnBatteryTimeBase);
         }
+
+        mPerDisplayBatteryStats = new DisplayBatteryStats[1];
+        mPerDisplayBatteryStats[0] = new DisplayBatteryStats(mClocks, mOnBatteryTimeBase);
+
         mInteractiveTimer = new StopwatchTimer(mClocks, null, -10, null, mOnBatteryTimeBase);
         mPowerSaveModeEnabledTimer = new StopwatchTimer(mClocks, null, -2, null,
                 mOnBatteryTimeBase);
@@ -10806,6 +11121,8 @@
             // Initialize the estimated battery capacity to a known preset one.
             mEstimatedBatteryCapacityMah = (int) mPowerProfile.getBatteryCapacity();
         }
+
+        setDisplayCountLocked(mPowerProfile.getNumDisplays());
     }
 
     PowerProfile getPowerProfile() {
@@ -10838,6 +11155,16 @@
         mExternalSync = sync;
     }
 
+    /**
+     * Initialize and set multi display timers and states.
+     */
+    public void setDisplayCountLocked(int numDisplays) {
+        mPerDisplayBatteryStats = new DisplayBatteryStats[numDisplays];
+        for (int i = 0; i < numDisplays; i++) {
+            mPerDisplayBatteryStats[i] = new DisplayBatteryStats(mClocks, mOnBatteryTimeBase);
+        }
+    }
+
     public void updateDailyDeadlineLocked() {
         // Get the current time.
         long currentTimeMs = mDailyStartTimeMs = mClocks.currentTimeMillis();
@@ -11314,6 +11641,11 @@
             mScreenBrightnessTimer[i].reset(false, elapsedRealtimeUs);
         }
 
+        final int numDisplays = mPerDisplayBatteryStats.length;
+        for (int i = 0; i < numDisplays; i++) {
+            mPerDisplayBatteryStats[i].reset(elapsedRealtimeUs);
+        }
+
         if (mPowerProfile != null) {
             mEstimatedBatteryCapacityMah = (int) mPowerProfile.getBatteryCapacity();
         } else {
@@ -12597,22 +12929,43 @@
      * is always 0 when the screen is not "ON" and whenever the rail energy is 0 (if supported).
      * To the extent that those assumptions are violated, the algorithm will err.
      *
-     * @param chargeUC amount of charge (microcoulombs) used by Display since this was last called.
-     * @param screenState screen state at the time this data collection was scheduled
+     * @param chargesUC amount of charge (microcoulombs) used by each Display since this was last
+     *                 called.
+     * @param screenStates each screen state at the time this data collection was scheduled
      */
     @GuardedBy("this")
-    public void updateDisplayMeasuredEnergyStatsLocked(long chargeUC, int screenState,
+    public void updateDisplayMeasuredEnergyStatsLocked(long[] chargesUC, int[] screenStates,
             long elapsedRealtimeMs) {
-        if (DEBUG_ENERGY) Slog.d(TAG, "Updating display stats: " + chargeUC);
+        if (DEBUG_ENERGY) Slog.d(TAG, "Updating display stats: " + Arrays.toString(chargesUC));
         if (mGlobalMeasuredEnergyStats == null) {
             return;
         }
 
-        final @StandardPowerBucket int powerBucket =
-                MeasuredEnergyStats.getDisplayPowerBucket(mScreenStateAtLastEnergyMeasurement);
-        mScreenStateAtLastEnergyMeasurement = screenState;
+        final int numDisplays;
+        if (mPerDisplayBatteryStats.length == screenStates.length) {
+            numDisplays = screenStates.length;
+        } else {
+            // if this point is reached, it will be reached every display state change.
+            // Rate limit the wtf logging to once every 100 display updates.
+            if (mDisplayMismatchWtfCount++ % 100 == 0) {
+                Slog.wtf(TAG, "Mismatch between PowerProfile reported display count ("
+                        + mPerDisplayBatteryStats.length
+                        + ") and PowerStatsHal reported display count (" + screenStates.length
+                        + ")");
+            }
+            // Keep the show going, use the shorter of the two.
+            numDisplays = mPerDisplayBatteryStats.length < screenStates.length
+                    ? mPerDisplayBatteryStats.length : screenStates.length;
+        }
 
-        if (!mOnBatteryInternal || chargeUC <= 0) {
+        final int[] oldScreenStates = new int[numDisplays];
+        for (int i = 0; i < numDisplays; i++) {
+            final int screenState = screenStates[i];
+            oldScreenStates[i] = mPerDisplayBatteryStats[i].screenStateAtLastEnergyMeasurement;
+            mPerDisplayBatteryStats[i].screenStateAtLastEnergyMeasurement = screenState;
+        }
+
+        if (!mOnBatteryInternal) {
             // There's nothing further to update.
             return;
         }
@@ -12627,17 +12980,31 @@
             return;
         }
 
-        mGlobalMeasuredEnergyStats.updateStandardBucket(powerBucket, chargeUC);
+        long totalScreenOnChargeUC = 0;
+        for (int i = 0; i < numDisplays; i++) {
+            final long chargeUC = chargesUC[i];
+            if (chargeUC <= 0) {
+                // There's nothing further to update.
+                continue;
+            }
+
+            final @StandardPowerBucket int powerBucket =
+                    MeasuredEnergyStats.getDisplayPowerBucket(oldScreenStates[i]);
+            mGlobalMeasuredEnergyStats.updateStandardBucket(powerBucket, chargeUC);
+            if (powerBucket == MeasuredEnergyStats.POWER_BUCKET_SCREEN_ON) {
+                totalScreenOnChargeUC += chargeUC;
+            }
+        }
 
         // Now we blame individual apps, but only if the display was ON.
-        if (powerBucket != MeasuredEnergyStats.POWER_BUCKET_SCREEN_ON) {
+        if (totalScreenOnChargeUC <= 0) {
             return;
         }
         // TODO(b/175726779): Consider unifying the code with the non-rail display power blaming.
 
         // NOTE: fg time is NOT pooled. If two uids are both somehow in fg, then that time is
         // 'double counted' and will simply exceed the realtime that elapsed.
-        // If multidisplay becomes a reality, this is probably more reasonable than pooling.
+        // TODO(b/175726779): collect per display uid visibility for display power attribution.
 
         // Collect total time since mark so that we can normalize power.
         final SparseDoubleArray fgTimeUsArray = new SparseDoubleArray();
@@ -12650,7 +13017,8 @@
             if (fgTimeUs == 0) continue;
             fgTimeUsArray.put(uid.getUid(), (double) fgTimeUs);
         }
-        distributeEnergyToUidsLocked(powerBucket, chargeUC, fgTimeUsArray, 0);
+        distributeEnergyToUidsLocked(MeasuredEnergyStats.POWER_BUCKET_SCREEN_ON,
+                totalScreenOnChargeUC, fgTimeUsArray, 0);
     }
 
     /**
@@ -14556,7 +14924,12 @@
     public void initMeasuredEnergyStatsLocked(@Nullable boolean[] supportedStandardBuckets,
             String[] customBucketNames) {
         boolean supportedBucketMismatch = false;
-        mScreenStateAtLastEnergyMeasurement = mScreenState;
+
+        final int numDisplays = mPerDisplayBatteryStats.length;
+        for (int i = 0; i < numDisplays; i++) {
+            final int screenState = mPerDisplayBatteryStats[i].screenState;
+            mPerDisplayBatteryStats[i].screenStateAtLastEnergyMeasurement = screenState;
+        }
 
         if (supportedStandardBuckets == null) {
             if (mGlobalMeasuredEnergyStats != null) {
diff --git a/core/java/com/android/internal/os/PowerCalculator.java b/core/java/com/android/internal/os/PowerCalculator.java
index 4979ecb..93d562c 100644
--- a/core/java/com/android/internal/os/PowerCalculator.java
+++ b/core/java/com/android/internal/os/PowerCalculator.java
@@ -133,32 +133,6 @@
     }
 
     /**
-     * Returns either the measured energy converted to mAh or a usage-based estimate.
-     */
-    protected static double getMeasuredOrEstimatedPower(@BatteryConsumer.PowerModel int powerModel,
-            long measuredEnergyUC, UsageBasedPowerEstimator powerEstimator, long durationMs) {
-        switch (powerModel) {
-            case BatteryConsumer.POWER_MODEL_MEASURED_ENERGY:
-                return uCtoMah(measuredEnergyUC);
-            case BatteryConsumer.POWER_MODEL_POWER_PROFILE:
-            default:
-                return powerEstimator.calculatePower(durationMs);
-        }
-    }
-
-    /**
-     * Returns either the measured energy converted to mAh or a usage-based estimate.
-     */
-    protected static double getMeasuredOrEstimatedPower(
-            long measuredEnergyUC, UsageBasedPowerEstimator powerEstimator, long durationMs) {
-        if (measuredEnergyUC != BatteryStats.POWER_DATA_UNAVAILABLE) {
-            return uCtoMah(measuredEnergyUC);
-        } else {
-            return powerEstimator.calculatePower(durationMs);
-        }
-    }
-
-    /**
      * Prints formatted amount of power in milli-amp-hours.
      */
     public static void printPowerMah(PrintWriter pw, double powerMah) {
diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java
index add2304..4d19b35 100644
--- a/core/java/com/android/internal/os/PowerProfile.java
+++ b/core/java/com/android/internal/os/PowerProfile.java
@@ -17,10 +17,12 @@
 package com.android.internal.os;
 
 
+import android.annotation.StringDef;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
+import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -30,6 +32,8 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.HashMap;
 
@@ -40,6 +44,8 @@
  */
 public class PowerProfile {
 
+    public static final String TAG = "PowerProfile";
+
     /*
      * POWER_CPU_SUSPEND: Power consumption when CPU is in power collapse mode.
      * POWER_CPU_IDLE: Power consumption when CPU is awake (when a wake lock is held). This should
@@ -145,12 +151,18 @@
 
     /**
      * Power consumption when screen is in doze/ambient/always-on mode, including backlight power.
+     *
+     * @deprecated Use {@link #POWER_GROUP_DISPLAY_AMBIENT} instead.
      */
+    @Deprecated
     public static final String POWER_AMBIENT_DISPLAY = "ambient.on";
 
     /**
      * Power consumption when screen is on, not including the backlight power.
+     *
+     * @deprecated Use {@link #POWER_GROUP_DISPLAY_SCREEN_ON} instead.
      */
+    @Deprecated
     @UnsupportedAppUsage
     public static final String POWER_SCREEN_ON = "screen.on";
 
@@ -175,7 +187,10 @@
     /**
      * Power consumption at full backlight brightness. If the backlight is at
      * 50% brightness, then this should be multiplied by 0.5
+     *
+     * @deprecated Use {@link #POWER_GROUP_DISPLAY_SCREEN_FULL} instead.
      */
+    @Deprecated
     @UnsupportedAppUsage
     public static final String POWER_SCREEN_FULL = "screen.full";
 
@@ -221,6 +236,29 @@
     public static final String POWER_BATTERY_CAPACITY = "battery.capacity";
 
     /**
+     * Power consumption when a screen is in doze/ambient/always-on mode, including backlight power.
+     */
+    public static final String POWER_GROUP_DISPLAY_AMBIENT = "ambient.on.display";
+
+    /**
+     * Power consumption when a screen is on, not including the backlight power.
+     */
+    public static final String POWER_GROUP_DISPLAY_SCREEN_ON = "screen.on.display";
+
+    /**
+     * Power consumption of a screen at full backlight brightness.
+     */
+    public static final String POWER_GROUP_DISPLAY_SCREEN_FULL = "screen.full.display";
+
+    @StringDef(prefix = { "POWER_GROUP_" }, value = {
+            POWER_GROUP_DISPLAY_AMBIENT,
+            POWER_GROUP_DISPLAY_SCREEN_ON,
+            POWER_GROUP_DISPLAY_SCREEN_FULL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PowerGroup {}
+
+    /**
      * A map from Power Use Item to its power consumption.
      */
     static final HashMap<String, Double> sPowerItemMap = new HashMap<>();
@@ -255,6 +293,7 @@
                 readPowerValuesFromXml(context, forTest);
             }
             initCpuClusters();
+            initDisplays();
         }
     }
 
@@ -424,6 +463,58 @@
         return 0;
     }
 
+    private int mNumDisplays;
+
+    private void initDisplays() {
+        // Figure out how many displays are listed in the power profile.
+        mNumDisplays = 0;
+        while (!Double.isNaN(
+                getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, mNumDisplays, Double.NaN))
+                || !Double.isNaN(
+                getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, mNumDisplays, Double.NaN))
+                || !Double.isNaN(
+                getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, mNumDisplays,
+                        Double.NaN))) {
+            mNumDisplays++;
+        }
+
+        // Handle legacy display power constants.
+        final Double deprecatedAmbientDisplay = sPowerItemMap.get(POWER_AMBIENT_DISPLAY);
+        boolean legacy = false;
+        if (deprecatedAmbientDisplay != null && mNumDisplays == 0) {
+            final String key = getOrdinalPowerType(POWER_GROUP_DISPLAY_AMBIENT, 0);
+            Slog.w(TAG, POWER_AMBIENT_DISPLAY + " is deprecated! Use " + key + " instead.");
+            sPowerItemMap.put(key, deprecatedAmbientDisplay);
+            legacy = true;
+        }
+
+        final Double deprecatedScreenOn = sPowerItemMap.get(POWER_SCREEN_ON);
+        if (deprecatedScreenOn != null && mNumDisplays == 0) {
+            final String key = getOrdinalPowerType(POWER_GROUP_DISPLAY_SCREEN_ON, 0);
+            Slog.w(TAG, POWER_SCREEN_ON + " is deprecated! Use " + key + " instead.");
+            sPowerItemMap.put(key, deprecatedScreenOn);
+            legacy = true;
+        }
+
+        final Double deprecatedScreenFull = sPowerItemMap.get(POWER_SCREEN_FULL);
+        if (deprecatedScreenFull != null && mNumDisplays == 0) {
+            final String key = getOrdinalPowerType(POWER_GROUP_DISPLAY_SCREEN_FULL, 0);
+            Slog.w(TAG, POWER_SCREEN_FULL + " is deprecated! Use " + key + " instead.");
+            sPowerItemMap.put(key, deprecatedScreenFull);
+            legacy = true;
+        }
+        if (legacy) {
+            mNumDisplays = 1;
+        }
+    }
+
+    /**
+     * Returns the number built in displays on the device as defined in the power_profile.xml.
+     */
+    public int getNumDisplays() {
+        return mNumDisplays;
+    }
+
     /**
      * Returns the number of memory bandwidth buckets defined in power_profile.xml, or a
      * default value if the subsystem has no recorded value.
@@ -496,6 +587,32 @@
     }
 
     /**
+     * Returns the average current in mA consumed by an ordinaled subsystem, or the given
+     * default value if the subsystem has no recorded value.
+     *
+     * @param group        the subsystem {@link PowerGroup}.
+     * @param ordinal      which entity in the {@link PowerGroup}.
+     * @param defaultValue the value to return if the subsystem has no recorded value.
+     * @return the average current in milliAmps.
+     */
+    public double getAveragePowerForOrdinal(@PowerGroup String group, int ordinal,
+            double defaultValue) {
+        final String type = getOrdinalPowerType(group, ordinal);
+        return getAveragePowerOrDefault(type, defaultValue);
+    }
+
+    /**
+     * Returns the average current in mA consumed by an ordinaled subsystem.
+     *
+     * @param group        the subsystem {@link PowerGroup}.
+     * @param ordinal      which entity in the {@link PowerGroup}.
+     * @return the average current in milliAmps.
+     */
+    public double getAveragePowerForOrdinal(@PowerGroup String group, int ordinal) {
+        return getAveragePowerForOrdinal(group, ordinal, 0);
+    }
+
+    /**
      * Returns the battery capacity, if available, in milli Amp Hours. If not available,
      * it returns zero.
      *
@@ -682,4 +799,9 @@
             }
         }
     }
+
+    // Creates the key for an ordinaled power constant from the group and ordinal.
+    private static String getOrdinalPowerType(@PowerGroup String group, int ordinal) {
+        return group + ordinal;
+    }
 }
diff --git a/core/java/com/android/internal/os/ScreenPowerCalculator.java b/core/java/com/android/internal/os/ScreenPowerCalculator.java
index 1b3bc23..2b63459 100644
--- a/core/java/com/android/internal/os/ScreenPowerCalculator.java
+++ b/core/java/com/android/internal/os/ScreenPowerCalculator.java
@@ -16,6 +16,9 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_ON;
+
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
@@ -41,8 +44,8 @@
     // Minimum amount of time the screen should be on to start smearing drain to apps
     public static final long MIN_ACTIVE_TIME_FOR_SMEARING = 10 * DateUtils.MINUTE_IN_MILLIS;
 
-    private final UsageBasedPowerEstimator mScreenOnPowerEstimator;
-    private final UsageBasedPowerEstimator mScreenFullPowerEstimator;
+    private final UsageBasedPowerEstimator[] mScreenOnPowerEstimators;
+    private final UsageBasedPowerEstimator[] mScreenFullPowerEstimators;
 
     private static class PowerAndDuration {
         public long durationMs;
@@ -50,10 +53,16 @@
     }
 
     public ScreenPowerCalculator(PowerProfile powerProfile) {
-        mScreenOnPowerEstimator = new UsageBasedPowerEstimator(
-                powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_ON));
-        mScreenFullPowerEstimator = new UsageBasedPowerEstimator(
-                powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL));
+        final int numDisplays = powerProfile.getNumDisplays();
+        mScreenOnPowerEstimators = new UsageBasedPowerEstimator[numDisplays];
+        mScreenFullPowerEstimators = new UsageBasedPowerEstimator[numDisplays];
+        for (int display = 0; display < numDisplays; display++) {
+            mScreenOnPowerEstimators[display] = new UsageBasedPowerEstimator(
+                    powerProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, display));
+            mScreenFullPowerEstimators[display] = new UsageBasedPowerEstimator(
+                    powerProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL,
+                            display));
+        }
     }
 
     @Override
@@ -168,7 +177,7 @@
             case BatteryConsumer.POWER_MODEL_POWER_PROFILE:
             default:
                 totalPowerAndDuration.powerMah = calculateTotalPowerFromBrightness(batteryStats,
-                        rawRealtimeUs, statsType, totalPowerAndDuration.durationMs);
+                        rawRealtimeUs);
         }
     }
 
@@ -190,19 +199,25 @@
         return batteryStats.getScreenOnTime(rawRealtimeUs, statsType) / 1000;
     }
 
-    private double calculateTotalPowerFromBrightness(BatteryStats batteryStats, long rawRealtimeUs,
-            int statsType, long durationMs) {
-        double power = mScreenOnPowerEstimator.calculatePower(durationMs);
-        for (int i = 0; i < BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; i++) {
-            final long brightnessTime =
-                    batteryStats.getScreenBrightnessTime(i, rawRealtimeUs, statsType) / 1000;
-            final double binPowerMah = mScreenFullPowerEstimator.calculatePower(brightnessTime)
-                    * (i + 0.5f) / BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS;
-            if (DEBUG && binPowerMah != 0) {
-                Slog.d(TAG, "Screen bin #" + i + ": time=" + brightnessTime
-                        + " power=" + formatCharge(binPowerMah));
+    private double calculateTotalPowerFromBrightness(BatteryStats batteryStats,
+            long rawRealtimeUs) {
+        final int numDisplays = mScreenOnPowerEstimators.length;
+        double power = 0;
+        for (int display = 0; display < numDisplays; display++) {
+            final long displayTime = batteryStats.getDisplayScreenOnTime(display, rawRealtimeUs)
+                    / 1000;
+            power += mScreenOnPowerEstimators[display].calculatePower(displayTime);
+            for (int bin = 0; bin < BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; bin++) {
+                final long brightnessTime = batteryStats.getDisplayScreenBrightnessTime(display,
+                        bin, rawRealtimeUs) / 1000;
+                final double binPowerMah = mScreenFullPowerEstimators[display].calculatePower(
+                        brightnessTime) * (bin + 0.5f) / BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS;
+                if (DEBUG && binPowerMah != 0) {
+                    Slog.d(TAG, "Screen bin #" + bin + ": time=" + brightnessTime
+                            + " power=" + formatCharge(binPowerMah));
+                }
+                power += binPowerMah;
             }
-            power += binPowerMah;
         }
         return power;
     }
diff --git a/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl b/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
index 8e454db..419b1f8 100644
--- a/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
+++ b/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
@@ -20,5 +20,4 @@
     void onSimSecureStateChanged(boolean simSecure);
     void onInputRestrictedStateChanged(boolean inputRestricted);
     void onTrustedChanged(boolean trusted);
-    void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper);
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/policy/ScreenDecorationsUtils.java b/core/java/com/android/internal/policy/ScreenDecorationsUtils.java
index 52172cf..ec62839 100644
--- a/core/java/com/android/internal/policy/ScreenDecorationsUtils.java
+++ b/core/java/com/android/internal/policy/ScreenDecorationsUtils.java
@@ -16,7 +16,9 @@
 
 package com.android.internal.policy;
 
+import android.content.Context;
 import android.content.res.Resources;
+import android.view.RoundedCorners;
 
 import com.android.internal.R;
 
@@ -29,23 +31,28 @@
      * Corner radius that should be used on windows in order to cover the display.
      * These values are expressed in pixels because they should not respect display or font
      * scaling, this means that we don't have to reload them on config changes.
+     *
+     * Note that if the context is not an UI context(not associated with Display), it will use
+     * default display.
      */
-    public static float getWindowCornerRadius(Resources resources) {
+    public static float getWindowCornerRadius(Context context) {
+        final Resources resources = context.getResources();
         if (!supportsRoundedCornersOnWindows(resources)) {
             return 0f;
         }
-
+        // Use Context#getDisplayNoVerify() in case the context is not an UI context.
+        final String displayUniqueId = context.getDisplayNoVerify().getUniqueId();
         // Radius that should be used in case top or bottom aren't defined.
-        float defaultRadius = resources.getDimension(R.dimen.rounded_corner_radius)
-                - resources.getDimension(R.dimen.rounded_corner_radius_adjustment);
+        float defaultRadius = RoundedCorners.getRoundedCornerRadius(resources, displayUniqueId)
+                - RoundedCorners.getRoundedCornerRadiusAdjustment(resources, displayUniqueId);
 
-        float topRadius = resources.getDimension(R.dimen.rounded_corner_radius_top)
-                - resources.getDimension(R.dimen.rounded_corner_radius_top_adjustment);
+        float topRadius = RoundedCorners.getRoundedCornerTopRadius(resources, displayUniqueId)
+                - RoundedCorners.getRoundedCornerRadiusTopAdjustment(resources, displayUniqueId);
         if (topRadius == 0f) {
             topRadius = defaultRadius;
         }
-        float bottomRadius = resources.getDimension(R.dimen.rounded_corner_radius_bottom)
-                - resources.getDimension(R.dimen.rounded_corner_radius_bottom_adjustment);
+        float bottomRadius = RoundedCorners.getRoundedCornerBottomRadius(resources, displayUniqueId)
+                - RoundedCorners.getRoundedCornerRadiusBottomAdjustment(resources, displayUniqueId);
         if (bottomRadius == 0f) {
             bottomRadius = defaultRadius;
         }
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 60a8d80..d3224b1 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -16,16 +16,20 @@
 
 package com.android.internal.policy;
 
+import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
+import static android.view.WindowManager.TRANSIT_OLD_NONE;
 import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN;
+import static android.view.WindowManager.TRANSIT_OPEN;
 
+import android.annotation.DrawableRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -34,12 +38,18 @@
 import android.content.res.ResourceId;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Picture;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.hardware.HardwareBuffer;
 import android.os.SystemProperties;
 import android.util.Slog;
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.TransitionOldType;
+import android.view.WindowManager.TransitionType;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
 import android.view.animation.AnimationSet;
@@ -56,11 +66,17 @@
 
 /** @hide */
 public class TransitionAnimation {
+    public static final int WALLPAPER_TRANSITION_NONE = 0;
+    public static final int WALLPAPER_TRANSITION_OPEN = 1;
+    public static final int WALLPAPER_TRANSITION_CLOSE = 2;
+    public static final int WALLPAPER_TRANSITION_INTRA_OPEN = 3;
+    public static final int WALLPAPER_TRANSITION_INTRA_CLOSE = 4;
+
     // These are the possible states for the enter/exit activities during a thumbnail transition
-    public static final int THUMBNAIL_TRANSITION_ENTER_SCALE_UP = 0;
-    public static final int THUMBNAIL_TRANSITION_EXIT_SCALE_UP = 1;
-    public static final int THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN = 2;
-    public static final int THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN = 3;
+    private static final int THUMBNAIL_TRANSITION_ENTER_SCALE_UP = 0;
+    private static final int THUMBNAIL_TRANSITION_EXIT_SCALE_UP = 1;
+    private static final int THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN = 2;
+    private static final int THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN = 3;
 
     /**
      * Maximum duration for the clip reveal animation. This is used when there is a lot of movement
@@ -72,9 +88,15 @@
 
     public static final int DEFAULT_APP_TRANSITION_DURATION = 336;
 
+    /** Fraction of animation at which the recents thumbnail stays completely transparent */
+    private static final float RECENTS_THUMBNAIL_FADEIN_FRACTION = 0.5f;
     /** Fraction of animation at which the recents thumbnail becomes completely transparent */
     private static final float RECENTS_THUMBNAIL_FADEOUT_FRACTION = 0.5f;
 
+    /** Interpolator to be used for animations that respond directly to a touch */
+    static final Interpolator TOUCH_RESPONSE_INTERPOLATOR =
+            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+
     private static final String DEFAULT_PACKAGE = "android";
 
     private final Context mContext;
@@ -86,7 +108,9 @@
             new PathInterpolator(0.3f, 0f, 0.1f, 1f);
     private final Interpolator mClipHorizontalInterpolator = new PathInterpolator(0, 0, 0.4f, 1f);
     private final Interpolator mDecelerateInterpolator;
+    private final Interpolator mFastOutLinearInInterpolator;
     private final Interpolator mLinearOutSlowInInterpolator;
+    private final Interpolator mThumbnailFadeInInterpolator;
     private final Interpolator mThumbnailFadeOutInterpolator;
     private final Rect mTmpFromClipRect = new Rect();
     private final Rect mTmpToClipRect = new Rect();
@@ -107,8 +131,19 @@
 
         mDecelerateInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.decelerate_cubic);
+        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.fast_out_linear_in);
         mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.linear_out_slow_in);
+        mThumbnailFadeInInterpolator = input -> {
+            // Linear response for first fraction, then complete after that.
+            if (input < RECENTS_THUMBNAIL_FADEIN_FRACTION) {
+                return 0f;
+            }
+            float t = (input - RECENTS_THUMBNAIL_FADEIN_FRACTION)
+                    / (1f - RECENTS_THUMBNAIL_FADEIN_FRACTION);
+            return mFastOutLinearInInterpolator.getInterpolation(t);
+        };
         mThumbnailFadeOutInterpolator = input -> {
             // Linear response for first fraction, then complete after that.
             if (input < RECENTS_THUMBNAIL_FADEOUT_FRACTION) {
@@ -181,6 +216,13 @@
                 DEFAULT_PACKAGE, com.android.internal.R.anim.cross_profile_apps_thumbnail_enter);
     }
 
+    @Nullable
+    public Animation createCrossProfileAppsThumbnailAnimationLocked(Rect appRect) {
+        final Animation animation = loadCrossProfileAppThumbnailEnterAnimation();
+        return prepareThumbnailAnimationWithDuration(animation, appRect.width(),
+                appRect.height(), 0, null);
+    }
+
     /** Load animation by resource Id from specific package. */
     @Nullable
     public Animation loadAnimationRes(String packageName, int resId) {
@@ -347,8 +389,15 @@
         }
     }
 
-    public Animation createClipRevealAnimationLocked(int transit, boolean enter, Rect appFrame,
-            Rect displayFrame, Rect startRect) {
+    public Animation createClipRevealAnimationLocked(@TransitionType int transit,
+            int wallpaperTransit, boolean enter, Rect appFrame, Rect displayFrame, Rect startRect) {
+        return createClipRevealAnimationLockedCompat(
+                getTransitCompatType(transit, wallpaperTransit), enter, appFrame, displayFrame,
+                startRect);
+    }
+
+    public Animation createClipRevealAnimationLockedCompat(@TransitionOldType int transit,
+            boolean enter, Rect appFrame, Rect displayFrame, Rect startRect) {
         final Animation anim;
         if (enter) {
             final int appWidth = appFrame.width();
@@ -458,8 +507,14 @@
         return anim;
     }
 
-    public Animation createScaleUpAnimationLocked(int transit, boolean enter,
-            Rect containingFrame, Rect startRect) {
+    public Animation createScaleUpAnimationLocked(@TransitionType int transit, int wallpaperTransit,
+            boolean enter, Rect containingFrame, Rect startRect) {
+        return createScaleUpAnimationLockedCompat(getTransitCompatType(transit, wallpaperTransit),
+                enter, containingFrame, startRect);
+    }
+
+    public Animation createScaleUpAnimationLockedCompat(@TransitionOldType int transit,
+            boolean enter, Rect containingFrame, Rect startRect) {
         Animation a;
         setupDefaultNextAppTransitionStartRect(startRect, mTmpRect);
         final int appWidth = containingFrame.width();
@@ -514,12 +569,19 @@
         return a;
     }
 
+    public Animation createThumbnailEnterExitAnimationLocked(boolean enter, boolean scaleUp,
+            Rect containingFrame, @TransitionType int transit, int wallpaperTransit,
+            HardwareBuffer thumbnailHeader, Rect startRect) {
+        return createThumbnailEnterExitAnimationLockedCompat(enter, scaleUp, containingFrame,
+                getTransitCompatType(transit, wallpaperTransit), thumbnailHeader, startRect);
+    }
+
     /**
      * This animation is created when we are doing a thumbnail transition, for the activity that is
      * leaving, and the activity that is entering.
      */
-    public Animation createThumbnailEnterExitAnimationLocked(int thumbTransitState,
-            Rect containingFrame, int transit, HardwareBuffer thumbnailHeader,
+    public Animation createThumbnailEnterExitAnimationLockedCompat(boolean enter, boolean scaleUp,
+            Rect containingFrame, @TransitionOldType int transit, HardwareBuffer thumbnailHeader,
             Rect startRect) {
         final int appWidth = containingFrame.width();
         final int appHeight = containingFrame.height();
@@ -529,6 +591,7 @@
         final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1;
         final int thumbHeightI = thumbnailHeader != null ? thumbnailHeader.getHeight() : appHeight;
         final float thumbHeight = thumbHeightI > 0 ? thumbHeightI : 1;
+        final int thumbTransitState = getThumbnailTransitionState(enter, scaleUp);
 
         switch (thumbTransitState) {
             case THUMBNAIL_TRANSITION_ENTER_SCALE_UP: {
@@ -587,8 +650,8 @@
      * This alternate animation is created when we are doing a thumbnail transition, for the
      * activity that is leaving, and the activity that is entering.
      */
-    public Animation createAspectScaledThumbnailEnterExitAnimationLocked(int thumbTransitState,
-            int orientation, int transit, Rect containingFrame, Rect contentInsets,
+    public Animation createAspectScaledThumbnailEnterExitAnimationLocked(boolean enter,
+            boolean scaleUp, int orientation, int transit, Rect containingFrame, Rect contentInsets,
             @Nullable Rect surfaceInsets, @Nullable Rect stableInsets, boolean freeform,
             Rect startRect, Rect defaultStartRect) {
         Animation a;
@@ -601,11 +664,11 @@
         final float thumbHeight = thumbHeightI > 0 ? thumbHeightI : 1;
         final int thumbStartX = mTmpRect.left - containingFrame.left - contentInsets.left;
         final int thumbStartY = mTmpRect.top - containingFrame.top;
+        final int thumbTransitState = getThumbnailTransitionState(enter, scaleUp);
 
         switch (thumbTransitState) {
             case THUMBNAIL_TRANSITION_ENTER_SCALE_UP:
             case THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN: {
-                final boolean scaleUp = thumbTransitState == THUMBNAIL_TRANSITION_ENTER_SCALE_UP;
                 if (freeform && scaleUp) {
                     a = createAspectScaledThumbnailEnterFreeformAnimationLocked(
                             containingFrame, surfaceInsets, startRect, defaultStartRect);
@@ -720,10 +783,151 @@
     }
 
     /**
+     * This animation runs for the thumbnail that gets cross faded with the enter/exit activity
+     * when a thumbnail is specified with the pending animation override.
+     */
+    public Animation createThumbnailAspectScaleAnimationLocked(Rect appRect,
+            @Nullable Rect contentInsets, HardwareBuffer thumbnailHeader, int orientation,
+            Rect startRect, Rect defaultStartRect, boolean scaleUp) {
+        Animation a;
+        final int thumbWidthI = thumbnailHeader.getWidth();
+        final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1;
+        final int thumbHeightI = thumbnailHeader.getHeight();
+        final int appWidth = appRect.width();
+
+        float scaleW = appWidth / thumbWidth;
+        getNextAppTransitionStartRect(startRect, defaultStartRect, mTmpRect);
+        final float fromX;
+        float fromY;
+        final float toX;
+        float toY;
+        final float pivotX;
+        final float pivotY;
+        if (shouldScaleDownThumbnailTransition(orientation)) {
+            fromX = mTmpRect.left;
+            fromY = mTmpRect.top;
+
+            // For the curved translate animation to work, the pivot points needs to be at the
+            // same absolute position as the one from the real surface.
+            toX = mTmpRect.width() / 2 * (scaleW - 1f) + appRect.left;
+            toY = appRect.height() / 2 * (1 - 1 / scaleW) + appRect.top;
+            pivotX = mTmpRect.width() / 2;
+            pivotY = appRect.height() / 2 / scaleW;
+            if (mGridLayoutRecentsEnabled) {
+                // In the grid layout, the header is displayed above the thumbnail instead of
+                // overlapping it.
+                fromY -= thumbHeightI;
+                toY -= thumbHeightI * scaleW;
+            }
+        } else {
+            pivotX = 0;
+            pivotY = 0;
+            fromX = mTmpRect.left;
+            fromY = mTmpRect.top;
+            toX = appRect.left;
+            toY = appRect.top;
+        }
+        if (scaleUp) {
+            // Animation up from the thumbnail to the full screen
+            Animation scale = new ScaleAnimation(1f, scaleW, 1f, scaleW, pivotX, pivotY);
+            scale.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+            scale.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
+            Animation alpha = new AlphaAnimation(1f, 0f);
+            alpha.setInterpolator(mThumbnailFadeOutInterpolator);
+            alpha.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
+            Animation translate = createCurvedMotion(fromX, toX, fromY, toY);
+            translate.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+            translate.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
+
+            mTmpFromClipRect.set(0, 0, thumbWidthI, thumbHeightI);
+            mTmpToClipRect.set(appRect);
+
+            // Containing frame is in screen space, but we need the clip rect in the
+            // app space.
+            mTmpToClipRect.offsetTo(0, 0);
+            mTmpToClipRect.right = (int) (mTmpToClipRect.right / scaleW);
+            mTmpToClipRect.bottom = (int) (mTmpToClipRect.bottom / scaleW);
+
+            if (contentInsets != null) {
+                mTmpToClipRect.inset((int) (-contentInsets.left * scaleW),
+                        (int) (-contentInsets.top * scaleW),
+                        (int) (-contentInsets.right * scaleW),
+                        (int) (-contentInsets.bottom * scaleW));
+            }
+
+            Animation clipAnim = new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect);
+            clipAnim.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+            clipAnim.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
+
+            // This AnimationSet uses the Interpolators assigned above.
+            AnimationSet set = new AnimationSet(false);
+            set.addAnimation(scale);
+            if (!mGridLayoutRecentsEnabled) {
+                // In the grid layout, the header should be shown for the whole animation.
+                set.addAnimation(alpha);
+            }
+            set.addAnimation(translate);
+            set.addAnimation(clipAnim);
+            a = set;
+        } else {
+            // Animation down from the full screen to the thumbnail
+            Animation scale = new ScaleAnimation(scaleW, 1f, scaleW, 1f, pivotX, pivotY);
+            scale.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+            scale.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
+            Animation alpha = new AlphaAnimation(0f, 1f);
+            alpha.setInterpolator(mThumbnailFadeInInterpolator);
+            alpha.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
+            Animation translate = createCurvedMotion(toX, fromX, toY, fromY);
+            translate.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+            translate.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
+
+            // This AnimationSet uses the Interpolators assigned above.
+            AnimationSet set = new AnimationSet(false);
+            set.addAnimation(scale);
+            if (!mGridLayoutRecentsEnabled) {
+                // In the grid layout, the header should be shown for the whole animation.
+                set.addAnimation(alpha);
+            }
+            set.addAnimation(translate);
+            a = set;
+
+        }
+        return prepareThumbnailAnimationWithDuration(a, appWidth, appRect.height(), 0,
+                null);
+    }
+
+    /**
+     * Creates an overlay with a background color and a thumbnail for the cross profile apps
+     * animation.
+     */
+    public HardwareBuffer createCrossProfileAppsThumbnail(
+            @DrawableRes int thumbnailDrawableRes, Rect frame) {
+        final int width = frame.width();
+        final int height = frame.height();
+
+        final Picture picture = new Picture();
+        final Canvas canvas = picture.beginRecording(width, height);
+        canvas.drawColor(Color.argb(0.6f, 0, 0, 0));
+        final int thumbnailSize = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.cross_profile_apps_thumbnail_size);
+        final Drawable drawable = mContext.getDrawable(thumbnailDrawableRes);
+        drawable.setBounds(
+                (width - thumbnailSize) / 2,
+                (height - thumbnailSize) / 2,
+                (width + thumbnailSize) / 2,
+                (height + thumbnailSize) / 2);
+        drawable.setTint(mContext.getColor(android.R.color.white));
+        drawable.draw(canvas);
+        picture.endRecording();
+
+        return Bitmap.createBitmap(picture).getHardwareBuffer();
+    }
+
+    /**
      * Prepares the specified animation with a standard duration, interpolator, etc.
      */
     private Animation prepareThumbnailAnimation(Animation a, int appWidth, int appHeight,
-            int transit) {
+            @TransitionOldType int transit) {
         // Pick the desired duration.  If this is an inter-activity transition,
         // it  is the standard duration for that.  Otherwise we use the longer
         // task transition duration.
@@ -820,6 +1024,22 @@
         return anim;
     }
 
+    private static @TransitionOldType int getTransitCompatType(@TransitionType int transit,
+            int wallpaperTransit) {
+        if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) {
+            return TRANSIT_OLD_WALLPAPER_INTRA_OPEN;
+        } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) {
+            return TRANSIT_OLD_WALLPAPER_INTRA_CLOSE;
+        } else if (transit == TRANSIT_OPEN) {
+            return TRANSIT_OLD_ACTIVITY_OPEN;
+        } else if (transit == TRANSIT_CLOSE) {
+            return TRANSIT_OLD_ACTIVITY_CLOSE;
+        }
+
+        // We only do some special handle for above type, so use type NONE for default behavior.
+        return TRANSIT_OLD_NONE;
+    }
+
     /**
      * Calculates the duration for the clip reveal animation. If the clip is "cut off", meaning that
      * the start rect is outside of the target rect, and there is a lot of movement going on.
@@ -843,10 +1063,33 @@
     }
 
     /**
+     * Return the current thumbnail transition state.
+     */
+    private int getThumbnailTransitionState(boolean enter, boolean scaleUp) {
+        if (enter) {
+            if (scaleUp) {
+                return THUMBNAIL_TRANSITION_ENTER_SCALE_UP;
+            } else {
+                return THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN;
+            }
+        } else {
+            if (scaleUp) {
+                return THUMBNAIL_TRANSITION_EXIT_SCALE_UP;
+            } else {
+                return THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN;
+            }
+        }
+    }
+
+    /**
      * Prepares the specified animation with a standard duration, interpolator, etc.
      */
-    private static Animation prepareThumbnailAnimationWithDuration(Animation a, int appWidth,
+    public static Animation prepareThumbnailAnimationWithDuration(Animation a, int appWidth,
             int appHeight, long duration, Interpolator interpolator) {
+        if (a == null) {
+            return null;
+        }
+
         if (duration > 0) {
             a.setDuration(duration);
         }
diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java
index ce3efd3..954204f 100644
--- a/core/java/com/android/internal/protolog/ProtoLogGroup.java
+++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java
@@ -80,6 +80,11 @@
             Consts.TAG_WM),
     WM_DEBUG_WINDOW_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
             Consts.TAG_WM),
+    WM_DEBUG_WINDOW_INSETS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+            Consts.TAG_WM),
+    WM_DEBUG_LAYER_MIRRORING(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+            Consts.TAG_WM),
+    WM_DEBUG_WALLPAPER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM),
     TEST_GROUP(true, true, false, "WindowManagerProtoLogTest");
 
     private final boolean mEnabled;
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 10224a4..353c6c0 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -28,7 +28,7 @@
  */
 public class ProtoLogImpl extends BaseProtoLogImpl {
     private static final int BUFFER_CAPACITY = 1024 * 1024;
-    private static final String LOG_FILENAME = "/data/misc/wmtrace/wm_log.pb";
+    private static final String LOG_FILENAME = "/data/misc/wmtrace/wm_log.winscope";
     private static final String VIEWER_CONFIG_FILENAME = "/system/etc/protolog.conf.json.gz";
 
     private static ProtoLogImpl sServiceInstance = null;
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index ed6415d..b427e8b 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -25,6 +25,7 @@
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.service.notification.StatusBarNotification;
+import android.view.InsetsVisibilities;
 
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.view.AppearanceRegion;
@@ -182,7 +183,7 @@
     /**
      * Notifies System UI side of system bar attribute change on the specified display.
      *
-     * @param displayId the ID of the display to notify
+     * @param displayId the ID of the display to notify.
      * @param appearance the appearance of the focused window. The light top bar appearance is not
      *                   controlled here, but primaryAppearance and secondaryAppearance.
      * @param appearanceRegions a set of appearances which will be only applied in their own bounds.
@@ -191,11 +192,12 @@
      *                         stacks.
      * @param navbarColorManagedByIme {@code true} if navigation bar color is managed by IME.
      * @param behavior the behavior of the focused window.
-     * @param isFullscreen whether any of status or navigation bar is requested invisible.
+     * @param requestedVisibilities the collection of the requested visibilities of system insets.
+     * @param packageName the package name of the focused app.
      */
     void onSystemBarAttributesChanged(int displayId, int appearance,
             in AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            int behavior, boolean isFullscreen);
+            int behavior, in InsetsVisibilities requestedVisibilities, String packageName);
 
     /**
      * Notifies System UI to show transient bars. The transient bars are system bars, e.g., status
diff --git a/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java b/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
index 8fb2f9c..4dcc82e 100644
--- a/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
+++ b/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
@@ -21,6 +21,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArrayMap;
+import android.view.InsetsVisibilities;
 
 import com.android.internal.view.AppearanceRegion;
 
@@ -39,14 +40,15 @@
     public final IBinder mImeToken;
     public final boolean mNavbarColorManagedByIme;
     public final int mBehavior;
-    public final boolean mAppFullscreen;
+    public final InsetsVisibilities mRequestedVisibilities;
+    public final String mPackageName;
     public final int[] mTransientBarTypes;
 
     public RegisterStatusBarResult(ArrayMap<String, StatusBarIcon> icons, int disabledFlags1,
             int appearance, AppearanceRegion[] appearanceRegions, int imeWindowVis,
             int imeBackDisposition, boolean showImeSwitcher, int disabledFlags2, IBinder imeToken,
-            boolean navbarColorManagedByIme, int behavior, boolean appFullscreen,
-            @NonNull int[] transientBarTypes) {
+            boolean navbarColorManagedByIme, int behavior, InsetsVisibilities requestedVisibilities,
+            String packageName, @NonNull int[] transientBarTypes) {
         mIcons = new ArrayMap<>(icons);
         mDisabledFlags1 = disabledFlags1;
         mAppearance = appearance;
@@ -58,7 +60,8 @@
         mImeToken = imeToken;
         mNavbarColorManagedByIme = navbarColorManagedByIme;
         mBehavior = behavior;
-        mAppFullscreen = appFullscreen;
+        mRequestedVisibilities = requestedVisibilities;
+        mPackageName = packageName;
         mTransientBarTypes = transientBarTypes;
     }
 
@@ -80,7 +83,8 @@
         dest.writeStrongBinder(mImeToken);
         dest.writeBoolean(mNavbarColorManagedByIme);
         dest.writeInt(mBehavior);
-        dest.writeBoolean(mAppFullscreen);
+        dest.writeTypedObject(mRequestedVisibilities, 0);
+        dest.writeString(mPackageName);
         dest.writeIntArray(mTransientBarTypes);
     }
 
@@ -104,12 +108,14 @@
                     final IBinder imeToken = source.readStrongBinder();
                     final boolean navbarColorManagedByIme = source.readBoolean();
                     final int behavior = source.readInt();
-                    final boolean appFullscreen = source.readBoolean();
+                    final InsetsVisibilities requestedVisibilities =
+                            source.readTypedObject(InsetsVisibilities.CREATOR);
+                    final String packageName = source.readString();
                     final int[] transientBarTypes = source.createIntArray();
                     return new RegisterStatusBarResult(icons, disabledFlags1, appearance,
                             appearanceRegions, imeWindowVis, imeBackDisposition, showImeSwitcher,
                             disabledFlags2, imeToken, navbarColorManagedByIme, behavior,
-                            appFullscreen, transientBarTypes);
+                            requestedVisibilities, packageName, transientBarTypes);
                 }
 
                 @Override
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index f040462..4c519f4 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -14,15 +14,20 @@
 
 package com.android.internal.util;
 
+import static android.os.Trace.TRACE_TAG_APP;
+
 import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.os.Build;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.provider.DeviceConfig;
+import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
-import android.util.SparseLongArray;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.EventLogTags;
@@ -31,6 +36,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Class to track various latencies in SystemUI. It then writes the latency to statsd and also
@@ -44,6 +50,7 @@
     private static final String TAG = "LatencyTracker";
     private static final String SETTINGS_ENABLED_KEY = "enabled";
     private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
+    private static final boolean DEBUG = false;
     /** Default to being enabled on debug builds. */
     private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE;
     /** Default to collecting data for 1/5 of all actions (randomly sampled). */
@@ -110,6 +117,11 @@
      */
     public static final int ACTION_LOCKSCREEN_UNLOCK = 11;
 
+    /**
+     * Time it takes to switch users.
+     */
+    public static final int ACTION_USER_SWITCH = 12;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -122,7 +134,8 @@
         ACTION_START_RECENTS_ANIMATION,
         ACTION_ROTATE_SCREEN_SENSOR,
         ACTION_ROTATE_SCREEN_CAMERA_CHECK,
-        ACTION_LOCKSCREEN_UNLOCK
+        ACTION_LOCKSCREEN_UNLOCK,
+        ACTION_USER_SWITCH
     };
 
     /** @hide */
@@ -138,7 +151,8 @@
         ACTION_START_RECENTS_ANIMATION,
         ACTION_ROTATE_SCREEN_SENSOR,
         ACTION_ROTATE_SCREEN_CAMERA_CHECK,
-        ACTION_LOCKSCREEN_UNLOCK
+        ACTION_LOCKSCREEN_UNLOCK,
+        ACTION_USER_SWITCH
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -156,13 +170,15 @@
             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION,
             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR,
             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_CAMERA_CHECK,
-            FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK
+            FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK,
+            FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_USER_SWITCH
     };
 
     private static LatencyTracker sLatencyTracker;
 
     private final Object mLock = new Object();
-    private final SparseLongArray mStartRtc = new SparseLongArray();
+    @GuardedBy("mLock")
+    private final SparseArray<Session> mSessions = new SparseArray<>();
     @GuardedBy("mLock")
     private final int[] mTraceThresholdPerAction = new int[ACTIONS_ALL.length];
     @GuardedBy("mLock")
@@ -239,13 +255,19 @@
                 return "ACTION_ROTATE_SCREEN_SENSOR";
             case 12:
                 return "ACTION_LOCKSCREEN_UNLOCK";
+            case 13:
+                return "ACTION_USER_SWITCH";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
     }
 
-    private static String getTraceNameOfAction(@Action int action) {
-        return "L<" + getNameOfAction(STATSD_ACTION[action]) + ">";
+    private static String getTraceNameOfAction(@Action int action, String tag) {
+        if (TextUtils.isEmpty(tag)) {
+            return "L<" + getNameOfAction(STATSD_ACTION[action]) + ">";
+        } else {
+            return "L<" + getNameOfAction(STATSD_ACTION[action]) + "::" + tag + ">";
+        }
     }
 
     private static String getTraceTriggerNameForAction(@Action int action) {
@@ -263,35 +285,82 @@
     }
 
     /**
-     * Notifies that an action is starting. This needs to be called from the main thread.
+     * Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
      *
      * @param action The action to start. One of the ACTION_* values.
      */
     public void onActionStart(@Action int action) {
-        if (!isEnabled()) {
-            return;
-        }
-        Trace.asyncTraceBegin(Trace.TRACE_TAG_APP, getTraceNameOfAction(action), 0);
-        mStartRtc.put(action, SystemClock.elapsedRealtime());
+        onActionStart(action, null);
     }
 
     /**
-     * Notifies that an action has ended. This needs to be called from the main thread.
+     * Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
+     *
+     * @param action The action to start. One of the ACTION_* values.
+     * @param tag The brief description of the action.
+     */
+    public void onActionStart(@Action int action, String tag) {
+        synchronized (mLock) {
+            if (!isEnabled()) {
+                return;
+            }
+            // skip if the action is already instrumenting.
+            if (mSessions.get(action) != null) {
+                return;
+            }
+            Session session = new Session(action, tag);
+            session.begin(() -> onActionCancel(action));
+            mSessions.put(action, session);
+
+            if (DEBUG) {
+                Log.d(TAG, "onActionStart: " + session.name() + ", start=" + session.mStartRtc);
+            }
+        }
+    }
+
+    /**
+     * Notifies that an action has ended. <s>This needs to be called from the main thread.</s>
      *
      * @param action The action to end. One of the ACTION_* values.
      */
     public void onActionEnd(@Action int action) {
-        if (!isEnabled()) {
-            return;
+        synchronized (mLock) {
+            if (!isEnabled()) {
+                return;
+            }
+            Session session = mSessions.get(action);
+            if (session == null) {
+                return;
+            }
+            session.end();
+            mSessions.delete(action);
+            logAction(action, session.duration());
+
+            if (DEBUG) {
+                Log.d(TAG, "onActionEnd:" + session.name() + ", duration=" + session.duration());
+            }
         }
-        long endRtc = SystemClock.elapsedRealtime();
-        long startRtc = mStartRtc.get(action, -1);
-        if (startRtc == -1) {
-            return;
+    }
+
+    /**
+     * Notifies that an action has canceled. <s>This needs to be called from the main thread.</s>
+     *
+     * @param action The action to cancel. One of the ACTION_* values.
+     * @hide
+     */
+    public void onActionCancel(@Action int action) {
+        synchronized (mLock) {
+            Session session = mSessions.get(action);
+            if (session == null) {
+                return;
+            }
+            session.cancel();
+            mSessions.delete(action);
+
+            if (DEBUG) {
+                Log.d(TAG, "onActionCancel: " + session.name());
+            }
         }
-        mStartRtc.delete(action);
-        Trace.asyncTraceEnd(Trace.TRACE_TAG_APP, getTraceNameOfAction(action), 0);
-        logAction(action, (int) (endRtc - startRtc));
     }
 
     /**
@@ -332,4 +401,57 @@
                     FrameworkStatsLog.UI_ACTION_LATENCY_REPORTED, STATSD_ACTION[action], duration);
         }
     }
+
+    static class Session {
+        @Action
+        private final int mAction;
+        private final String mTag;
+        private final String mName;
+        private Runnable mTimeoutRunnable;
+        private long mStartRtc = -1;
+        private long mEndRtc = -1;
+
+        Session(@Action int action, @Nullable String tag) {
+            mAction = action;
+            mTag = tag;
+            mName = TextUtils.isEmpty(mTag)
+                    ? getNameOfAction(STATSD_ACTION[mAction])
+                    : getNameOfAction(STATSD_ACTION[mAction]) + "::" + mTag;
+        }
+
+        String name() {
+            return mName;
+        }
+
+        String traceName() {
+            return getTraceNameOfAction(mAction, mTag);
+        }
+
+        void begin(@NonNull Runnable timeoutAction) {
+            mStartRtc = SystemClock.elapsedRealtime();
+            Trace.asyncTraceBegin(TRACE_TAG_APP, traceName(), 0);
+
+            // start counting timeout.
+            mTimeoutRunnable = timeoutAction;
+            BackgroundThread.getHandler()
+                    .postDelayed(mTimeoutRunnable, TimeUnit.SECONDS.toMillis(15));
+        }
+
+        void end() {
+            mEndRtc = SystemClock.elapsedRealtime();
+            Trace.asyncTraceEnd(TRACE_TAG_APP, traceName(), 0);
+            BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
+            mTimeoutRunnable = null;
+        }
+
+        void cancel() {
+            Trace.asyncTraceEnd(TRACE_TAG_APP, traceName(), 0);
+            BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
+            mTimeoutRunnable = null;
+        }
+
+        int duration() {
+            return (int) (mEndRtc - mStartRtc);
+        }
+    }
 }
diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl
index 8d82e33..5354afb 100644
--- a/core/java/com/android/internal/view/IInputMethod.aidl
+++ b/core/java/com/android/internal/view/IInputMethod.aidl
@@ -35,7 +35,7 @@
  * {@hide}
  */
 oneway interface IInputMethod {
-    void initializeInternal(IBinder token, int displayId, IInputMethodPrivilegedOperations privOps,
+    void initializeInternal(IBinder token, IInputMethodPrivilegedOperations privOps,
              int configChanges);
 
     void onCreateInlineSuggestionsRequest(in InlineSuggestionsRequestInfo requestInfo,
diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java
index e3a9fda..72b5488 100644
--- a/core/java/com/android/internal/view/ScrollCaptureInternal.java
+++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java
@@ -25,6 +25,7 @@
 import android.view.ScrollCaptureCallback;
 import android.view.View;
 import android.view.ViewGroup;
+import android.webkit.WebView;
 import android.widget.ListView;
 
 /**
@@ -43,7 +44,7 @@
     private static final int DOWN = 1;
 
     /**
-     * Not a ViewGroup, or cannot scroll according to View APIs.
+     * Cannot scroll according to {@link View#canScrollVertically}.
      */
     public static final int TYPE_FIXED = 0;
 
@@ -60,7 +61,7 @@
     public static final int TYPE_RECYCLING = 2;
 
     /**
-     * The ViewGroup scrolls, but has no child views in
+     * Unknown scrollable view with no child views (or not a subclass of ViewGroup).
      */
     private static final int TYPE_OPAQUE = 3;
 
@@ -73,16 +74,6 @@
      * as excluded during scroll capture search.
      */
     private static int detectScrollingType(View view) {
-        // Must be a ViewGroup
-        if (!(view instanceof ViewGroup)) {
-            if (DEBUG_VERBOSE) {
-                Log.v(TAG, "hint: not a subclass of ViewGroup");
-            }
-            return TYPE_FIXED;
-        }
-        if (DEBUG_VERBOSE) {
-            Log.v(TAG, "hint: is a subclass of ViewGroup");
-        }
         // Confirm that it can scroll.
         if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) {
             // Nothing to scroll here, move along.
@@ -94,6 +85,17 @@
         if (DEBUG_VERBOSE) {
             Log.v(TAG, "hint: can be scrolled up or down");
         }
+        // Must be a ViewGroup
+        if (!(view instanceof ViewGroup)) {
+            if (DEBUG_VERBOSE) {
+                Log.v(TAG, "hint: not a subclass of ViewGroup");
+            }
+            return TYPE_OPAQUE;
+        }
+        if (DEBUG_VERBOSE) {
+            Log.v(TAG, "hint: is a subclass of ViewGroup");
+        }
+
         // ScrollViews accept only a single child.
         if (((ViewGroup) view).getChildCount() > 1) {
             if (DEBUG_VERBOSE) {
@@ -188,6 +190,18 @@
                 }
                 return new ScrollCaptureViewSupport<>((ViewGroup) view,
                         new RecyclerViewCaptureHelper());
+            case TYPE_OPAQUE:
+                if (DEBUG) {
+                    Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName()
+                            + "[" + resolveId(view.getContext(), view.getId()) + "]"
+                            + " -> TYPE_OPAQUE");
+                }
+                if (view instanceof WebView) {
+                    Log.d(TAG, "scroll capture: Using WebView support");
+                    return new ScrollCaptureViewSupport<>((WebView) view,
+                            new WebViewCaptureHelper());
+                }
+                break;
             case TYPE_FIXED:
                 // ignore
                 break;
diff --git a/core/java/com/android/internal/view/WebViewCaptureHelper.java b/core/java/com/android/internal/view/WebViewCaptureHelper.java
new file mode 100644
index 0000000..e6a311c
--- /dev/null
+++ b/core/java/com/android/internal/view/WebViewCaptureHelper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.view;
+
+import static android.util.MathUtils.constrain;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.webkit.WebView;
+
+/**
+ * ScrollCapture for WebView.
+ */
+class WebViewCaptureHelper implements ScrollCaptureViewHelper<WebView> {
+    private static final String TAG = "WebViewScrollCapture";
+
+    private final Rect mRequestWebViewLocal = new Rect();
+    private final Rect mWebViewBounds = new Rect();
+
+    private int mOriginScrollY;
+    private int mOriginScrollX;
+
+    @Override
+    public boolean onAcceptSession(@NonNull WebView view) {
+        return view.isVisibleToUser()
+                && (view.getContentHeight() * view.getScale()) > view.getHeight();
+    }
+
+    @Override
+    public void onPrepareForStart(@NonNull WebView view, @NonNull Rect scrollBounds) {
+        mOriginScrollX = view.getScrollX();
+        mOriginScrollY = view.getScrollY();
+    }
+
+    @NonNull
+    @Override
+    public ScrollResult onScrollRequested(@NonNull WebView view, @NonNull Rect scrollBounds,
+            @NonNull Rect requestRect) {
+
+        int scrollDelta = view.getScrollY() - mOriginScrollY;
+
+        ScrollResult result = new ScrollResult();
+        result.requestedArea = new Rect(requestRect);
+        result.availableArea = new Rect();
+        result.scrollDelta = scrollDelta;
+
+        mWebViewBounds.set(0, 0, view.getWidth(), view.getHeight());
+
+        if (!view.isVisibleToUser()) {
+            return result;
+        }
+
+        // Map the request into local coordinates
+        mRequestWebViewLocal.set(requestRect);
+        mRequestWebViewLocal.offset(0, -scrollDelta);
+
+        // Offset to center the rect vertically, clamp to available content
+        int upLimit = min(0, -view.getScrollY());
+        int contentHeightPx = (int) (view.getContentHeight() * view.getScale());
+        int downLimit = max(0, (contentHeightPx - view.getHeight()) - view.getScrollY());
+        int scrollToCenter = mRequestWebViewLocal.centerY() - mWebViewBounds.centerY();
+        int scrollMovement = constrain(scrollToCenter, upLimit, downLimit);
+
+        // Scroll and update relative based on  the new position
+        view.scrollBy(mOriginScrollX, scrollMovement);
+        scrollDelta = view.getScrollY() - mOriginScrollY;
+        mRequestWebViewLocal.offset(0, -scrollMovement);
+        result.scrollDelta = scrollDelta;
+
+        if (mRequestWebViewLocal.intersect(mWebViewBounds)) {
+            result.availableArea = new Rect(mRequestWebViewLocal);
+            result.availableArea.offset(0, result.scrollDelta);
+        }
+        return result;
+    }
+
+    @Override
+    public void onPrepareForEnd(@NonNull WebView view) {
+        view.scrollTo(mOriginScrollX, mOriginScrollY);
+    }
+
+}
+
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index fd6038f..4fc135c 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -19,6 +19,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.graphics.Canvas;
+import android.graphics.Insets;
 import android.graphics.Paint;
 import android.graphics.Paint.FontMetricsInt;
 import android.graphics.Path;
@@ -136,6 +137,7 @@
     private final FontMetricsInt mTextMetrics = new FontMetricsInt();
     private int mHeaderBottom;
     private int mHeaderPaddingTop = 0;
+    private Insets mWaterfallInsets = Insets.NONE;
     @UnsupportedAppUsage
     private boolean mCurDown;
     @UnsupportedAppUsage
@@ -229,8 +231,10 @@
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         if (insets.getDisplayCutout() != null) {
             mHeaderPaddingTop = insets.getDisplayCutout().getSafeInsetTop();
+            mWaterfallInsets = insets.getDisplayCutout().getWaterfallInsets();
         } else {
             mHeaderPaddingTop = 0;
+            mWaterfallInsets = Insets.NONE;
         }
         return super.onApplyWindowInsets(insets);
     }
@@ -266,11 +270,6 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        final int w = getWidth();
-        final int itemW = w/7;
-        final int base = mHeaderPaddingTop-mTextMetrics.ascent+1;
-        final int bottom = mHeaderBottom;
-
         final int NP = mPointers.size();
 
         if (!mSystemGestureExclusion.isEmpty()) {
@@ -286,71 +285,7 @@
         }
 
         // Labels
-        if (mActivePointerId >= 0) {
-            final PointerState ps = mPointers.get(mActivePointerId);
-            
-            canvas.drawRect(0, mHeaderPaddingTop, itemW-1, bottom,mTextBackgroundPaint);
-            canvas.drawText(mText.clear()
-                    .append("P: ").append(mCurNumPointers)
-                    .append(" / ").append(mMaxNumPointers)
-                    .toString(), 1, base, mTextPaint);
-
-            final int N = ps.mTraceCount;
-            if ((mCurDown && ps.mCurDown) || N == 0) {
-                canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
-                        mTextBackgroundPaint);
-                canvas.drawText(mText.clear()
-                        .append("X: ").append(ps.mCoords.x, 1)
-                        .toString(), 1 + itemW, base, mTextPaint);
-                canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
-                        mTextBackgroundPaint);
-                canvas.drawText(mText.clear()
-                        .append("Y: ").append(ps.mCoords.y, 1)
-                        .toString(), 1 + itemW * 2, base, mTextPaint);
-            } else {
-                float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
-                float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
-                canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
-                        Math.abs(dx) < mVC.getScaledTouchSlop()
-                        ? mTextBackgroundPaint : mTextLevelPaint);
-                canvas.drawText(mText.clear()
-                        .append("dX: ").append(dx, 1)
-                        .toString(), 1 + itemW, base, mTextPaint);
-                canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
-                        Math.abs(dy) < mVC.getScaledTouchSlop()
-                        ? mTextBackgroundPaint : mTextLevelPaint);
-                canvas.drawText(mText.clear()
-                        .append("dY: ").append(dy, 1)
-                        .toString(), 1 + itemW * 2, base, mTextPaint);
-            }
-
-            canvas.drawRect(itemW * 3, mHeaderPaddingTop, (itemW * 4) - 1, bottom,
-                    mTextBackgroundPaint);
-            canvas.drawText(mText.clear()
-                    .append("Xv: ").append(ps.mXVelocity, 3)
-                    .toString(), 1 + itemW * 3, base, mTextPaint);
-
-            canvas.drawRect(itemW * 4, mHeaderPaddingTop, (itemW * 5) - 1, bottom,
-                    mTextBackgroundPaint);
-            canvas.drawText(mText.clear()
-                    .append("Yv: ").append(ps.mYVelocity, 3)
-                    .toString(), 1 + itemW * 4, base, mTextPaint);
-
-            canvas.drawRect(itemW * 5, mHeaderPaddingTop, (itemW * 6) - 1, bottom,
-                    mTextBackgroundPaint);
-            canvas.drawRect(itemW * 5, mHeaderPaddingTop,
-                    (itemW * 5) + (ps.mCoords.pressure * itemW) - 1, bottom, mTextLevelPaint);
-            canvas.drawText(mText.clear()
-                    .append("Prs: ").append(ps.mCoords.pressure, 2)
-                    .toString(), 1 + itemW * 5, base, mTextPaint);
-
-            canvas.drawRect(itemW * 6, mHeaderPaddingTop, w, bottom, mTextBackgroundPaint);
-            canvas.drawRect(itemW * 6, mHeaderPaddingTop,
-                    (itemW * 6) + (ps.mCoords.size * itemW) - 1, bottom, mTextLevelPaint);
-            canvas.drawText(mText.clear()
-                    .append("Size: ").append(ps.mCoords.size, 2)
-                    .toString(), 1 + itemW * 6, base, mTextPaint);
-        }
+        drawLabels(canvas);
 
         // Pointer trace.
         for (int p = 0; p < NP; p++) {
@@ -463,6 +398,84 @@
         }
     }
 
+    private void drawLabels(Canvas canvas) {
+        if (mActivePointerId < 0) {
+            return;
+        }
+
+        final int w = getWidth() - mWaterfallInsets.left - mWaterfallInsets.right;
+        final int itemW = w / 7;
+        final int base = mHeaderPaddingTop - mTextMetrics.ascent + 1;
+        final int bottom = mHeaderBottom;
+
+        canvas.save();
+        canvas.translate(mWaterfallInsets.left, 0);
+        final PointerState ps = mPointers.get(mActivePointerId);
+
+        canvas.drawRect(0, mHeaderPaddingTop, itemW - 1, bottom, mTextBackgroundPaint);
+        canvas.drawText(mText.clear()
+                .append("P: ").append(mCurNumPointers)
+                .append(" / ").append(mMaxNumPointers)
+                .toString(), 1, base, mTextPaint);
+
+        final int count = ps.mTraceCount;
+        if ((mCurDown && ps.mCurDown) || count == 0) {
+            canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
+                    mTextBackgroundPaint);
+            canvas.drawText(mText.clear()
+                    .append("X: ").append(ps.mCoords.x, 1)
+                    .toString(), 1 + itemW, base, mTextPaint);
+            canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
+                    mTextBackgroundPaint);
+            canvas.drawText(mText.clear()
+                    .append("Y: ").append(ps.mCoords.y, 1)
+                    .toString(), 1 + itemW * 2, base, mTextPaint);
+        } else {
+            float dx = ps.mTraceX[count - 1] - ps.mTraceX[0];
+            float dy = ps.mTraceY[count - 1] - ps.mTraceY[0];
+            canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
+                    Math.abs(dx) < mVC.getScaledTouchSlop()
+                            ? mTextBackgroundPaint : mTextLevelPaint);
+            canvas.drawText(mText.clear()
+                    .append("dX: ").append(dx, 1)
+                    .toString(), 1 + itemW, base, mTextPaint);
+            canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
+                    Math.abs(dy) < mVC.getScaledTouchSlop()
+                            ? mTextBackgroundPaint : mTextLevelPaint);
+            canvas.drawText(mText.clear()
+                    .append("dY: ").append(dy, 1)
+                    .toString(), 1 + itemW * 2, base, mTextPaint);
+        }
+
+        canvas.drawRect(itemW * 3, mHeaderPaddingTop, (itemW * 4) - 1, bottom,
+                mTextBackgroundPaint);
+        canvas.drawText(mText.clear()
+                .append("Xv: ").append(ps.mXVelocity, 3)
+                .toString(), 1 + itemW * 3, base, mTextPaint);
+
+        canvas.drawRect(itemW * 4, mHeaderPaddingTop, (itemW * 5) - 1, bottom,
+                mTextBackgroundPaint);
+        canvas.drawText(mText.clear()
+                .append("Yv: ").append(ps.mYVelocity, 3)
+                .toString(), 1 + itemW * 4, base, mTextPaint);
+
+        canvas.drawRect(itemW * 5, mHeaderPaddingTop, (itemW * 6) - 1, bottom,
+                mTextBackgroundPaint);
+        canvas.drawRect(itemW * 5, mHeaderPaddingTop,
+                (itemW * 5) + (ps.mCoords.pressure * itemW) - 1, bottom, mTextLevelPaint);
+        canvas.drawText(mText.clear()
+                .append("Prs: ").append(ps.mCoords.pressure, 2)
+                .toString(), 1 + itemW * 5, base, mTextPaint);
+
+        canvas.drawRect(itemW * 6, mHeaderPaddingTop, w, bottom, mTextBackgroundPaint);
+        canvas.drawRect(itemW * 6, mHeaderPaddingTop,
+                (itemW * 6) + (ps.mCoords.size * itemW) - 1, bottom, mTextLevelPaint);
+        canvas.drawText(mText.clear()
+                .append("Size: ").append(ps.mCoords.size, 2)
+                .toString(), 1 + itemW * 6, base, mTextPaint);
+        canvas.restore();
+    }
+
     private void logMotionEvent(String type, MotionEvent event) {
         final int action = event.getAction();
         final int N = event.getHistorySize();
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 2cfec4b..49066e3 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -224,6 +224,7 @@
                 "fd_utils.cpp",
                 "android_hardware_input_InputWindowHandle.cpp",
                 "android_hardware_input_InputApplicationHandle.cpp",
+                "android_window_WindowInfosListener.cpp",
             ],
 
             static_libs: [
@@ -231,15 +232,19 @@
                 "libbinderthreadstateutils",
                 "libdmabufinfo",
                 "libgif",
+                "libgui_window_info_static",
                 "libseccomp_policy",
                 "libgrallocusage",
                 "libscrypt_static",
                 "libstatssocket_lazy",
+                "libskia",
             ],
 
             shared_libs: [
                 "audioclient-types-aidl-cpp",
                 "audioflinger-aidl-cpp",
+                "audiopolicy-types-aidl-cpp",
+                "spatializer-aidl-cpp",
                 "av-types-aidl-cpp",
                 "android.hardware.camera.device@3.2",
                 "libandroidicu",
@@ -371,6 +376,7 @@
                 "libinput",
                 "libbinderthreadstateutils",
                 "libsqlite",
+                "libgui_window_info_static",
             ],
             shared_libs: [
                 // libbinder needs to be shared since it has global state
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 97cac29..aa9995d 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -208,6 +208,7 @@
 extern int register_com_android_internal_os_ZygoteInit(JNIEnv *env);
 extern int register_com_android_internal_security_VerityUtils(JNIEnv* env);
 extern int register_com_android_internal_util_VirtualRefBasePtr(JNIEnv *env);
+extern int register_android_window_WindowInfosListener(JNIEnv* env);
 
 // Namespace for Android Runtime flags applied during boot time.
 static const char* RUNTIME_NATIVE_BOOT_NAMESPACE = "runtime_native_boot";
@@ -1654,6 +1655,8 @@
         REG_JNI(register_com_android_internal_os_KernelCpuUidBpfMapReader),
         REG_JNI(register_com_android_internal_os_KernelSingleProcessCpuThreadReader),
         REG_JNI(register_com_android_internal_os_KernelSingleUidTimeReader),
+
+        REG_JNI(register_android_window_WindowInfosListener),
 };
 
 /*
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index da89663..17b627f 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -22,6 +22,7 @@
 # WindowManager
 per-file android_graphics_BLASTBufferQueue.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 per-file android_view_Surface* = file:/services/core/java/com/android/server/wm/OWNERS
+per-file android_window_WindowInfosListener.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 
 # Resources
 per-file android_content_res_* = file:/core/java/android/content/res/OWNERS
diff --git a/core/jni/android_hardware_input_InputApplicationHandle.cpp b/core/jni/android_hardware_input_InputApplicationHandle.cpp
index 995bfa9..24d3531 100644
--- a/core/jni/android_hardware_input_InputApplicationHandle.cpp
+++ b/core/jni/android_hardware_input_InputApplicationHandle.cpp
@@ -28,6 +28,8 @@
 namespace android {
 
 static struct {
+    jclass clazz;
+    jmethodID ctor;
     jfieldID ptr;
     jfieldID name;
     jfieldID dispatchingTimeoutMillis;
@@ -101,6 +103,15 @@
     return *handle;
 }
 
+jobject android_view_InputApplicationHandle_fromInputApplicationInfo(
+        JNIEnv* env, gui::InputApplicationInfo inputApplicationInfo) {
+    jobject binderObject = javaObjectForIBinder(env, inputApplicationInfo.token);
+    ScopedLocalRef<jstring> name(env, env->NewStringUTF(inputApplicationInfo.name.data()));
+    return env->NewObject(gInputApplicationHandleClassInfo.clazz,
+                          gInputApplicationHandleClassInfo.ctor, binderObject, name.get(),
+                          inputApplicationInfo.dispatchingTimeoutMillis);
+}
+
 // --- JNI ---
 
 static void android_view_InputApplicationHandle_nativeDispose(JNIEnv* env, jobject obj) {
@@ -131,6 +142,10 @@
         var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
         LOG_FATAL_IF(! (var), "Unable to find field " fieldName);
 
+#define GET_METHOD_ID(var, clazz, methodName, methodSignature)  \
+    var = env->GetMethodID(clazz, methodName, methodSignature); \
+    LOG_ALWAYS_FATAL_IF(!(var), "Unable to find method " methodName);
+
 int register_android_view_InputApplicationHandle(JNIEnv* env) {
     int res = jniRegisterNativeMethods(env, "android/view/InputApplicationHandle",
             gInputApplicationHandleMethods, NELEM(gInputApplicationHandleMethods));
@@ -139,6 +154,10 @@
 
     jclass clazz;
     FIND_CLASS(clazz, "android/view/InputApplicationHandle");
+    gInputApplicationHandleClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+
+    GET_METHOD_ID(gInputApplicationHandleClassInfo.ctor, clazz, "<init>",
+                  "(Landroid/os/IBinder;Ljava/lang/String;J)V");
 
     GET_FIELD_ID(gInputApplicationHandleClassInfo.ptr, clazz,
             "ptr", "J");
diff --git a/core/jni/android_hardware_input_InputApplicationHandle.h b/core/jni/android_hardware_input_InputApplicationHandle.h
index ec99d6d..5d88d8e 100644
--- a/core/jni/android_hardware_input_InputApplicationHandle.h
+++ b/core/jni/android_hardware_input_InputApplicationHandle.h
@@ -19,7 +19,7 @@
 
 #include <string>
 
-#include <input/InputApplication.h>
+#include <gui/InputApplication.h>
 
 #include <nativehelper/JNIHelp.h>
 #include "jni.h"
@@ -42,6 +42,9 @@
 extern std::shared_ptr<InputApplicationHandle> android_view_InputApplicationHandle_getHandle(
         JNIEnv* env, jobject inputApplicationHandleObj);
 
+extern jobject android_view_InputApplicationHandle_fromInputApplicationInfo(
+        JNIEnv* env, gui::InputApplicationInfo inputApplicationInfo);
+
 } // namespace android
 
 #endif // _ANDROID_VIEW_INPUT_APPLICATION_HANDLE_H
diff --git a/core/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp
index 463d909..e4ef7d3 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.cpp
+++ b/core/jni/android_hardware_input_InputWindowHandle.cpp
@@ -26,14 +26,19 @@
 #include <ui/Region.h>
 #include <utils/threads.h>
 
+#include <android/graphics/matrix.h>
+#include <gui/WindowInfo.h>
+#include "SkRegion.h"
 #include "android_hardware_input_InputApplicationHandle.h"
 #include "android_util_Binder.h"
 #include "core_jni_helpers.h"
-#include "input/InputWindow.h"
 #include "jni.h"
 
 namespace android {
 
+using gui::TouchOcclusionMode;
+using gui::WindowInfo;
+
 struct WeakRefHandleField {
     jfieldID ctrl;
     jmethodID get;
@@ -41,6 +46,8 @@
 };
 
 static struct {
+    jclass clazz;
+    jmethodID ctor;
     jfieldID ptr;
     jfieldID inputApplicationHandle;
     jfieldID token;
@@ -66,11 +73,18 @@
     jfieldID packageName;
     jfieldID inputFeatures;
     jfieldID displayId;
-    jfieldID portalToDisplayId;
     jfieldID replaceTouchableRegionWithCrop;
     WeakRefHandleField touchableRegionSurfaceControl;
+    jfieldID transform;
+    jfieldID windowToken;
 } gInputWindowHandleClassInfo;
 
+static struct {
+    jclass clazz;
+    jmethodID ctor;
+    jfieldID nativeRegion;
+} gRegionClassInfo;
+
 static Mutex gHandleMutex;
 
 
@@ -115,9 +129,9 @@
 
     mInfo.name = getStringField(env, obj, gInputWindowHandleClassInfo.name, "<null>");
 
-    mInfo.flags = Flags<InputWindowInfo::Flag>(
+    mInfo.flags = Flags<WindowInfo::Flag>(
             env->GetIntField(obj, gInputWindowHandleClassInfo.layoutParamsFlags));
-    mInfo.type = static_cast<InputWindowInfo::Type>(
+    mInfo.type = static_cast<WindowInfo::Type>(
             env->GetIntField(obj, gInputWindowHandleClassInfo.layoutParamsType));
     mInfo.dispatchingTimeout = std::chrono::milliseconds(
             env->GetLongField(obj, gInputWindowHandleClassInfo.dispatchingTimeoutMillis));
@@ -159,12 +173,10 @@
     mInfo.ownerUid = env->GetIntField(obj,
             gInputWindowHandleClassInfo.ownerUid);
     mInfo.packageName = getStringField(env, obj, gInputWindowHandleClassInfo.packageName, "<null>");
-    mInfo.inputFeatures = static_cast<InputWindowInfo::Feature>(
+    mInfo.inputFeatures = static_cast<WindowInfo::Feature>(
             env->GetIntField(obj, gInputWindowHandleClassInfo.inputFeatures));
     mInfo.displayId = env->GetIntField(obj,
             gInputWindowHandleClassInfo.displayId);
-    mInfo.portalToDisplayId = env->GetIntField(obj,
-            gInputWindowHandleClassInfo.portalToDisplayId);
 
     jobject inputApplicationHandleObj = env->GetObjectField(obj,
             gInputWindowHandleClassInfo.inputApplicationHandle);
@@ -204,6 +216,14 @@
         mInfo.touchableRegionCropHandle.clear();
     }
 
+    jobject windowTokenObj = env->GetObjectField(obj, gInputWindowHandleClassInfo.windowToken);
+    if (windowTokenObj) {
+        mInfo.windowToken = ibinderForJavaObject(env, windowTokenObj);
+        env->DeleteLocalRef(windowTokenObj);
+    } else {
+        mInfo.windowToken.clear();
+    }
+
     env->DeleteLocalRef(obj);
     return true;
 }
@@ -233,6 +253,81 @@
     return handle;
 }
 
+jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, gui::WindowInfo windowInfo) {
+    ScopedLocalRef<jobject>
+            applicationHandle(env,
+                              android_view_InputApplicationHandle_fromInputApplicationInfo(
+                                      env, windowInfo.applicationInfo));
+
+    jobject inputWindowHandle =
+            env->NewObject(gInputWindowHandleClassInfo.clazz, gInputWindowHandleClassInfo.ctor,
+                           applicationHandle.get(), windowInfo.displayId);
+    env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.token,
+                        javaObjectForIBinder(env, windowInfo.token));
+    env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.name,
+                        env->NewStringUTF(windowInfo.name.data()));
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.layoutParamsFlags,
+                     static_cast<uint32_t>(windowInfo.flags.get()));
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.layoutParamsType,
+                     static_cast<int32_t>(windowInfo.type));
+    env->SetLongField(inputWindowHandle, gInputWindowHandleClassInfo.dispatchingTimeoutMillis,
+                      std::chrono::duration_cast<std::chrono::milliseconds>(
+                              windowInfo.dispatchingTimeout)
+                              .count());
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.frameLeft,
+                     windowInfo.frameLeft);
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.frameTop, windowInfo.frameTop);
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.frameRight,
+                     windowInfo.frameRight);
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.frameBottom,
+                     windowInfo.frameBottom);
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.surfaceInset,
+                     windowInfo.surfaceInset);
+    env->SetFloatField(inputWindowHandle, gInputWindowHandleClassInfo.scaleFactor,
+                       windowInfo.globalScaleFactor);
+
+    SkRegion* region = new SkRegion();
+    for (const auto& r : windowInfo.touchableRegion) {
+        region->op({r.left, r.top, r.right, r.bottom}, SkRegion::kUnion_Op);
+    }
+    ScopedLocalRef<jobject> regionObj(env,
+                                      env->NewObject(gRegionClassInfo.clazz,
+                                                     gRegionClassInfo.ctor));
+    env->SetLongField(regionObj.get(), gRegionClassInfo.nativeRegion,
+                      reinterpret_cast<jlong>(region));
+    env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.touchableRegion,
+                        regionObj.get());
+
+    env->SetBooleanField(inputWindowHandle, gInputWindowHandleClassInfo.visible,
+                         windowInfo.visible);
+    env->SetBooleanField(inputWindowHandle, gInputWindowHandleClassInfo.focusable,
+                         windowInfo.focusable);
+    env->SetBooleanField(inputWindowHandle, gInputWindowHandleClassInfo.hasWallpaper,
+                         windowInfo.hasWallpaper);
+    env->SetBooleanField(inputWindowHandle, gInputWindowHandleClassInfo.paused, windowInfo.paused);
+    env->SetBooleanField(inputWindowHandle, gInputWindowHandleClassInfo.trustedOverlay,
+                         windowInfo.trustedOverlay);
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.touchOcclusionMode,
+                     static_cast<int32_t>(windowInfo.touchOcclusionMode));
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.ownerPid, windowInfo.ownerPid);
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.ownerUid, windowInfo.ownerUid);
+    env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.packageName,
+                        env->NewStringUTF(windowInfo.packageName.data()));
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.inputFeatures,
+                     static_cast<int32_t>(windowInfo.inputFeatures.get()));
+
+    float transformVals[9];
+    for (int i = 0; i < 9; i++) {
+        transformVals[i] = windowInfo.transform[i % 3][i / 3];
+    }
+    ScopedLocalRef<jobject> matrixObj(env, AMatrix_newInstance(env, transformVals));
+    env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.transform, matrixObj.get());
+
+    env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.windowToken,
+                        javaObjectForIBinder(env, windowInfo.windowToken));
+
+    return inputWindowHandle;
+}
 
 // --- JNI ---
 
@@ -275,6 +370,10 @@
 
     jclass clazz;
     FIND_CLASS(clazz, "android/view/InputWindowHandle");
+    gInputWindowHandleClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+
+    GET_METHOD_ID(gInputWindowHandleClassInfo.ctor, clazz, "<init>",
+                  "(Landroid/view/InputApplicationHandle;I)V");
 
     GET_FIELD_ID(gInputWindowHandleClassInfo.ptr, clazz,
             "ptr", "J");
@@ -348,12 +447,15 @@
     GET_FIELD_ID(gInputWindowHandleClassInfo.displayId, clazz,
             "displayId", "I");
 
-    GET_FIELD_ID(gInputWindowHandleClassInfo.portalToDisplayId, clazz,
-            "portalToDisplayId", "I");
-
     GET_FIELD_ID(gInputWindowHandleClassInfo.replaceTouchableRegionWithCrop, clazz,
             "replaceTouchableRegionWithCrop", "Z");
 
+    GET_FIELD_ID(gInputWindowHandleClassInfo.transform, clazz, "transform",
+                 "Landroid/graphics/Matrix;");
+
+    GET_FIELD_ID(gInputWindowHandleClassInfo.windowToken, clazz, "windowToken",
+                 "Landroid/os/IBinder;");
+
     jclass weakRefClazz;
     FIND_CLASS(weakRefClazz, "java/lang/ref/Reference");
 
@@ -368,6 +470,11 @@
     GET_FIELD_ID(gInputWindowHandleClassInfo.touchableRegionSurfaceControl.mNativeObject,
         surfaceControlClazz, "mNativeObject", "J");
 
+    jclass regionClazz;
+    FIND_CLASS(regionClazz, "android/graphics/Region");
+    gRegionClassInfo.clazz = MakeGlobalRefOrDie(env, regionClazz);
+    GET_METHOD_ID(gRegionClassInfo.ctor, gRegionClassInfo.clazz, "<init>", "()V");
+    GET_FIELD_ID(gRegionClassInfo.nativeRegion, gRegionClassInfo.clazz, "mNativeRegion", "J");
     return 0;
 }
 
diff --git a/core/jni/android_hardware_input_InputWindowHandle.h b/core/jni/android_hardware_input_InputWindowHandle.h
index de5bd6e..408e0f1 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.h
+++ b/core/jni/android_hardware_input_InputWindowHandle.h
@@ -17,14 +17,14 @@
 #ifndef _ANDROID_VIEW_INPUT_WINDOW_HANDLE_H
 #define _ANDROID_VIEW_INPUT_WINDOW_HANDLE_H
 
-#include <input/InputWindow.h>
+#include <gui/WindowInfo.h>
 
 #include <nativehelper/JNIHelp.h>
 #include "jni.h"
 
 namespace android {
 
-class NativeInputWindowHandle : public InputWindowHandle {
+class NativeInputWindowHandle : public gui::WindowInfoHandle {
 public:
     NativeInputWindowHandle(jweak objWeak);
     virtual ~NativeInputWindowHandle();
@@ -37,10 +37,12 @@
     jweak mObjWeak;
 };
 
-
 extern sp<NativeInputWindowHandle> android_view_InputWindowHandle_getHandle(
         JNIEnv* env, jobject inputWindowHandleObj);
 
+extern jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env,
+                                                             gui::WindowInfo windowInfo);
+
 } // namespace android
 
 #endif // _ANDROID_VIEW_INPUT_WINDOW_HANDLE_H
diff --git a/core/jni/android_media_AudioDeviceAttributes.cpp b/core/jni/android_media_AudioDeviceAttributes.cpp
index 2a16dce..6879a60 100644
--- a/core/jni/android_media_AudioDeviceAttributes.cpp
+++ b/core/jni/android_media_AudioDeviceAttributes.cpp
@@ -24,6 +24,11 @@
 
 static jclass gAudioDeviceAttributesClass;
 static jmethodID gAudioDeviceAttributesCstor;
+static struct {
+    jfieldID mAddress;
+    jfieldID mNativeType;
+    // other fields unused by JNI
+} gAudioDeviceAttributesFields;
 
 namespace android {
 
@@ -33,12 +38,25 @@
     jint jNativeType = (jint)devTypeAddr->mType;
     ScopedLocalRef<jstring> jAddress(env, env->NewStringUTF(devTypeAddr->getAddress()));
 
-    *jAudioDeviceAttributes = env->NewObject(gAudioDeviceAttributesClass, gAudioDeviceAttributesCstor,
-            jNativeType, jAddress.get());
+    *jAudioDeviceAttributes =
+        env->NewObject(gAudioDeviceAttributesClass, gAudioDeviceAttributesCstor,
+                       jNativeType, jAddress.get());
 
     return jStatus;
 }
 
+jint createAudioDeviceTypeAddrFromJava(JNIEnv *env, AudioDeviceTypeAddr *devTypeAddr,
+                                       const jobject jAudioDeviceAttributes) {
+    devTypeAddr->mType = (audio_devices_t)env->GetIntField(jAudioDeviceAttributes,
+                         gAudioDeviceAttributesFields.mNativeType);
+
+    jstring jAddress = (jstring)env->GetObjectField(jAudioDeviceAttributes,
+                       gAudioDeviceAttributesFields.mAddress);
+    devTypeAddr->setAddress(ScopedUtfChars(env, jAddress).c_str());
+
+    return AUDIO_JAVA_SUCCESS;
+}
+
 } // namespace android
 
 int register_android_media_AudioDeviceAttributes(JNIEnv *env) {
@@ -48,5 +66,10 @@
     gAudioDeviceAttributesCstor =
             GetMethodIDOrDie(env, audioDeviceTypeAddressClass, "<init>", "(ILjava/lang/String;)V");
 
+    gAudioDeviceAttributesFields.mNativeType =
+            GetFieldIDOrDie(env, gAudioDeviceAttributesClass, "mNativeType", "I");
+    gAudioDeviceAttributesFields.mAddress =
+            GetFieldIDOrDie(env, gAudioDeviceAttributesClass, "mAddress", "Ljava/lang/String;");
+
     return 0;
 }
diff --git a/core/jni/android_media_AudioDeviceAttributes.h b/core/jni/android_media_AudioDeviceAttributes.h
index b49d9ba..4a1f40d 100644
--- a/core/jni/android_media_AudioDeviceAttributes.h
+++ b/core/jni/android_media_AudioDeviceAttributes.h
@@ -28,6 +28,9 @@
 
 extern jint createAudioDeviceAttributesFromNative(JNIEnv *env, jobject *jAudioDeviceAttributes,
                                         const AudioDeviceTypeAddr *devTypeAddr);
+
+extern jint createAudioDeviceTypeAddrFromJava(JNIEnv *env, AudioDeviceTypeAddr *devTypeAddr,
+                                        const jobject jAudioDeviceAttributes);
 } // namespace android
 
 #endif
\ No newline at end of file
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 4b93363..509b7ad 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -27,6 +27,8 @@
 #include "core_jni_helpers.h"
 
 #include <android/media/AudioVibratorInfo.h>
+#include <android/media/INativeSpatializerCallback.h>
+#include <android/media/ISpatializer.h>
 #include <audiomanager/AudioManager.h>
 #include <media/AudioPolicy.h>
 #include <media/AudioSystem.h>
@@ -207,6 +209,7 @@
     jmethodID getId;
     jmethodID getResonantFrequency;
     jmethodID getQFactor;
+    jmethodID getMaxAmplitude;
 } gVibratorMethods;
 
 static Mutex gLock;
@@ -2022,6 +2025,18 @@
     AudioSystem::setRoutingCallback(android_media_AudioSystem_routing_callback);
 }
 
+void javaAudioFormatToNativeAudioConfig(JNIEnv *env, audio_config_t *nConfig,
+                                       const jobject jFormat, bool isInput) {
+    *nConfig = AUDIO_CONFIG_INITIALIZER;
+    nConfig->format = audioFormatToNative(env->GetIntField(jFormat, gAudioFormatFields.mEncoding));
+    nConfig->sample_rate = env->GetIntField(jFormat, gAudioFormatFields.mSampleRate);
+    jint jChannelMask = env->GetIntField(jFormat, gAudioFormatFields.mChannelMask);
+    if (isInput) {
+        nConfig->channel_mask = inChannelMaskToNative(jChannelMask);
+    } else {
+        nConfig->channel_mask = outChannelMaskToNative(jChannelMask);
+    }
+}
 
 static jint convertAudioMixToNative(JNIEnv *env,
                                     AudioMix *nAudioMix,
@@ -2042,13 +2057,7 @@
     nAudioMix->mCbFlags = env->GetIntField(jAudioMix, gAudioMixFields.mCallbackFlags);
 
     jobject jFormat = env->GetObjectField(jAudioMix, gAudioMixFields.mFormat);
-    nAudioMix->mFormat = AUDIO_CONFIG_INITIALIZER;
-    nAudioMix->mFormat.sample_rate = env->GetIntField(jFormat,
-                                                     gAudioFormatFields.mSampleRate);
-    nAudioMix->mFormat.channel_mask = outChannelMaskToNative(env->GetIntField(jFormat,
-                                                     gAudioFormatFields.mChannelMask));
-    nAudioMix->mFormat.format = audioFormatToNative(env->GetIntField(jFormat,
-                                                     gAudioFormatFields.mEncoding));
+    javaAudioFormatToNativeAudioConfig(env, &nAudioMix->mFormat, jFormat, false /*isInput*/);
     env->DeleteLocalRef(jFormat);
 
     jobject jRule = env->GetObjectField(jAudioMix, gAudioMixFields.mRule);
@@ -2704,11 +2713,65 @@
         vibratorInfo.resonantFrequency =
                 env->CallFloatMethod(jVibrator.get(), gVibratorMethods.getResonantFrequency);
         vibratorInfo.qFactor = env->CallFloatMethod(jVibrator.get(), gVibratorMethods.getQFactor);
+        vibratorInfo.maxAmplitude =
+                env->CallFloatMethod(jVibrator.get(), gVibratorMethods.getMaxAmplitude);
         vibratorInfos.push_back(vibratorInfo);
     }
     return (jint)check_AudioSystem_Command(AudioSystem::setVibratorInfos(vibratorInfos));
 }
 
+static jobject android_media_AudioSystem_getSpatializer(JNIEnv *env, jobject thiz,
+                                                       jobject jISpatializerCallback) {
+    sp<media::INativeSpatializerCallback> nISpatializerCallback
+            = interface_cast<media::INativeSpatializerCallback>(
+                    ibinderForJavaObject(env, jISpatializerCallback));
+    sp<media::ISpatializer> nSpatializer;
+    status_t status = AudioSystem::getSpatializer(nISpatializerCallback,
+                                        &nSpatializer);
+    if (status != NO_ERROR) {
+        return nullptr;
+    }
+    return javaObjectForIBinder(env, IInterface::asBinder(nSpatializer));
+}
+
+static jboolean android_media_AudioSystem_canBeSpatialized(JNIEnv *env, jobject thiz,
+                                                       jobject jaa, jobject jFormat,
+                                                       jobjectArray jDeviceArray) {
+    JNIAudioAttributeHelper::UniqueAaPtr paa = JNIAudioAttributeHelper::makeUnique();
+    jint jStatus = JNIAudioAttributeHelper::nativeFromJava(env, jaa, paa.get());
+    if (jStatus != (jint)AUDIO_JAVA_SUCCESS) {
+       return false;
+    }
+
+    AudioDeviceTypeAddrVector nDevices;
+
+    const size_t numDevices = env->GetArrayLength(jDeviceArray);
+    for (size_t i = 0;  i < numDevices; ++i) {
+        AudioDeviceTypeAddr device;
+        jobject jDevice  = env->GetObjectArrayElement(jDeviceArray, i);
+        if (jDevice == nullptr) {
+            return false;
+        }
+        jStatus = createAudioDeviceTypeAddrFromJava(env, &device, jDevice);
+        if (jStatus != (jint)AUDIO_JAVA_SUCCESS) {
+            return false;
+        }
+        nDevices.push_back(device);
+    }
+
+    audio_config_t nConfig;
+    javaAudioFormatToNativeAudioConfig(env, &nConfig, jFormat, false /*isInput*/);
+
+    bool canBeSpatialized;
+    status_t status =
+            AudioSystem::canBeSpatialized(paa.get(), &nConfig, nDevices, &canBeSpatialized);
+    if (status != NO_ERROR) {
+        ALOGW("%s native returned error %d", __func__, status);
+        return false;
+    }
+    return canBeSpatialized;
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] =
@@ -2845,7 +2908,15 @@
           (void *)android_media_AudioSystem_removeUserIdDeviceAffinities},
          {"setCurrentImeUid", "(I)I", (void *)android_media_AudioSystem_setCurrentImeUid},
          {"setVibratorInfos", "(Ljava/util/List;)I",
-          (void *)android_media_AudioSystem_setVibratorInfos}};
+          (void *)android_media_AudioSystem_setVibratorInfos},
+         {"nativeGetSpatializer",
+          "(Landroid/media/INativeSpatializerCallback;)Landroid/os/IBinder;",
+          (void *)android_media_AudioSystem_getSpatializer},
+         {"canBeSpatialized",
+          "(Landroid/media/AudioAttributes;Landroid/media/AudioFormat;"
+          "[Landroid/media/AudioDeviceAttributes;)Z",
+          (void *)android_media_AudioSystem_canBeSpatialized}};
+
 
 static const JNINativeMethod gEventHandlerMethods[] = {
     {"native_setup",
@@ -3070,6 +3141,8 @@
     gVibratorMethods.getResonantFrequency =
             GetMethodIDOrDie(env, vibratorClass, "getResonantFrequency", "()F");
     gVibratorMethods.getQFactor = GetMethodIDOrDie(env, vibratorClass, "getQFactor", "()F");
+    gVibratorMethods.getMaxAmplitude =
+            GetMethodIDOrDie(env, vibratorClass, "getHapticChannelMaximumAmplitude", "()F");
 
     AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback);
 
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index e93b00d..86d7810 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -92,6 +92,7 @@
   jfieldID mSmallestScreenWidthDpOffset;
   jfieldID mScreenWidthDpOffset;
   jfieldID mScreenHeightDpOffset;
+  jfieldID mScreenLayoutOffset;
 } gConfigurationOffsets;
 
 static struct arraymap_offsets_t {
@@ -1019,6 +1020,7 @@
                    config.smallestScreenWidthDp);
   env->SetIntField(result, gConfigurationOffsets.mScreenWidthDpOffset, config.screenWidthDp);
   env->SetIntField(result, gConfigurationOffsets.mScreenHeightDpOffset, config.screenHeightDp);
+  env->SetIntField(result, gConfigurationOffsets.mScreenLayoutOffset, config.screenLayout);
   return result;
 }
 
@@ -1553,6 +1555,8 @@
       GetFieldIDOrDie(env, configurationClass, "screenWidthDp", "I");
   gConfigurationOffsets.mScreenHeightDpOffset =
       GetFieldIDOrDie(env, configurationClass, "screenHeightDp", "I");
+  gConfigurationOffsets.mScreenLayoutOffset =
+          GetFieldIDOrDie(env, configurationClass, "screenLayout", "I");
 
   jclass arrayMapClass = FindClassOrDie(env, "android/util/ArrayMap");
   gArrayMapOffsets.classObject = MakeGlobalRefOrDie(env, arrayMapClass);
diff --git a/core/jni/android_view_InputEventSender.cpp b/core/jni/android_view_InputEventSender.cpp
index 45e3d1b..16366a4 100644
--- a/core/jni/android_view_InputEventSender.cpp
+++ b/core/jni/android_view_InputEventSender.cpp
@@ -155,6 +155,7 @@
                                                    event->getYPrecision(),
                                                    event->getRawXCursorPosition(),
                                                    event->getRawYCursorPosition(),
+                                                   event->getDisplayOrientation(),
                                                    event->getDisplaySize().x,
                                                    event->getDisplaySize().y, event->getDownTime(),
                                                    event->getHistoricalEventTime(i),
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index 6971301..cabf3ab 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -22,6 +22,7 @@
 #include <android_runtime/AndroidRuntime.h>
 #include <android_runtime/Log.h>
 #include <attestation/HmacKeyManager.h>
+#include <gui/constants.h>
 #include <input/Input.h>
 #include <nativehelper/ScopedUtfChars.h>
 #include <utils/Log.h>
@@ -56,6 +57,8 @@
     jfieldID toolMajor;
     jfieldID toolMinor;
     jfieldID orientation;
+    jfieldID relativeX;
+    jfieldID relativeY;
 } gPointerCoordsClassInfo;
 
 static struct {
@@ -212,6 +215,12 @@
             env->GetFloatField(pointerCoordsObj, gPointerCoordsClassInfo.toolMinor));
     outRawPointerCoords->setAxisValue(AMOTION_EVENT_AXIS_ORIENTATION,
             env->GetFloatField(pointerCoordsObj, gPointerCoordsClassInfo.orientation));
+    outRawPointerCoords->setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X,
+                                      env->GetFloatField(pointerCoordsObj,
+                                                         gPointerCoordsClassInfo.relativeX));
+    outRawPointerCoords->setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y,
+                                      env->GetFloatField(pointerCoordsObj,
+                                                         gPointerCoordsClassInfo.relativeY));
 
     BitSet64 bits =
             BitSet64(env->GetLongField(pointerCoordsObj, gPointerCoordsClassInfo.mPackedAxisBits));
@@ -261,6 +270,12 @@
     float rawY = rawPointerCoords->getAxisValue(AMOTION_EVENT_AXIS_Y);
     vec2 transformed = transform.transform(rawX, rawY);
 
+    float rawRelX = rawPointerCoords->getAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X);
+    float rawRelY = rawPointerCoords->getAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y);
+    // Apply only rotation and scale, not translation.
+    const vec2 transformedOrigin = transform.transform(0, 0);
+    const vec2 transformedRel = transform.transform(rawRelX, rawRelY) - transformedOrigin;
+
     env->SetFloatField(outPointerCoordsObj, gPointerCoordsClassInfo.x, transformed.x);
     env->SetFloatField(outPointerCoordsObj, gPointerCoordsClassInfo.y, transformed.y);
     env->SetFloatField(outPointerCoordsObj, gPointerCoordsClassInfo.pressure,
@@ -277,6 +292,8 @@
             rawPointerCoords->getAxisValue(AMOTION_EVENT_AXIS_TOOL_MINOR));
     env->SetFloatField(outPointerCoordsObj, gPointerCoordsClassInfo.orientation,
             rawPointerCoords->getAxisValue(AMOTION_EVENT_AXIS_ORIENTATION));
+    env->SetFloatField(outPointerCoordsObj, gPointerCoordsClassInfo.relativeX, transformedRel.x);
+    env->SetFloatField(outPointerCoordsObj, gPointerCoordsClassInfo.relativeY, transformedRel.y);
 
     uint64_t outBits = 0;
     BitSet64 bits = BitSet64(rawPointerCoords->bits);
@@ -289,6 +306,8 @@
     bits.clearBit(AMOTION_EVENT_AXIS_TOOL_MAJOR);
     bits.clearBit(AMOTION_EVENT_AXIS_TOOL_MINOR);
     bits.clearBit(AMOTION_EVENT_AXIS_ORIENTATION);
+    bits.clearBit(AMOTION_EVENT_AXIS_RELATIVE_X);
+    bits.clearBit(AMOTION_EVENT_AXIS_RELATIVE_Y);
     if (!bits.isEmpty()) {
         uint32_t packedAxesCount = bits.count();
         jfloatArray outValuesArray = obtainPackedAxisValuesArray(env, packedAxesCount,
@@ -378,8 +397,8 @@
                       flags, edgeFlags, metaState, buttonState,
                       static_cast<MotionClassification>(classification), transform, xPrecision,
                       yPrecision, AMOTION_EVENT_INVALID_CURSOR_POSITION,
-                      AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_DISPLAY_SIZE,
-                      AMOTION_EVENT_INVALID_DISPLAY_SIZE, downTimeNanos, eventTimeNanos,
+                      AMOTION_EVENT_INVALID_CURSOR_POSITION, ui::Transform::ROT_0,
+                      INVALID_DISPLAY_SIZE, INVALID_DISPLAY_SIZE, downTimeNanos, eventTimeNanos,
                       pointerCount, pointerProperties, rawPointerCoords);
 
     return reinterpret_cast<jlong>(event.release());
@@ -872,6 +891,8 @@
     gPointerCoordsClassInfo.toolMajor = GetFieldIDOrDie(env, clazz, "toolMajor", "F");
     gPointerCoordsClassInfo.toolMinor = GetFieldIDOrDie(env, clazz, "toolMinor", "F");
     gPointerCoordsClassInfo.orientation = GetFieldIDOrDie(env, clazz, "orientation", "F");
+    gPointerCoordsClassInfo.relativeX = GetFieldIDOrDie(env, clazz, "relativeX", "F");
+    gPointerCoordsClassInfo.relativeY = GetFieldIDOrDie(env, clazz, "relativeY", "F");
 
     clazz = FindClassOrDie(env, "android/view/MotionEvent$PointerProperties");
 
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 8d12df2..1452c67 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -62,6 +62,8 @@
 
 namespace android {
 
+using gui::FocusRequest;
+
 static void doThrowNPE(JNIEnv* env) {
     jniThrowNullPointerException(env, NULL);
 }
@@ -868,6 +870,13 @@
     transaction->setFixedTransformHint(ctrl, transformHint);
 }
 
+static void nativeSetDropInputMode(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                   jlong nativeObject, jint mode) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+    SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    transaction->setDropInputMode(ctrl, static_cast<gui::DropInputMode>(mode));
+}
+
 static jlongArray nativeGetPhysicalDisplayIds(JNIEnv* env, jclass clazz) {
     const auto displayIds = SurfaceComposerClient::getPhysicalDisplayIds();
     jlongArray array = env->NewLongArray(displayIds.size());
@@ -889,6 +898,12 @@
     return array;
 }
 
+static jlong nativeGetPrimaryPhysicalDisplayId(JNIEnv* env, jclass clazz) {
+    PhysicalDisplayId displayId;
+    SurfaceComposerClient::getPrimaryPhysicalDisplayId(&displayId);
+    return static_cast<jlong>(displayId.value);
+}
+
 static jobject nativeGetPhysicalDisplayToken(JNIEnv* env, jclass clazz, jlong physicalDisplayId) {
     sp<IBinder> token =
             SurfaceComposerClient::getPhysicalDisplayToken(PhysicalDisplayId(physicalDisplayId));
@@ -1014,6 +1029,17 @@
     }
 }
 
+static void nativeSetDisplayFlags(JNIEnv* env, jclass clazz, jlong transactionObj, jobject tokenObj,
+                                  jint flags) {
+    sp<IBinder> token(ibinderForJavaObject(env, tokenObj));
+    if (token == NULL) return;
+
+    {
+        auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+        transaction->setDisplayFlags(token, flags);
+    }
+}
+
 static void nativeSetDisplayProjection(JNIEnv* env, jclass clazz,
         jlong transactionObj,
         jobject tokenObj, jint orientation,
@@ -1789,16 +1815,18 @@
     if (surface == nullptr) {
         return;
     }
-    surface->setTransformHint(
-            ui::Transform::toRotationFlags(static_cast<ui::Rotation>(transformHint)));
+    surface->setTransformHint(transformHint);
 }
 
 static jint nativeGetTransformHint(JNIEnv* env, jclass clazz, jlong nativeSurfaceControl) {
     sp<SurfaceControl> surface(reinterpret_cast<SurfaceControl*>(nativeSurfaceControl));
-    ui::Transform::RotationFlags transformHintRotationFlags =
-            static_cast<ui::Transform::RotationFlags>(surface->getTransformHint());
+    return surface->getTransformHint();
+}
 
-    return toRotationInt(ui::Transform::toRotation((transformHintRotationFlags)));
+static jint nativeGetLayerId(JNIEnv* env, jclass clazz, jlong nativeSurfaceControl) {
+    sp<SurfaceControl> surface(reinterpret_cast<SurfaceControl*>(nativeSurfaceControl));
+
+    return surface->getLayerId();
 }
 
 // ----------------------------------------------------------------------------
@@ -1879,6 +1907,8 @@
             (void*)nativeReleaseFrameRateFlexibilityToken },
     {"nativeGetPhysicalDisplayIds", "()[J",
             (void*)nativeGetPhysicalDisplayIds },
+    {"nativeGetPrimaryPhysicalDisplayId", "()J",
+            (void*)nativeGetPrimaryPhysicalDisplayId },
     {"nativeGetPhysicalDisplayToken", "(J)Landroid/os/IBinder;",
             (void*)nativeGetPhysicalDisplayToken },
     {"nativeCreateDisplay", "(Ljava/lang/String;Z)Landroid/os/IBinder;",
@@ -1889,6 +1919,8 @@
             (void*)nativeSetDisplaySurface },
     {"nativeSetDisplayLayerStack", "(JLandroid/os/IBinder;I)V",
             (void*)nativeSetDisplayLayerStack },
+    {"nativeSetDisplayFlags", "(JLandroid/os/IBinder;I)V",
+            (void*)nativeSetDisplayFlags },
     {"nativeSetDisplayProjection", "(JLandroid/os/IBinder;IIIIIIIII)V",
             (void*)nativeSetDisplayProjection },
     {"nativeSetDisplaySize", "(JLandroid/os/IBinder;II)V",
@@ -1994,6 +2026,10 @@
             (void*)nativeGetTransformHint },
     {"nativeSetTrustedOverlay", "(JJZ)V",
             (void*)nativeSetTrustedOverlay },
+    {"nativeSetDropInputMode", "(JJI)V",
+             (void*)nativeSetDropInputMode },
+    {"nativeGetLayerId", "(J)I",
+            (void*)nativeGetLayerId },
         // clang-format on
 };
 
diff --git a/core/jni/android_window_WindowInfosListener.cpp b/core/jni/android_window_WindowInfosListener.cpp
new file mode 100644
index 0000000..ab88b53
--- /dev/null
+++ b/core/jni/android_window_WindowInfosListener.cpp
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "WindowInfosListener"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <gui/SurfaceComposerClient.h>
+#include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+
+#include "android_hardware_input_InputWindowHandle.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+using gui::WindowInfo;
+
+namespace {
+
+static struct {
+    jclass clazz;
+    jmethodID onWindowInfosChanged;
+} gListenerClassInfo;
+
+static jclass gInputWindowHandleClass;
+
+struct WindowInfosListener : public gui::WindowInfosListener {
+    WindowInfosListener(JNIEnv* env, jobject listener)
+          : mListener(env->NewWeakGlobalRef(listener)) {}
+
+    void onWindowInfosChanged(const std::vector<WindowInfo>& windowInfos) override {
+        JNIEnv* env = AndroidRuntime::getJNIEnv();
+        LOG_ALWAYS_FATAL_IF(env == nullptr, "Unable to retrieve JNIEnv in onWindowInfoChanged.");
+
+        jobject listener = env->NewGlobalRef(mListener);
+        if (listener == nullptr) {
+            // Weak reference went out of scope
+            return;
+        }
+
+        jobjectArray jWindowHandlesArray =
+                env->NewObjectArray(windowInfos.size(), gInputWindowHandleClass, nullptr);
+        for (int i = 0; i < windowInfos.size(); i++) {
+            ScopedLocalRef<jobject>
+                    jWindowHandle(env,
+                                  android_view_InputWindowHandle_fromWindowInfo(env,
+                                                                                windowInfos[i]));
+            env->SetObjectArrayElement(jWindowHandlesArray, i, jWindowHandle.get());
+        }
+
+        env->CallVoidMethod(listener, gListenerClassInfo.onWindowInfosChanged, jWindowHandlesArray);
+        env->DeleteGlobalRef(listener);
+
+        if (env->ExceptionCheck()) {
+            ALOGE("WindowInfosListener.onWindowInfosChanged() failed.");
+            LOGE_EX(env);
+            env->ExceptionClear();
+        }
+    }
+
+    ~WindowInfosListener() override {
+        JNIEnv* env = AndroidRuntime::getJNIEnv();
+        env->DeleteWeakGlobalRef(mListener);
+    }
+
+private:
+    jweak mListener;
+};
+
+jlong nativeCreate(JNIEnv* env, jclass clazz, jobject obj) {
+    WindowInfosListener* listener = new WindowInfosListener(env, obj);
+    listener->incStrong((void*)nativeCreate);
+    return reinterpret_cast<jlong>(listener);
+}
+
+void destroyNativeService(void* ptr) {
+    WindowInfosListener* listener = reinterpret_cast<WindowInfosListener*>(ptr);
+    listener->decStrong((void*)nativeCreate);
+}
+
+void nativeRegister(JNIEnv* env, jclass clazz, jlong ptr) {
+    sp<WindowInfosListener> listener = reinterpret_cast<WindowInfosListener*>(ptr);
+    SurfaceComposerClient::getDefault()->addWindowInfosListener(listener);
+}
+
+void nativeUnregister(JNIEnv* env, jclass clazz, jlong ptr) {
+    sp<WindowInfosListener> listener = reinterpret_cast<WindowInfosListener*>(ptr);
+    SurfaceComposerClient::getDefault()->removeWindowInfosListener(listener);
+}
+
+static jlong nativeGetFinalizer(JNIEnv* /* env */, jclass /* clazz */) {
+    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&destroyNativeService));
+}
+
+const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeCreate", "(Landroid/window/WindowInfosListener;)J", (void*)nativeCreate},
+        {"nativeRegister", "(J)V", (void*)nativeRegister},
+        {"nativeUnregister", "(J)V", (void*)nativeUnregister},
+        {"nativeGetFinalizer", "()J", (void*)nativeGetFinalizer}};
+
+} // namespace
+
+int register_android_window_WindowInfosListener(JNIEnv* env) {
+    int res = jniRegisterNativeMethods(env, "android/window/WindowInfosListener", gMethods,
+                                       NELEM(gMethods));
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    jclass clazz = env->FindClass("android/window/WindowInfosListener");
+    gListenerClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gListenerClassInfo.onWindowInfosChanged =
+            env->GetMethodID(gListenerClassInfo.clazz, "onWindowInfosChanged",
+                             "([Landroid/view/InputWindowHandle;)V");
+
+    clazz = env->FindClass("android/view/InputWindowHandle");
+    gInputWindowHandleClass = MakeGlobalRefOrDie(env, clazz);
+    return 0;
+}
+
+} // namespace android
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 1bba12f..ba4a5b0 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -79,7 +79,7 @@
         optional SettingProto accessibility_magnification_mode = 34 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto button_targets = 35 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_magnification_capability = 36 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        // Settings for accessibility button mode (navigation bar or floating action menu).
+        // Settings for accessibility button related config
         optional SettingProto accessibility_button_mode = 37 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_floating_menu_size = 38 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_floating_menu_icon_type = 39 [ (android.privacy).dest = DEST_AUTOMATIC ];
diff --git a/core/proto/android/server/accessibilitytrace.proto b/core/proto/android/server/accessibilitytrace.proto
index 1fc4a01..41fecfd 100644
--- a/core/proto/android/server/accessibilitytrace.proto
+++ b/core/proto/android/server/accessibilitytrace.proto
@@ -46,17 +46,17 @@
     /* required: elapsed realtime in nanos since boot of when this entry was logged */
     optional fixed64 elapsed_realtime_nanos = 1;
     optional string calendar_time = 2;
-
-    optional string process_name = 3;
-    optional string thread_id_name = 4;
+    repeated string logging_type = 3;
+    optional string process_name = 4;
+    optional string thread_id_name = 5;
 
     /* where the trace originated */
-    optional string where = 5;
+    optional string where = 6;
 
-    optional string calling_pkg = 6;
-    optional string calling_params = 7;
-    optional string calling_stacks = 8;
+    optional string calling_pkg = 7;
+    optional string calling_params = 8;
+    optional string calling_stacks = 9;
 
-    optional AccessibilityDumpProto accessibility_service = 9;
-    optional com.android.server.wm.WindowManagerServiceDumpProto window_manager_service = 10;
+    optional AccessibilityDumpProto accessibility_service = 10;
+    optional com.android.server.wm.WindowManagerServiceDumpProto window_manager_service = 11;
 }
diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto
index fa1e9d4..6f81b82 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -69,6 +69,7 @@
     // know what activity types to check for when invoking splitscreen multi-window.
     optional bool is_home_recents_component = 6;
     repeated IdentifierProto pending_activities = 7 [deprecated=true];
+    optional int32 default_min_size_resizable_task = 8;
 }
 
 message BarControllerProto {
@@ -278,7 +279,7 @@
 message TaskProto {
     option (.android.msg_privacy).dest = DEST_AUTOMATIC;
 
-    optional WindowContainerProto window_container = 1;
+    optional WindowContainerProto window_container = 1 [deprecated=true];
     optional int32 id = 2;
     reserved 3; // activity
     optional bool fills_parent = 4;
@@ -295,12 +296,12 @@
     optional string real_activity = 13;
     optional string orig_activity = 14;
 
-    optional int32 display_id = 15;
+    optional int32 display_id = 15 [deprecated=true];
     optional int32 root_task_id = 16;
-    optional int32 activity_type = 17 [(.android.typedef) = "android.app.WindowConfiguration.ActivityType"];
+    optional int32 activity_type = 17 [(.android.typedef) = "android.app.WindowConfiguration.ActivityType", deprecated=true] ;
     optional int32 resize_mode = 18 [(.android.typedef) = "android.appwidget.AppWidgetProviderInfo.ResizeModeFlags"];
-    optional int32 min_width = 19;
-    optional int32 min_height = 20;
+    optional int32 min_width = 19 [deprecated=true];
+    optional int32 min_height = 20 [deprecated=true];
 
     optional .android.graphics.RectProto adjusted_bounds = 21;
     optional .android.graphics.RectProto last_non_fullscreen_bounds = 22;
@@ -312,6 +313,18 @@
     optional bool created_by_organizer = 28;
     optional string affinity = 29;
     optional bool has_child_pip_activity = 30;
+    optional TaskFragmentProto task_fragment = 31;
+}
+
+/* represents TaskFragment */
+message TaskFragmentProto {
+    option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    optional WindowContainerProto window_container = 1;
+    optional int32 display_id = 2;
+    optional int32 activity_type = 3 [(.android.typedef) = "android.app.WindowConfiguration.ActivityType"];
+    optional int32 min_width = 4;
+    optional int32 min_height = 5;
 }
 
 /* represents ActivityRecordProto */
@@ -468,6 +481,7 @@
     optional SurfaceAnimatorProto surface_animator = 4;
     repeated WindowContainerChildProto children = 5;
     optional IdentifierProto identifier = 6;
+    optional .android.view.SurfaceControlProto surface_control = 7;
 }
 
 /* represents a generic child of a WindowContainer */
@@ -493,6 +507,8 @@
     optional WindowTokenProto window_token = 7;
     /* represents a WindowState child */
     optional WindowStateProto window = 8;
+    /* represents a WindowState child */
+    optional TaskFragmentProto task_fragment = 9;
 }
 
 /* represents ConfigurationContainer */
diff --git a/core/proto/android/view/surfacecontrol.proto b/core/proto/android/view/surfacecontrol.proto
index cbb243b..5a5f035 100644
--- a/core/proto/android/view/surfacecontrol.proto
+++ b/core/proto/android/view/surfacecontrol.proto
@@ -29,4 +29,5 @@
 
     optional int32 hash_code = 1;
     optional string name = 2 [ (android.privacy).dest = DEST_EXPLICIT ];
+    optional int32 layerId = 3;
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a774cb4..08361a8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4870,6 +4870,18 @@
     <permission android:name="android.permission.WRITE_SETTINGS_HOMEPAGE_DATA"
                 android:protectionLevel="signature|privileged" />
 
+    <!-- An application needs this permission for
+         {@link android.provider.Settings#ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY} to show its
+         {@link android.app.Activity} embedded in Settings app. -->
+    <permission android:name="android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK"
+                android:protectionLevel="signature|preinstalled" />
+
+    <!-- @SystemApi {@link android.app.Activity} should require this permission to ensure that only
+         the settings app can embed it in a multi pane window.
+         @hide -->
+    <permission android:name="android.permission.ALLOW_PLACE_IN_MULTI_PANE_SETTINGS"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi Allows applications to set a live wallpaper.
          @hide XXX Change to signature once the picker is moved to its
          own apk as Ghod Intended. -->
@@ -5872,7 +5884,6 @@
                   android:excludeFromRecents="true"
                   android:documentLaunchMode="never"
                   android:relinquishTaskIdentity="true"
-                  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
                   android:process=":ui"
                   android:visibleToInstantApps="true">
             <intent-filter>
diff --git a/core/res/res/drawable-nodpi/default_wallpaper.png b/core/res/res/drawable-nodpi/default_wallpaper.png
index ce546f0..5152972 100644
--- a/core/res/res/drawable-nodpi/default_wallpaper.png
+++ b/core/res/res/drawable-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png b/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png
index af8e251..26376fb 100644
--- a/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png
+++ b/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png b/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png
index cb00d82..490ebee 100644
--- a/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png
+++ b/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/drawable/ic_corp_badge.xml b/core/res/res/drawable/ic_corp_badge.xml
index 16df452..6c40f10 100644
--- a/core/res/res/drawable/ic_corp_badge.xml
+++ b/core/res/res/drawable/ic_corp_badge.xml
@@ -22,8 +22,5 @@
         android:viewportHeight="24">
     <path
         android:fillColor="@android:color/white"
-        android:pathData="M20,6h-4V4c0-1.1-0.9-2-2-2h-4C8.9,2,8,2.9,8,4v2H4C2.9,6,2,6.9,2,8l0,11c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V8 C22,6.9,21.1,6,20,6z M10,4h4v2h-4V4z M20,19H4V8h16V19z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 12 12 C 12.8284271247 12 13.5 12.6715728753 13.5 13.5 C 13.5 14.3284271247 12.8284271247 15 12 15 C 11.1715728753 15 10.5 14.3284271247 10.5 13.5 C 10.5 12.6715728753 11.1715728753 12 12 12 Z" />
+        android:pathData="@string/config_work_badge_path_24"/>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_corp_badge_case.xml b/core/res/res/drawable/ic_corp_badge_case.xml
index 1cd995e..838426e 100644
--- a/core/res/res/drawable/ic_corp_badge_case.xml
+++ b/core/res/res/drawable/ic_corp_badge_case.xml
@@ -1,9 +1,9 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
         android:width="20dp"
         android:height="20dp"
-        android:viewportWidth="20.0"
-        android:viewportHeight="20.0">
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
     <path
-        android:pathData="M14.67,6.5h-2.33V5.33c0,-0.65 -0.52,-1.17 -1.17,-1.17H8.83c-0.65,0 -1.17,0.52 -1.17,1.17V6.5H5.33c-0.65,0 -1.16,0.52 -1.16,1.17l-0.01,6.42c0,0.65 0.52,1.17 1.17,1.17h9.33c0.65,0 1.17,-0.52 1.17,-1.17V7.67C15.83,7.02 15.31,6.5 14.67,6.5zM10,11.75c-0.64,0 -1.17,-0.52 -1.17,-1.17c0,-0.64 0.52,-1.17 1.17,-1.17c0.64,0 1.17,0.52 1.17,1.17C11.17,11.22 10.64,11.75 10,11.75zM11.17,6.5H8.83V5.33h2.33V6.5z"
+        android:pathData="@string/config_work_badge_path_24"
         android:fillColor="#1A73E8"/>
 </vector>
diff --git a/core/res/res/drawable/ic_corp_badge_no_background.xml b/core/res/res/drawable/ic_corp_badge_no_background.xml
index 8f7fb70..e81e69f 100644
--- a/core/res/res/drawable/ic_corp_badge_no_background.xml
+++ b/core/res/res/drawable/ic_corp_badge_no_background.xml
@@ -4,6 +4,6 @@
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
-        android:pathData="M20,6h-4V4c0,-1.11 -0.89,-2 -2,-2h-4C8.89,2 8,2.89 8,4v2H4C2.89,6 2.01,6.89 2.01,8L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V8C22,6.89 21.11,6 20,6zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S13.1,15 12,15zM14,6h-4V4h4V6z"
+        android:pathData="@string/config_work_badge_path_24"
         android:fillColor="#FFFFFF"/>
 </vector>
diff --git a/core/res/res/drawable/ic_corp_icon.xml b/core/res/res/drawable/ic_corp_icon.xml
index 48531dd..86bb98e 100644
--- a/core/res/res/drawable/ic_corp_icon.xml
+++ b/core/res/res/drawable/ic_corp_icon.xml
@@ -4,6 +4,6 @@
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
-        android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM14,6h-4L10,4h4v2z"
+        android:pathData="@string/config_work_badge_path_24"
         android:fillColor="#000000"/>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_corp_icon_badge_case.xml b/core/res/res/drawable/ic_corp_icon_badge_case.xml
index 50551d40..09cf9cb 100644
--- a/core/res/res/drawable/ic_corp_icon_badge_case.xml
+++ b/core/res/res/drawable/ic_corp_icon_badge_case.xml
@@ -22,9 +22,15 @@
 
     <path
         android:pathData="M 42 42 L 58 42 L 58 58 L 42 58 L 42 42 Z" />
-    <path
-        android:fillColor="#1A73E8"
-        android:pathData="M55.33,46H52.67V44.67a1.33,1.33,0,0,0-1.33-1.33H48.67a1.33,1.33,0,0,0-1.33,1.33V46H44.67a1.32,1.32,0,0,0-1.33,1.33v7.33A1.33,1.33,0,0,0,44.67,56H55.33a1.33,1.33,0,0,0,1.33-1.33V47.33A1.33,1.33,0,0,0,55.33,46ZM50,52a1.33,1.33,0,1,1,1.33-1.33A1.34,1.34,0,0,1,50,52Zm1.33-6H48.67V44.67h2.67Z" />
+    <group
+        android:scaleX=".66"
+        android:scaleY=".66"
+        android:translateX="42"
+        android:translateY="42">
+        <path
+            android:pathData="@string/config_work_badge_path_24"
+            android:fillColor="#1A73E8"/>
+    </group>
     <path
         android:pathData="M 0 0 H 64 V 64 H 0 V 0 Z" />
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_corp_statusbar_icon.xml b/core/res/res/drawable/ic_corp_statusbar_icon.xml
index 8f7fb70..e81e69f 100644
--- a/core/res/res/drawable/ic_corp_statusbar_icon.xml
+++ b/core/res/res/drawable/ic_corp_statusbar_icon.xml
@@ -4,6 +4,6 @@
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
-        android:pathData="M20,6h-4V4c0,-1.11 -0.89,-2 -2,-2h-4C8.89,2 8,2.89 8,4v2H4C2.89,6 2.01,6.89 2.01,8L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V8C22,6.89 21.11,6 20,6zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S13.1,15 12,15zM14,6h-4V4h4V6z"
+        android:pathData="@string/config_work_badge_path_24"
         android:fillColor="#FFFFFF"/>
 </vector>
diff --git a/core/res/res/drawable/ic_corp_user_badge.xml b/core/res/res/drawable/ic_corp_user_badge.xml
index a08f2d4..76ba450 100644
--- a/core/res/res/drawable/ic_corp_user_badge.xml
+++ b/core/res/res/drawable/ic_corp_user_badge.xml
@@ -8,9 +8,6 @@
         android:pathData="M16.3,11.3h3.4v1.7h-3.4z"
         android:fillColor="#FFFFFF"/>
     <path
-        android:pathData="M18,17.17c-0.92,0 -1.67,0.75 -1.67,1.67c0,0.92 0.75,1.67 1.67,1.67c0.92,0 1.67,-0.75 1.67,-1.67C19.67,17.92 18.92,17.17 18,17.17z"
-        android:fillColor="#FFFFFF"/>
-    <path
         android:pathData="M18,0C8.06,0 0,8.06 0,18s8.06,18 18,18s18,-8.06 18,-18S27.94,0 18,0zM26.3,23.83c0,0.92 -0.71,1.67 -1.63,1.67H11.33c-0.93,0 -1.67,-0.74 -1.67,-1.67l0.01,-9.17c0,-0.92 0.73,-1.67 1.66,-1.67h3.37v-1.67c0,-0.93 0.71,-1.63 1.63,-1.63h3.33c0.93,0 1.63,0.71 1.63,1.63V13h3.37c0.93,0 1.63,0.74 1.63,1.67V23.83z"
         android:fillColor="#FFFFFF"/>
 </vector>
diff --git a/core/res/res/layout-car/car_alert_dialog.xml b/core/res/res/layout-car/car_alert_dialog.xml
index 569e594..2e7b62c 100644
--- a/core/res/res/layout-car/car_alert_dialog.xml
+++ b/core/res/res/layout-car/car_alert_dialog.xml
@@ -54,7 +54,7 @@
                     android:layout_height="wrap_content"
                     android:layout_marginStart="@dimen/text_view_start_margin"
                     android:layout_marginEnd="@dimen/text_view_end_margin"
-                    style="@style/CarBody2"/>
+                    style="@style/CarBody4"/>
 
                 <!-- we don't need this spacer, but the id needs to be here for compatibility -->
                 <Space
diff --git a/core/res/res/layout-car/car_alert_dialog_button_bar.xml b/core/res/res/layout-car/car_alert_dialog_button_bar.xml
index 277b0dc..4f815b8 100644
--- a/core/res/res/layout-car/car_alert_dialog_button_bar.xml
+++ b/core/res/res/layout-car/car_alert_dialog_button_bar.xml
@@ -36,6 +36,9 @@
         <Button
             android:id="@+id/button3"
             style="@style/CarAction1"
+            android:minWidth="@dimen/car_touch_target_size"
+            android:paddingStart="@dimen/car_padding_2"
+            android:paddingEnd="@dimen/car_padding_2"
             android:background="@drawable/car_dialog_button_background"
             android:layout_marginRight="@dimen/button_end_margin"
             android:layout_width="wrap_content"
@@ -44,6 +47,9 @@
         <Button
             android:id="@+id/button2"
             style="@style/CarAction1"
+            android:minWidth="@dimen/car_touch_target_size"
+            android:paddingStart="@dimen/car_padding_2"
+            android:paddingEnd="@dimen/car_padding_2"
             android:background="@drawable/car_dialog_button_background"
             android:layout_marginRight="@dimen/button_end_margin"
             android:layout_width="wrap_content"
@@ -52,6 +58,9 @@
         <Button
             android:id="@+id/button1"
             style="@style/CarAction1"
+            android:minWidth="@dimen/car_touch_target_size"
+            android:paddingStart="@dimen/car_padding_2"
+            android:paddingEnd="@dimen/car_padding_2"
             android:background="@drawable/car_dialog_button_background"
             android:layout_width="wrap_content"
             android:layout_height="@dimen/button_layout_height" />
diff --git a/core/res/res/layout/accessibility_shortcut_chooser_item.xml b/core/res/res/layout/accessibility_shortcut_chooser_item.xml
index 7cca129..4d7946b 100644
--- a/core/res/res/layout/accessibility_shortcut_chooser_item.xml
+++ b/core/res/res/layout/accessibility_shortcut_chooser_item.xml
@@ -39,15 +39,20 @@
         android:layout_height="48dp"
         android:scaleType="fitCenter"/>
 
-    <TextView
-        android:id="@+id/accessibility_shortcut_target_label"
+    <LinearLayout
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginStart="16dp"
-        android:layout_weight="1"
-        android:textSize="20sp"
-        android:textColor="?attr/textColorPrimary"
-        android:fontFamily="sans-serif-medium"/>
+        android:layout_weight="1">
+
+        <TextView
+            android:id="@+id/accessibility_shortcut_target_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="?attr/textColorPrimary"
+            android:fontFamily="sans-serif-medium"/>
+    </LinearLayout>
 
     <TextView
         android:id="@+id/accessibility_shortcut_target_status"
diff --git a/core/res/res/layout/transient_notification.xml b/core/res/res/layout/transient_notification.xml
index 8a3d734..3259201 100644
--- a/core/res/res/layout/transient_notification.xml
+++ b/core/res/res/layout/transient_notification.xml
@@ -40,6 +40,5 @@
         android:maxLines="2"
         android:paddingTop="12dp"
         android:paddingBottom="12dp"
-        android:lineHeight="20sp"
         android:textAppearance="@style/TextAppearance.Toast"/>
 </LinearLayout>
diff --git a/core/res/res/values-sw600dp/config.xml b/core/res/res/values-sw600dp/config.xml
index 34b6a54..861e329 100644
--- a/core/res/res/values-sw600dp/config.xml
+++ b/core/res/res/values-sw600dp/config.xml
@@ -51,5 +51,10 @@
 
     <!-- If true, show multiuser switcher by default unless the user specifically disables it. -->
     <bool name="config_showUserSwitcherByDefault">true</bool>
+
+    <!-- Enable dynamic keyguard positioning for large-width screens. This will cause the keyguard
+         to be aligned to one side of the screen when in landscape mode. -->
+    <bool name="config_enableDynamicKeyguardPositioning">true</bool>
+
 </resources>
 
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index a5f5051..ab39152 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -8371,6 +8371,19 @@
              @hide @SystemApi -->
         <attr name="supportsAmbientMode" format="boolean" />
 
+        <!-- Indicates that this wallpaper service should receive zoom transition updates when
+             changing the structural state of the device (e.g. when folding or unfolding
+             a foldable device). When this value is set to true
+             {@link android.service.wallpaper.WallpaperService.Engine} could receive zoom updates
+             before or after changing the device state. Wallpapers receive zoom updates using
+             {@link android.service.wallpaper.WallpaperService.Engine#onZoomChanged(float)} and
+             zoom rendering should be handled manually. Zoom updates are delivered only when
+             {@link android.service.wallpaper.WallpaperService.Engine} is created and not destroyed.
+             Default value is true.
+             Corresponds to
+             {@link android.app.WallpaperInfo#shouldUseDefaultUnfoldTransition()} -->
+        <attr name="shouldUseDefaultUnfoldTransition" format="boolean" />
+
         <!-- Uri that specifies a settings Slice for this wallpaper. -->
         <attr name="settingsSliceUri" format="string"/>
 
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index b191584..59d6005 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -246,37 +246,37 @@
     <!-- Lightest shade of the accent color used by the system. White.
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_0">#ffffff</color>
-    <!-- Shade of the accent system color at 99% lightness.
+    <!-- Shade of the accent system color at 99% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_10">#F9FCFF</color>
-    <!-- Shade of the accent system color at 95% lightness.
+    <!-- Shade of the accent system color at 95% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_50">#E0F3FF</color>
-    <!-- Shade of the accent system color at 90% lightness.
+    <!-- Shade of the accent system color at 90% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_100">#C1E8FF</color>
-    <!-- Shade of the accent system color at 80% lightness.
+    <!-- Shade of the accent system color at 80% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_200">#76D1FF</color>
-    <!-- Shade of the accent system color at 70% lightness.
+    <!-- Shade of the accent system color at 70% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_300">#4BB6E8</color>
-    <!-- Shade of the accent system color at 60% lightness.
+    <!-- Shade of the accent system color at 60% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_400">#219BCC</color>
-    <!-- Shade of the accent system color at 49% lightness.
+    <!-- Shade of the accent system color at 49.6% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_500">#007FAC</color>
-    <!-- Shade of the accent system color at 40% lightness.
+    <!-- Shade of the accent system color at 40% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_600">#00668B</color>
-    <!-- Shade of the accent system color at 30% lightness.
+    <!-- Shade of the accent system color at 30% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_700">#004C69</color>
-    <!-- Shade of the accent system color at 20% lightness.
+    <!-- Shade of the accent system color at 20% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_800">#003549</color>
-    <!-- Shade of the accent system color at 10% lightness.
+    <!-- Shade of the accent system color at 10% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent1_900">#001E2C</color>
     <!-- Darkest shade of the accent color used by the system. Black.
@@ -286,37 +286,37 @@
     <!-- Lightest shade of the secondary accent color used by the system. White.
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_0">#ffffff</color>
-    <!-- Shade of the secondary accent system color at 99% lightness.
+    <!-- Shade of the secondary accent system color at 99% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_10">#F9FCFF</color>
-    <!-- Shade of the secondary accent system color at 95% lightness.
+    <!-- Shade of the secondary accent system color at 95% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_50">#E0F3FF</color>
-    <!-- Shade of the secondary accent system color at 90% lightness.
+    <!-- Shade of the secondary accent system color at 90% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_100">#D1E5F4</color>
-    <!-- Shade of the secondary accent system color at 80% lightness.
+    <!-- Shade of the secondary accent system color at 80% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_200">#B5CAD7</color>
-    <!-- Shade of the secondary accent system color at 70% lightness.
+    <!-- Shade of the secondary accent system color at 70% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_300">#9AAEBB</color>
-    <!-- Shade of the secondary accent system color at 60% lightness.
+    <!-- Shade of the secondary accent system color at 60% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_400">#8094A0</color>
-    <!-- Shade of the secondary accent system color at 49% lightness.
+    <!-- Shade of the secondary accent system color at 49.6% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_500">#657985</color>
-    <!-- Shade of the secondary accent system color at 40% lightness.
+    <!-- Shade of the secondary accent system color at 40% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_600">#4E616C</color>
-    <!-- Shade of the secondary accent system color at 30% lightness.
+    <!-- Shade of the secondary accent system color at 30% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_700">#374955</color>
-    <!-- Shade of the secondary accent system color at 20% lightness.
+    <!-- Shade of the secondary accent system color at 20% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_800">#20333D</color>
-    <!-- Shade of the secondary accent system color at 10% lightness.
+    <!-- Shade of the secondary accent system color at 10% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent2_900">#091E28</color>
     <!-- Darkest shade of the secondary accent color used by the system. Black.
@@ -326,37 +326,37 @@
     <!-- Lightest shade of the tertiary accent color used by the system. White.
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_0">#ffffff</color>
-    <!-- Shade of the tertiary accent system color at 99% lightness.
+    <!-- Shade of the tertiary accent system color at 99% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_10">#FFFBFF</color>
-    <!-- Shade of the tertiary accent system color at 95% lightness.
+    <!-- Shade of the tertiary accent system color at 95% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_50">#F5EEFF</color>
-    <!-- Shade of the tertiary accent system color at 90% lightness.
+    <!-- Shade of the tertiary accent system color at 90% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_100">#E6DEFF</color>
-    <!-- Shade of the tertiary accent system color at 80% lightness.
+    <!-- Shade of the tertiary accent system color at 80% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_200">#CAC1EA</color>
-    <!-- Shade of the tertiary accent system color at 70% lightness.
+    <!-- Shade of the tertiary accent system color at 70% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_300">#AEA6CE</color>
-    <!-- Shade of the tertiary accent system color at 60% lightness.
+    <!-- Shade of the tertiary accent system color at 60% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_400">#938CB1</color>
-    <!-- Shade of the tertiary accent system color at 49% lightness.
+    <!-- Shade of the tertiary accent system color at 49% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_500">#787296</color>
-    <!-- Shade of the tertiary accent system color at 40% lightness.
+    <!-- Shade of the tertiary accent system color at 40% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_600">#605A7C</color>
-    <!-- Shade of the tertiary accent system color at 30% lightness.
+    <!-- Shade of the tertiary accent system color at 30% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_700">#484264</color>
-    <!-- Shade of the tertiary accent system color at 20% lightness.
+    <!-- Shade of the tertiary accent system color at 20% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_800">#322C4C</color>
-    <!-- Shade of the tertiary accent system color at 10% lightness.
+    <!-- Shade of the tertiary accent system color at 10% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_accent3_900">#1D1736</color>
     <!-- Darkest shade of the tertiary accent color used by the system. Black.
@@ -366,37 +366,37 @@
     <!-- Lightest shade of the neutral color used by the system. White.
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_0">#ffffff</color>
-    <!-- Shade of the neutral system color at 99% lightness.
+    <!-- Shade of the neutral system color at 99% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_10">#FCFCFF</color>
-    <!-- Shade of the neutral system color at 95% lightness.
+    <!-- Shade of the neutral system color at 95% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_50">#F0F0F3</color>
-    <!-- Shade of the neutral system color at 90% lightness.
+    <!-- Shade of the neutral system color at 90% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_100">#E1E3E5</color>
-    <!-- Shade of the neutral system color at 80% lightness.
+    <!-- Shade of the neutral system color at 80% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_200">#C5C7C9</color>
-    <!-- Shade of the neutral system color at 70% lightness.
+    <!-- Shade of the neutral system color at 70% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_300">#AAABAE</color>
-    <!-- Shade of the neutral system color at 60% lightness.
+    <!-- Shade of the neutral system color at 60% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_400">#8F9193</color>
-    <!-- Shade of the neutral system color at 49% lightness.
+    <!-- Shade of the neutral system color at 49% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_500">#747679</color>
-    <!-- Shade of the neutral system color at 40% lightness.
+    <!-- Shade of the neutral system color at 40% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_600">#5C5F61</color>
-    <!-- Shade of the neutral system color at 30% lightness.
+    <!-- Shade of the neutral system color at 30% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_700">#454749</color>
-    <!-- Shade of the neutral system color at 20% lightness.
+    <!-- Shade of the neutral system color at 20% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_800">#2E3133</color>
-    <!-- Shade of the neutral system color at 10% lightness.
+    <!-- Shade of the neutral system color at 10% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral1_900">#191C1E</color>
     <!-- Darkest shade of the neutral color used by the system. Black.
@@ -406,37 +406,37 @@
     <!-- Lightest shade of the secondary neutral color used by the system. White.
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_0">#ffffff</color>
-    <!-- Shade of the secondary neutral system color at 99% lightness.
+    <!-- Shade of the secondary neutral system color at 99% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_10">#F9FCFF</color>
-    <!-- Shade of the secondary neutral system color at 95% lightness.
+    <!-- Shade of the secondary neutral system color at 95% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_50">#EBF1F8</color>
-    <!-- Shade of the secondary neutral system color at 90% lightness.
+    <!-- Shade of the secondary neutral system color at 90% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_100">#DCE3E9</color>
-    <!-- Shade of the secondary neutral system color at 80% lightness.
+    <!-- Shade of the secondary neutral system color at 80% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_200">#C0C7CD</color>
-    <!-- Shade of the secondary neutral system color at 70% lightness.
+    <!-- Shade of the secondary neutral system color at 70% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_300">#A5ACB2</color>
-    <!-- Shade of the secondary neutral system color at 60% lightness.
+    <!-- Shade of the secondary neutral system color at 60% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_400">#8A9297</color>
-    <!-- Shade of the secondary neutral system color at 49% lightness.
+    <!-- Shade of the secondary neutral system color at 49% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_500">#70777C</color>
-    <!-- Shade of the secondary neutral system color at 40% lightness.
+    <!-- Shade of the secondary neutral system color at 40% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_600">#585F65</color>
-    <!-- Shade of the secondary neutral system color at 30% lightness.
+    <!-- Shade of the secondary neutral system color at 30% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_700">#40484D</color>
-    <!-- Shade of the secondary neutral system color at 20% lightness.
+    <!-- Shade of the secondary neutral system color at 20% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_800">#2A3136</color>
-    <!-- Shade of the secondary neutral system color at 10% lightness.
+    <!-- Shade of the secondary neutral system color at 10% perceptual luminance (L* in L*a*b* color space).
      This value can be overlaid at runtime by OverlayManager RROs. -->
     <color name="system_neutral2_900">#161C20</color>
     <!-- Darkest shade of the secondary neutral color used by the system. Black.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 0e20e724..3718d28 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -654,15 +654,37 @@
         -->
     </integer-array>
 
+    <!-- When entering this device state (defined in device_state_configuration.xml),
+         we should wake the device. -1 to disable the feature (do not wake on any device-state
+         transition). -->
+    <integer name="config_deviceStateOnWhichToWakeUp">-1</integer>
+
     <!-- Indicate the display area rect for foldable devices in folded state. -->
     <string name="config_foldedArea"></string>
 
+    <!-- Indicates whether to enable an animation when unfolding a device or not -->
+    <bool name="config_unfoldTransitionEnabled">false</bool>
+
+    <!-- Indicates whether to enable hinge angle sensor when using unfold animation -->
+    <bool name="config_unfoldTransitionHingeAngle">false</bool>
+
     <!-- Indicates that the device supports having more than one internal display on at the same
          time. Only applicable to devices with more than one internal display. If this option is
          set to false, DisplayManager will make additional effort to ensure no more than 1 internal
          display is powered on at the same time. -->
     <bool name="config_supportsConcurrentInternalDisplays">true</bool>
 
+    <!-- Map of DeviceState to rotation lock setting. Each entry must be in the format
+         "key:value", for example: "0:1".
+          The keys are device states, and the values are one of
+          Settings.Secure.DeviceStateRotationLockSetting.
+          Any device state that doesn't have a default set here will be treated as
+          DEVICE_STATE_ROTATION_LOCK_IGNORED meaning it will not have its own rotation lock setting.
+          If this map is missing, the feature is disabled and only one global rotation lock setting
+           will apply, regardless of device state. -->
+    <string-array name="config_perDeviceStateRotationLockDefaults" />
+
+
     <!-- Desk dock behavior -->
 
     <!-- The number of degrees to rotate the display when the device is in a desk dock.
@@ -2301,6 +2323,15 @@
     <!-- Type of the tap sensor. Empty if tap is not supported. -->
     <string name="config_dozeTapSensorType" translatable="false"></string>
 
+    <!-- Type of the ambient tap sensor per device posture (defined by WM Jetpack posture).
+         Unspecified values use config_dozeTapSensor -->
+    <string-array name="config_dozeTapSensorPostureMapping" translatable="false">
+        <item></item> <!-- UNKNOWN -->
+        <item></item> <!-- CLOSED -->
+        <item></item> <!-- HALF_OPENED -->
+        <item></item> <!-- OPENED -->
+    </string-array>
+
     <!-- Type of the long press sensor. Empty if long press is not supported. -->
     <string name="config_dozeLongPressSensorType" translatable="false"></string>
 
@@ -3626,8 +3657,8 @@
     -->
     <integer name="config_largeScreenSmallestScreenWidthDp">600</integer>
 
-    <!-- True if the device is using leagacy split. -->
-    <bool name="config_useLegacySplit">true</bool>
+    <!-- True if the device is using legacy split. -->
+    <bool name="config_useLegacySplit">false</bool>
 
     <!-- True if the device supports running activities on secondary displays. -->
     <bool name="config_supportsMultiDisplay">true</bool>
@@ -4037,7 +4068,7 @@
     <string translatable="false" name="config_inCallNotificationSound">/product/media/audio/ui/InCallNotification.ogg</string>
 
     <!-- URI for default ringtone sound file to be used for silent ringer vibration -->
-    <string translatable="false" name="config_defaultRingtoneVibrationSound">/product/media/audio/ui/AttentionalHaptics.ogg</string>
+    <string translatable="false" name="config_defaultRingtoneVibrationSound"></string>
 
     <!-- Default number of notifications from the same app before they are automatically grouped by the OS -->
     <integer translatable="false" name="config_autoGroupAtCount">4</integer>
@@ -4571,6 +4602,20 @@
       -->
     </integer-array>
 
+    <!-- An array of arrays of side fingerprint sensor properties relative to each display.
+         Note: this value is temporary and is expected to be queried directly
+         from the HAL in the future. -->
+    <array name="config_sfps_sensor_props" translatable="false">
+        <!--
+            <array>
+                <item>displayId</item>
+                <item>sensorLocationX</item>
+                <item>sensorLocationY</item>
+                <item>sensorRadius</item>
+            <array>
+        -->
+    </array>
+
     <!-- How long it takes for the HW to start illuminating after the illumination is requested. -->
     <integer name="config_udfps_illumination_transition_ms">50</integer>
 
@@ -4832,8 +4877,8 @@
     <item name="config_fixedOrientationLetterboxAspectRatio" format="float" type="dimen">0.0</item>
 
     <!-- Corners radius for activity presented the letterbox mode. Values < 0 will be ignored and
-         corners of the activity won't be rounded. -->
-    <integer name="config_letterboxActivityCornersRadius">0</integer>
+         min between device bottom corner radii will be used instead. -->
+    <integer name="config_letterboxActivityCornersRadius">-1</integer>
 
     <!-- Blur radius for the Option 3 in R.integer.config_letterboxBackgroundType. Values < 0 are
         ignored and 0 is used. -->
@@ -4867,15 +4912,28 @@
             - Option 3 is selected for R.integer.config_letterboxBackgroundType and blur requested
             but isn't supported on the device or both dark scrim alpha and blur radius aren't
             provided.
-        Defaults to black if not specified.
      -->
-    <color name="config_letterboxBackgroundColor">#000</color>
+    <color name="config_letterboxBackgroundColor">@android:color/system_neutral2_500</color>
 
     <!-- Horizonal position of a center of the letterboxed app window.
         0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
         or > 1, it is ignored and central positionis used (0.5). -->
     <item name="config_letterboxHorizontalPositionMultiplier" format="float" type="dimen">0.5</item>
 
+    <!-- Whether reachability repositioning is allowed for letterboxed fullscreen apps in landscape
+        device orientation. -->
+    <bool name="config_letterboxIsReachabilityEnabled">false</bool>
+
+    <!-- Default horizonal position of the letterboxed app window when reachability is
+        enabled and an app is fullscreen in landscape device orientation. When reachability is
+        enabled, the position can change between left, center and right. This config defines the
+        default one:
+            - Option 0 - Left.
+            - Option 1 - Center.
+            - Option 2 - Right.
+        If given value is outside of this range, the option 1 (center) is assummed. -->
+    <integer name="config_letterboxDefaultPositionForReachability">1</integer>
+
     <!-- If true, hide the display cutout with display area -->
     <bool name="config_hideDisplayCutoutWithDisplayArea">false</bool>
 
@@ -5077,4 +5135,149 @@
 
     <!-- the number of the max cached processes in the system. -->
     <integer name="config_customizedMaxCachedProcesses">32</integer>
+
+    <!-- The display cutout configs for secondary built-in display. -->
+    <string name="config_secondaryBuiltInDisplayCutout" translatable="false"></string>
+    <string name="config_secondaryBuiltInDisplayCutoutRectApproximation" translatable="false">
+        @string/config_secondaryBuiltInDisplayCutout
+    </string>
+    <bool name="config_fillSecondaryBuiltInDisplayCutout">false</bool>
+    <bool name="config_maskSecondaryBuiltInDisplayCutout">false</bool>
+
+    <!-- An array contains unique ids of all built-in displays and the unique id of a display can be
+         obtained from {@link Display#getUniqueId}. This array should be set for multi-display
+         devices if there are different display related configs(e.g. display cutout, rounded corner)
+         between each built-in display.
+         It is used as an index for multi-display related configs:
+         First look up the index of the unique id of the given built-in display unique id in this
+         array and use this index to get the info in corresponding config arrays such as:
+           - config_displayCutoutPathArray
+           - config_displayCutoutApproximationRectArray
+           - config_fillBuiltInDisplayCutoutArray
+           - config_maskBuiltInDisplayCutoutArray
+           - config_waterfallCutoutArray
+           - config_roundedCornerRadiusArray
+           - config_roundedCornerTopRadiusArray
+           - config_roundedCornerBottomRadiusArray
+           - config_builtInDisplayIsRoundArray (config in SystemUI resource)
+           - config_roundedCornerMultipleRadiusArray (config in SystemUI resource)
+           - config_roundedCornerDrawableArray (config in SystemUI resource)
+           - config_roundedCornerTopDrawableArray (config in SystemUI resource)
+           - config_roundedCornerBottomDrawableArray (config in SystemUI resource)
+
+         Leave this array empty for single display device and the system will load the default main
+         built-in related configs.
+         -->
+    <string-array name="config_displayUniqueIdArray" translatable="false">
+        <!-- Example:
+        <item>"local:1234567891"</item> // main built-in display
+        <item>"local:1234567892"</item> // secondary built-in display
+        -->
+    </string-array>
+
+    <!-- The display cutout path config for each display in a multi-display device. -->
+    <string-array name="config_displayCutoutPathArray" translatable="false">
+        <item>@string/config_mainBuiltInDisplayCutout</item>
+        <item>@string/config_secondaryBuiltInDisplayCutout</item>
+    </string-array>
+
+    <!-- The display cutout approximation rect config for each display in a multi-display device.
+         -->
+    <string-array name="config_displayCutoutApproximationRectArray" translatable="false">
+        <item>@string/config_mainBuiltInDisplayCutoutRectApproximation</item>
+        <item>@string/config_secondaryBuiltInDisplayCutoutRectApproximation</item>
+    </string-array>
+
+    <!-- The maskBuiltInDisplayCutout config for each display in a multi-display device. -->
+    <array name="config_maskBuiltInDisplayCutoutArray" translatable="false">
+        <item>@bool/config_maskMainBuiltInDisplayCutout</item>
+        <item>@bool/config_maskSecondaryBuiltInDisplayCutout</item>
+    </array>
+
+    <!-- The fillBuiltInDisplayCutout config for each display in a multi-display device. -->
+    <array name="config_fillBuiltInDisplayCutoutArray" translatable="false">
+        <item>@bool/config_fillMainBuiltInDisplayCutout</item>
+        <item>@bool/config_fillSecondaryBuiltInDisplayCutout</item>
+    </array>
+
+    <array name="config_mainBuiltInDisplayWaterfallCutout" translatable="false">
+        <item>@dimen/waterfall_display_left_edge_size</item>
+        <item>@dimen/waterfall_display_top_edge_size</item>
+        <item>@dimen/waterfall_display_right_edge_size</item>
+        <item>@dimen/waterfall_display_bottom_edge_size</item>
+    </array>
+
+    <array name="config_secondaryBuiltInDisplayWaterfallCutout" translatable="false">
+        <item>@dimen/secondary_waterfall_display_left_edge_size</item>
+        <item>@dimen/secondary_waterfall_display_top_edge_size</item>
+        <item>@dimen/secondary_waterfall_display_right_edge_size</item>
+        <item>@dimen/secondary_waterfall_display_bottom_edge_size</item>
+    </array>
+
+    <!-- The waterfall cutout config for each display in a multi-display device. -->
+    <array name="config_waterfallCutoutArray" translatable="false">
+        <item>@array/config_mainBuiltInDisplayWaterfallCutout</item>
+        <item>@array/config_secondaryBuiltInDisplayWaterfallCutout</item>
+    </array>
+
+    <!-- The component name of the activity for the companion-device-manager notification access
+     confirmation. -->
+    <string name="config_notificationAccessConfirmationActivity" translatable="false">
+        com.android.settings/com.android.settings.notification.NotificationAccessConfirmationActivity
+    </string>
+
+    <!-- Whether the airplane mode should be reset when device boots in non-safemode after exiting
+         from safemode.
+         This flag should be enabled only when the product does not have any UI to toggle airplane
+         mode like automotive devices.-->
+    <bool name="config_autoResetAirplaneMode">false</bool>
+
+    <bool name="config_secondaryBuiltInDisplayIsRound">@bool/config_windowIsRound</bool>
+
+    <!-- The display round config for each display in a multi-display device. -->
+    <array name="config_builtInDisplayIsRoundArray" translatable="false">
+        <item>@bool/config_mainBuiltInDisplayIsRound</item>
+        <item>@bool/config_secondaryBuiltInDisplayIsRound</item>
+    </array>
+
+    <!-- The rounded corner radius for each display in a multi-display device. -->
+    <array name="config_roundedCornerRadiusArray" translatable="false">
+        <item>@dimen/rounded_corner_radius</item>
+        <item>@dimen/secondary_rounded_corner_radius</item>
+    </array>
+
+    <!-- The top rounded corner radius for each display in a multi-display device. -->
+    <array name="config_roundedCornerTopRadiusArray" translatable="false">
+        <item>@dimen/rounded_corner_radius_top</item>
+        <item>@dimen/secondary_rounded_corner_radius_top</item>
+    </array>
+
+    <!-- The bottom rounded corner radius for each display in a multi-display device. -->
+    <array name="config_roundedCornerBottomRadiusArray" translatable="false">
+        <item>@dimen/rounded_corner_radius_bottom</item>
+        <item>@dimen/secondary_rounded_corner_radius_bottom</item>
+    </array>
+
+    <!-- The rounded corner radius adjustment for each display in a multi-display device. -->
+    <array name="config_roundedCornerRadiusAdjustmentArray" translatable="false">
+        <item>@dimen/rounded_corner_radius_adjustment</item>
+        <item>@dimen/secondary_rounded_corner_radius_adjustment</item>
+    </array>
+
+    <!-- The rounded corner radius top adjustment for each display in a multi-display device. -->
+    <array name="config_roundedCornerTopRadiusAdjustmentArray" translatable="false">
+        <item>@dimen/rounded_corner_radius_top_adjustment</item>
+        <item>@dimen/secondary_rounded_corner_radius_top_adjustment</item>
+    </array>
+
+    <!-- The rounded corner radius bottom adjustment for each display in a multi-display device. -->
+    <array name="config_roundedCornerBottomRadiusAdjustmentArray" translatable="false">
+        <item>@dimen/rounded_corner_radius_bottom_adjustment</item>
+        <item>@dimen/secondary_rounded_corner_radius_bottom_adjustment</item>
+    </array>
+
+    <!-- Shape of the work badge icon for viewport size 24. -->
+    <string name="config_work_badge_path_24" translatable="false">
+        M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z
+    </string>
 </resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index a666a5b..ab923d0 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -55,6 +55,8 @@
     <dimen name="navigation_bar_height_landscape">48dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
     <dimen name="navigation_bar_width">48dp</dimen>
+    <!-- Height of the bottom taskbar not including decorations like rounded corners. -->
+    <dimen name="taskbar_frame_height">60dp</dimen>
     <!-- How much we expand the touchable region of the status bar below the notch to catch touches
          that just start below the notch. -->
     <dimen name="display_cutout_touchable_region_size">12dp</dimen>
@@ -926,7 +928,7 @@
 
     <dimen name="chooser_action_button_icon_size">18dp</dimen>
 
-    <!-- For Waterfall Display -->
+    <!-- For main built-in Waterfall Display -->
     <dimen name="waterfall_display_left_edge_size">0px</dimen>
     <dimen name="waterfall_display_top_edge_size">0px</dimen>
     <dimen name="waterfall_display_right_edge_size">0px</dimen>
@@ -949,4 +951,18 @@
     <dimen name="starting_surface_icon_size">160dp</dimen>
     <!-- The default width/height of the icon on the spec of adaptive icon drawable. -->
     <dimen name="starting_surface_default_icon_size">108dp</dimen>
+
+    <!-- For secondary built-in Waterfall Display -->
+    <dimen name="secondary_waterfall_display_left_edge_size">0px</dimen>
+    <dimen name="secondary_waterfall_display_top_edge_size">0px</dimen>
+    <dimen name="secondary_waterfall_display_right_edge_size">0px</dimen>
+    <dimen name="secondary_waterfall_display_bottom_edge_size">0px</dimen>
+
+    <!-- Rounded corner settings for secondary built-in display -->
+    <dimen name="secondary_rounded_corner_radius">0px</dimen>
+    <dimen name="secondary_rounded_corner_radius_top">0px</dimen>
+    <dimen name="secondary_rounded_corner_radius_bottom">0px</dimen>
+    <dimen name="secondary_rounded_corner_radius_adjustment">0px</dimen>
+    <dimen name="secondary_rounded_corner_radius_top_adjustment">0px</dimen>
+    <dimen name="secondary_rounded_corner_radius_bottom_adjustment">0px</dimen>
 </resources>
diff --git a/core/res/res/values/dimens_car.xml b/core/res/res/values/dimens_car.xml
index 0ef60c4..36f1edb 100644
--- a/core/res/res/values/dimens_car.xml
+++ b/core/res/res/values/dimens_car.xml
@@ -110,10 +110,10 @@
     <dimen name="car_textview_fading_edge_length">40dp</dimen>
 
     <!-- Dialog start padding for button bar layout -->
-    <dimen name="button_bar_layout_start_padding">@*android:dimen/car_keyline_1</dimen>
+    <dimen name="button_bar_layout_start_padding">@dimen/car_padding_2</dimen>
 
     <!-- Dialog end padding for button bar layout -->
-    <dimen name="button_bar_layout_end_padding">@*android:dimen/car_keyline_1</dimen>
+    <dimen name="button_bar_layout_end_padding">@dimen/car_padding_2</dimen>
 
     <!-- Dialog top padding for button bar layout -->
     <dimen name="button_bar_layout_top_padding">@*android:dimen/car_padding_2</dimen>
@@ -122,7 +122,7 @@
     <dimen name="button_layout_height">@*android:dimen/car_card_action_bar_height</dimen>
 
     <!-- Dialog button end margin -->
-    <dimen name="button_end_margin">@*android:dimen/car_padding_4</dimen>
+    <dimen name="button_end_margin">@*android:dimen/car_padding_2</dimen>
 
     <!-- Dialog top padding when there is no title -->
     <dimen name="dialog_no_title_padding_top">@*android:dimen/car_padding_4</dimen>
diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml
index c4838b8..84f82fd 100644
--- a/core/res/res/values/ids.xml
+++ b/core/res/res/values/ids.xml
@@ -254,6 +254,15 @@
   <!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_IME_ENTER}. -->
   <item type="id" name="accessibilityActionImeEnter" />
 
+  <!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_START}. -->
+  <item type="id" name="accessibilityActionDragStart" />
+
+  <!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_DROP}. -->
+  <item type="id" name="accessibilityActionDragDrop" />
+
+  <!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_CANCEL}. -->
+  <item type="id" name="accessibilityActionDragCancel" />
+
   <!-- View tag for remote views to store the index of the next child when adding nested remote views dynamically. -->
   <item type="id" name="remote_views_next_child" />
 
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 2403a60..7d48904 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3201,10 +3201,88 @@
     <public type="string" name="config_defaultRingtoneVibrationSound" id="0x0104003b" />
 
   <!-- ===============================================================
+    Resources added in version S-V2 of the platform
+
+    NOTE: add <public> elements within a <staging-public-group> like so:
+
+    <staging-public-group type="attr" first-id="0x01ff0000">
+        <public name="exampleAttr1" />
+        <public name="exampleAttr2" />
+    </staging-public-group>
+
+    To add a new <staging-public-group> block, find the id value for the
+    last <staging-public-group> block defined for thie API level, and
+    subtract 0x00010000 from it to get to the id of the new block.
+
+    For example, if the block closest to the end of this file has an id of
+    0x01ee0000, the id of the new block should be 0x01ed0000
+    (0x01ee0000 - 0x00010000 = 0x01ed0000).
+    =============================================================== -->
+  <eat-comment />
+
+  <staging-public-group type="attr" first-id="0x01ff0000">
+    <public name="shouldUseDefaultUnfoldTransition" />
+  </staging-public-group>
+
+  <staging-public-group type="id" first-id="0x01fe0000">
+    <public name="accessibilityActionDragStart" />
+    <public name="accessibilityActionDragDrop" />
+    <public name="accessibilityActionDragCancel" />
+  </staging-public-group>
+
+  <staging-public-group type="style" first-id="0x01fd0000">
+  </staging-public-group>
+
+  <staging-public-group type="string" first-id="0x01fc0000">
+  </staging-public-group>
+
+  <staging-public-group type="dimen" first-id="0x01fb0000">
+  </staging-public-group>
+
+  <staging-public-group type="color" first-id="0x01fa0000">
+  </staging-public-group>
+
+  <staging-public-group type="array" first-id="0x01f90000">
+  </staging-public-group>
+
+  <staging-public-group type="drawable" first-id="0x01f80000">
+  </staging-public-group>
+
+  <staging-public-group type="layout" first-id="0x01f70000">
+  </staging-public-group>
+
+  <staging-public-group type="anim" first-id="0x01f60000">
+  </staging-public-group>
+
+  <staging-public-group type="animator" first-id="0x01f50000">
+  </staging-public-group>
+
+  <staging-public-group type="interpolator" first-id="0x01f40000">
+  </staging-public-group>
+
+  <staging-public-group type="mipmap" first-id="0x01f30000">
+  </staging-public-group>
+
+  <staging-public-group type="integer" first-id="0x01f20000">
+  </staging-public-group>
+
+  <staging-public-group type="transition" first-id="0x01f10000">
+  </staging-public-group>
+
+  <staging-public-group type="raw" first-id="0x01f00000">
+  </staging-public-group>
+
+  <staging-public-group type="bool" first-id="0x01ef0000">
+  </staging-public-group>
+
+  <staging-public-group type="fraction" first-id="0x01ee0000">
+  </staging-public-group>
+
+  <!-- ===============================================================
        DO NOT ADD UN-GROUPED ITEMS HERE
 
        Any new items (attrs, styles, ids, etc.) *must* be added in a
-       public-group block, as the preceding comment explains.
+       staging-public-group block, as the preceding comment explains.
        Items added outside of a group may have their value recalculated
        every time something new is added to this file.
        =============================================================== -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b58638c..bb56b8d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4558,7 +4558,7 @@
     <string name="accessibility_shortcut_multiple_service_warning">Holding down both volume keys for a few seconds turns on accessibility features. This may change how your device works.\n\nCurrent features:\n<xliff:g id="service" example="TalkBack">%1$s</xliff:g>\nYou can change selected features in Settings > Accessibility.</string>
 
     <!-- Used in multiple service warning to list current features. [CHAR LIMIT=none] -->
-    <string name="accessibility_shortcut_multiple_service_list">\t• <xliff:g id="service" example="TalkBack">%1$s</xliff:g>\n</string>
+    <string name="accessibility_shortcut_multiple_service_list">\u0020• <xliff:g id="service" example="TalkBack">%1$s</xliff:g>\n</string>
 
     <!-- Dialog title for dialog shown when this accessibility shortcut is activated, and we want to confirm that the user understands what's going to happen. [CHAR LIMIT=none] -->
     <string name="accessibility_shortcut_single_service_warning_title">Turn on <xliff:g id="service" example="TalkBack">%1$s</xliff:g> shortcut?</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index bac9cf2..9553f95 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -966,6 +966,7 @@
     <style name="TextAppearance.Toast">
         <item name="fontFamily">@*android:string/config_bodyFontFamily</item>
         <item name="textSize">14sp</item>
+        <item name="lineHeight">20sp</item>
         <item name="textColor">?android:attr/textColorPrimary</item>
     </style>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 24da7b2..758990d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1742,6 +1742,7 @@
   <java-symbol type="dimen" name="navigation_bar_height_car_mode" />
   <java-symbol type="dimen" name="navigation_bar_height_landscape_car_mode" />
   <java-symbol type="dimen" name="navigation_bar_width_car_mode" />
+  <java-symbol type="dimen" name="taskbar_frame_height" />
   <java-symbol type="dimen" name="status_bar_height" />
   <java-symbol type="dimen" name="display_cutout_touchable_region_size" />
   <java-symbol type="dimen" name="system_gestures_start_threshold" />
@@ -2221,6 +2222,8 @@
   <java-symbol type="string" name="config_primaryLocationTimeZoneProviderPackageName" />
   <java-symbol type="bool" name="config_enableSecondaryLocationTimeZoneProvider" />
   <java-symbol type="string" name="config_secondaryLocationTimeZoneProviderPackageName" />
+  <java-symbol type="bool" name="config_autoResetAirplaneMode" />
+  <java-symbol type="string" name="config_notificationAccessConfirmationActivity" />
 
   <java-symbol type="layout" name="resolver_list" />
   <java-symbol type="id" name="resolver_list" />
@@ -2608,6 +2611,7 @@
   <java-symbol type="array" name="config_biometric_sensors" />
   <java-symbol type="bool" name="allow_test_udfps" />
   <java-symbol type="array" name="config_udfps_sensor_props" />
+  <java-symbol type="array" name="config_sfps_sensor_props" />
   <java-symbol type="integer" name="config_udfps_illumination_transition_ms" />
   <java-symbol type="bool" name="config_is_powerbutton_fps" />
   <java-symbol type="array" name="config_udfps_enroll_stage_thresholds" />
@@ -3252,6 +3256,7 @@
 
   <java-symbol type="string" name="config_dozeDoubleTapSensorType" />
   <java-symbol type="string" name="config_dozeTapSensorType" />
+  <java-symbol type="array" name="config_dozeTapSensorPostureMapping" />
   <java-symbol type="bool" name="config_dozePulsePickup" />
 
   <!-- Used for MimeIconUtils. -->
@@ -3843,8 +3848,13 @@
 
   <!-- For Foldables -->
   <java-symbol type="array" name="config_foldedDeviceStates" />
+  <java-symbol type="integer" name="config_deviceStateOnWhichToWakeUp" />
   <java-symbol type="string" name="config_foldedArea" />
   <java-symbol type="bool" name="config_supportsConcurrentInternalDisplays" />
+  <java-symbol type="bool" name="config_unfoldTransitionEnabled" />
+  <java-symbol type="bool" name="config_unfoldTransitionHingeAngle" />
+  <java-symbol type="array" name="config_perDeviceStateRotationLockDefaults" />
+
 
   <java-symbol type="array" name="config_disableApksUnlessMatchedSku_apk_list" />
   <java-symbol type="array" name="config_disableApkUnlessMatchedSku_skus_list" />
@@ -4253,6 +4263,8 @@
   <java-symbol type="integer" name="config_letterboxBackgroundType" />
   <java-symbol type="color" name="config_letterboxBackgroundColor" />
   <java-symbol type="dimen" name="config_letterboxHorizontalPositionMultiplier" />
+  <java-symbol type="bool" name="config_letterboxIsReachabilityEnabled" />
+  <java-symbol type="integer" name="config_letterboxDefaultPositionForReachability" />
 
   <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
 
@@ -4437,4 +4449,36 @@
   <java-symbol type="integer" name="config_customizedMaxCachedProcesses" />
 
   <java-symbol type="color" name="overview_background"/>
+
+  <java-symbol type="string" name="config_secondaryBuiltInDisplayCutout" />
+  <java-symbol type="string" name="config_secondaryBuiltInDisplayCutoutRectApproximation" />
+  <java-symbol type="bool" name="config_fillSecondaryBuiltInDisplayCutout" />
+  <java-symbol type="bool" name="config_maskSecondaryBuiltInDisplayCutout" />
+  <java-symbol type="array" name="config_displayUniqueIdArray" />
+  <java-symbol type="array" name="config_displayCutoutPathArray" />
+  <java-symbol type="array" name="config_displayCutoutApproximationRectArray" />
+  <java-symbol type="array" name="config_fillBuiltInDisplayCutoutArray" />
+  <java-symbol type="array" name="config_maskBuiltInDisplayCutoutArray" />
+  <java-symbol type="dimen" name="secondary_waterfall_display_left_edge_size" />
+  <java-symbol type="dimen" name="secondary_waterfall_display_top_edge_size" />
+  <java-symbol type="dimen" name="secondary_waterfall_display_right_edge_size" />
+  <java-symbol type="dimen" name="secondary_waterfall_display_bottom_edge_size" />
+  <java-symbol type="array" name="config_mainBuiltInDisplayWaterfallCutout" />
+  <java-symbol type="array" name="config_secondaryBuiltInDisplayWaterfallCutout" />
+  <java-symbol type="array" name="config_waterfallCutoutArray" />
+
+  <java-symbol type="dimen" name="secondary_rounded_corner_radius" />
+  <java-symbol type="dimen" name="secondary_rounded_corner_radius_top" />
+  <java-symbol type="dimen" name="secondary_rounded_corner_radius_bottom" />
+  <java-symbol type="dimen" name="secondary_rounded_corner_radius_adjustment" />
+  <java-symbol type="dimen" name="secondary_rounded_corner_radius_top_adjustment" />
+  <java-symbol type="dimen" name="secondary_rounded_corner_radius_bottom_adjustment" />
+  <java-symbol type="array" name="config_roundedCornerRadiusArray" />
+  <java-symbol type="array" name="config_roundedCornerTopRadiusArray" />
+  <java-symbol type="array" name="config_roundedCornerBottomRadiusArray" />
+  <java-symbol type="array" name="config_roundedCornerRadiusAdjustmentArray" />
+  <java-symbol type="array" name="config_roundedCornerTopRadiusAdjustmentArray" />
+  <java-symbol type="array" name="config_roundedCornerBottomRadiusAdjustmentArray" />
+  <java-symbol type="bool" name="config_secondaryBuiltInDisplayIsRound" />
+  <java-symbol type="array" name="config_builtInDisplayIsRoundArray" />
 </resources>
diff --git a/core/res/res/xml/power_profile.xml b/core/res/res/xml/power_profile.xml
index 166edca..d310736 100644
--- a/core/res/res/xml/power_profile.xml
+++ b/core/res/res/xml/power_profile.xml
@@ -27,9 +27,33 @@
        are totally dependent on the platform and can vary
        significantly, so should be measured on the shipping platform
        with a power meter. -->
-  <item name="ambient.on">0.1</item>  <!-- ~100mA -->
-  <item name="screen.on">0.1</item>  <!-- ~100mA -->
-  <item name="screen.full">0.1</item>  <!-- ~100mA -->
+
+  <!-- Display related values. -->
+  <!-- Average battery current draw of display0 while in ambient mode, including backlight.
+       There must be one of these for each display, labeled:
+       ambient.on.display0, ambient.on.display1, etc...
+
+       Each display suffix number should match it's ordinal in its display device config.
+  -->
+  <item name="ambient.on.display0">0.1</item>  <!-- ~100mA -->
+  <!-- Average battery current draw of display0 while on without backlight.
+       There must be one of these for each display, labeled:
+       screen.on.display0, screen.on.display1, etc...
+
+       Each display suffix number should match it's ordinal in its display device config.
+  -->
+  <item name="screen.on.display0">0.1</item>  <!-- ~100mA -->
+  <!-- Average battery current draw of the backlight at full brightness.
+       The full current draw of display N at full brightness should be the sum of screen.on.displayN
+       and screen.full.displayN
+
+       There must be one of these for each display, labeled:
+       screen.full.display0, screen.full.display1, etc...
+
+       Each display suffix number should match it's ordinal in its display device config.
+  -->
+  <item name="screen.full.display0">0.1</item>  <!-- ~100mA -->
+
   <item name="bluetooth.active">0.1</item> <!-- Bluetooth data transfer, ~10mA -->
   <item name="bluetooth.on">0.1</item>  <!-- Bluetooth on & connectable, but not connected, ~0.1mA -->
   <item name="wifi.on">0.1</item>  <!-- ~3mA -->
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
index 7c6271c..c194989 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
@@ -65,6 +65,7 @@
     @Mock ITunerSession mHalTunerSessionMock;
     private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks;
 
+    private final Object mLock = new Object();
     // RadioModule under test
     private RadioModule mRadioModule;
 
@@ -96,7 +97,7 @@
 
         mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(0, "",
                   0, "", "", "", "", 0, 0, false, false, null, false, new int[] {}, new int[] {},
-                  null, null));
+                  null, null), mLock);
 
         doAnswer((Answer) invocation -> {
             mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
diff --git a/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java b/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java
index c65ef9a..53ba140 100644
--- a/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java
+++ b/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java
@@ -16,18 +16,40 @@
 
 package android.accessibilityservice;
 
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.ImageReader;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityEvent;
+import android.window.WindowTokenClient;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,6 +64,8 @@
 public class AccessibilityServiceTest {
     private static final String TAG = "AccessibilityServiceTest";
     private static final int CONNECTION_ID = 1;
+    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(
+            TYPE_ACCESSIBILITY_OVERLAY);
 
     private static class AccessibilityServiceTestClass extends AccessibilityService {
         private IAccessibilityServiceClient mCallback;
@@ -49,7 +73,11 @@
 
         AccessibilityServiceTestClass() {
             super();
-            attachBaseContext(InstrumentationRegistry.getContext());
+            Context context = ApplicationProvider.getApplicationContext();
+            final Display display = context.getSystemService(DisplayManager.class)
+                    .getDisplay(DEFAULT_DISPLAY);
+
+            attachBaseContext(context.createTokenContext(new WindowTokenClient(), display));
             mLooper = InstrumentationRegistry.getContext().getMainLooper();
         }
 
@@ -78,14 +106,33 @@
     private @Mock IBinder mMockIBinder;
     private IAccessibilityServiceClient mServiceInterface;
     private AccessibilityServiceTestClass mService;
+    private final SparseArray<IBinder> mWindowTokens = new SparseArray<>();
 
     @Before
-    public void setUp() throws RemoteException {
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mService = new AccessibilityServiceTestClass();
+        mService.onCreate();
         mService.setupCallback(mMockClientForCallback);
         mServiceInterface = (IAccessibilityServiceClient) mService.onBind(new Intent());
         mServiceInterface.init(mMockConnection, CONNECTION_ID, mMockIBinder);
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            final int displayId = (int) args[0];
+            final IBinder token = new Binder();
+            WindowManagerGlobal.getWindowManagerService().addWindowToken(token,
+                    TYPE_ACCESSIBILITY_OVERLAY, displayId, null /* options */);
+            mWindowTokens.put(displayId, token);
+            return token;
+        }).when(mMockConnection).getOverlayWindowToken(anyInt());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        for (int i = mWindowTokens.size() - 1; i >= 0; --i) {
+            WindowManagerGlobal.getWindowManagerService().removeWindowToken(
+                    mWindowTokens.valueAt(i), mWindowTokens.keyAt(i));
+        }
     }
 
     @Test
@@ -101,4 +148,79 @@
 
         verify(mMockConnection).getSystemActions();
     }
+
+    @Test
+    public void testAddViewWithA11yServiceDerivedDisplayContext() throws Exception {
+        try (VirtualDisplaySession session = new VirtualDisplaySession()) {
+            final Context context = mService.createDisplayContext(session.getDisplay());
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                    () -> context.getSystemService(WindowManager.class)
+                            .addView(new View(context), mParams)
+            );
+        }
+    }
+
+    @Test
+    public void testAddViewWithA11yServiceDerivedWindowContext() throws Exception {
+        try (VirtualDisplaySession session = new VirtualDisplaySession()) {
+            final Context context = mService.createDisplayContext(session.getDisplay())
+                    .createWindowContext(TYPE_ACCESSIBILITY_OVERLAY, null /* options */);
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                    () -> context.getSystemService(WindowManager.class)
+                            .addView(new View(context), mParams)
+            );
+        }
+    }
+
+    @Test
+    public void testAddViewWithA11yServiceDerivedWindowContextWithDisplay() throws Exception {
+        try (VirtualDisplaySession session = new VirtualDisplaySession()) {
+            final Context context = mService.createWindowContext(session.getDisplay(),
+                    TYPE_ACCESSIBILITY_OVERLAY, null /* options */);
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                    () -> context.getSystemService(WindowManager.class)
+                            .addView(new View(context), mParams)
+            );
+        }
+    }
+
+    @Test(expected = WindowManager.BadTokenException.class)
+    public void testAddViewWithA11yServiceDerivedWindowContextWithDifferentType()
+            throws Exception {
+        try (VirtualDisplaySession session = new VirtualDisplaySession()) {
+            final Context context = mService.createWindowContext(session.getDisplay(),
+                    TYPE_APPLICATION_OVERLAY, null /* options */);
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                    () -> context.getSystemService(WindowManager.class)
+                            .addView(new View(context), mParams)
+            );
+        }
+    }
+
+
+    private static class VirtualDisplaySession implements AutoCloseable {
+        private final VirtualDisplay mVirtualDisplay;
+
+        VirtualDisplaySession() {
+            final DisplayManager displayManager = ApplicationProvider.getApplicationContext()
+                    .getSystemService(DisplayManager.class);
+            final int width = 800;
+            final int height = 480;
+            final int density = 160;
+            ImageReader reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888,
+                    2 /* maxImages */);
+            mVirtualDisplay = displayManager.createVirtualDisplay(
+                    TAG, width, height, density, reader.getSurface(),
+                    VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
+        }
+
+        private Display getDisplay() {
+            return mVirtualDisplay.getDisplay();
+        }
+
+        @Override
+        public void close() throws Exception {
+            mVirtualDisplay.release();
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index 49e7966..1f27063 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -291,7 +291,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 194242735)
     public void testHandleActivityConfigurationChanged_EnsureUpdatesProcessedInOrder()
             throws Exception {
         final TestActivity activity = mActivityTestRule.launchActivity(new Intent());
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 9915e38..3e261a7 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -156,7 +156,8 @@
                 .setProcState(procState).setState(bundle).setPersistentState(persistableBundle)
                 .setPendingResults(resultInfoList()).setPendingNewIntents(referrerIntentList())
                 .setIsForward(true).setAssistToken(assistToken)
-                .setShareableActivityToken(shareableActivityToken).build();
+                .setShareableActivityToken(shareableActivityToken)
+                .build();
 
         LaunchActivityItem emptyItem = new LaunchActivityItemBuilder().build();
         LaunchActivityItem item = itemSupplier.get();
diff --git a/core/tests/coretests/src/android/content/ContextTest.java b/core/tests/coretests/src/android/content/ContextTest.java
index d1776fb..3d7d807 100644
--- a/core/tests/coretests/src/android/content/ContextTest.java
+++ b/core/tests/coretests/src/android/content/ContextTest.java
@@ -32,7 +32,6 @@
 import android.graphics.PixelFormat;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
-import android.inputmethodservice.InputMethodService;
 import android.media.ImageReader;
 import android.os.UserHandle;
 import android.view.Display;
@@ -140,13 +139,6 @@
     }
 
     @Test
-    public void testIsUiContext_InputMethodService_returnsTrue() {
-        final InputMethodService ims = new InputMethodService();
-
-        assertTrue(ims.isUiContext());
-    }
-
-    @Test
     public void testGetDisplayFromDisplayContextDerivedContextOnPrimaryDisplay() {
         verifyGetDisplayFromDisplayContextDerivedContext(false /* onSecondaryDisplay */);
     }
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index 8fd1af8..4f1da1b2 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -111,7 +111,7 @@
                 new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(),
                 mMockController, 10 /* durationMs */, new LinearInterpolator(),
                 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */);
-        mController.mReadyDispatched = true;
+        mController.setReadyDispatched(true);
     }
 
     @Test
@@ -197,7 +197,7 @@
 
     @Test
     public void testCancelled_beforeReadyDispatched() {
-        mController.mReadyDispatched = false;
+        mController.setReadyDispatched(false);
         mController.cancel();
         assertFalse(mController.isReady());
         assertFalse(mController.isFinished());
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 6301f32..227a8657 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -19,13 +19,17 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.InsetsController.ANIMATION_TYPE_HIDE;
 import static android.view.InsetsController.ANIMATION_TYPE_NONE;
+import static android.view.InsetsController.ANIMATION_TYPE_RESIZE;
 import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
+import static android.view.InsetsController.AnimationType;
 import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED;
 import static android.view.InsetsSourceConsumer.ShowResult.SHOW_IMMEDIATELY;
+import static android.view.InsetsState.FIRST_TYPE;
 import static android.view.InsetsState.ITYPE_CAPTION_BAR;
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.InsetsState.LAST_TYPE;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
@@ -662,6 +666,97 @@
     }
 
     @Test
+    public void testResizeAnimation_insetsTypes() {
+        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+            final @AnimationType int expectedAnimationType =
+                    (InsetsState.toPublicType(type) & Type.systemBars()) != 0
+                            ? ANIMATION_TYPE_RESIZE
+                            : ANIMATION_TYPE_NONE;
+            doTestResizeAnimation_insetsTypes(type, expectedAnimationType);
+        }
+    }
+
+    private void doTestResizeAnimation_insetsTypes(@InternalInsetsType int type,
+            @AnimationType int expectedAnimationType) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            final InsetsState state1 = new InsetsState();
+            state1.getSource(type).setVisible(true);
+            state1.getSource(type).setFrame(0, 0, 500, 50);
+            final InsetsState state2 = new InsetsState(state1, true /* copySources */);
+            state2.getSource(type).setFrame(0, 0, 500, 60);
+            final String message = "Animation type of " + InsetsState.typeToString(type) + ":";
+
+            // New insets source won't cause the resize animation.
+            mController.onStateChanged(state1);
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+
+            // Changing frame might cause the resize animation. This depends on the insets type.
+            mController.onStateChanged(state2);
+            assertEquals(message, expectedAnimationType, mController.getAnimationType(type));
+
+            // Cancel the existing animations for the next iteration.
+            mController.cancelExistingAnimations();
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+        });
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    @Test
+    public void testResizeAnimation_displayFrame() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            final @InternalInsetsType int type = ITYPE_STATUS_BAR;
+            final InsetsState state1 = new InsetsState();
+            state1.setDisplayFrame(new Rect(0, 0, 500, 1000));
+            state1.getSource(type).setFrame(0, 0, 500, 50);
+            final InsetsState state2 = new InsetsState(state1, true /* copySources */);
+            state2.setDisplayFrame(new Rect(0, 0, 500, 1010));
+            state2.getSource(type).setFrame(0, 0, 500, 60);
+            final String message = "There must not be resize animation.";
+
+            // New insets source won't cause the resize animation.
+            mController.onStateChanged(state1);
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+
+            // Changing frame won't cause the resize animation if the display frame is also changed.
+            mController.onStateChanged(state2);
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+        });
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    @Test
+    public void testResizeAnimation_visibility() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            final @InternalInsetsType int type = ITYPE_STATUS_BAR;
+            final InsetsState state1 = new InsetsState();
+            state1.getSource(type).setVisible(true);
+            state1.getSource(type).setFrame(0, 0, 500, 50);
+            final InsetsState state2 = new InsetsState(state1, true /* copySources */);
+            state2.getSource(type).setVisible(false);
+            state2.getSource(type).setFrame(0, 0, 500, 60);
+            final InsetsState state3 = new InsetsState(state2, true /* copySources */);
+            state3.getSource(type).setVisible(true);
+            state3.getSource(type).setFrame(0, 0, 500, 70);
+            final String message = "There must not be resize animation.";
+
+            // New insets source won't cause the resize animation.
+            mController.onStateChanged(state1);
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+
+            // Changing source visibility (visible --> invisible) won't cause the resize animation.
+            // The previous source and the current one must be both visible.
+            mController.onStateChanged(state2);
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+
+            // Changing source visibility (invisible --> visible) won't cause the resize animation.
+            // The previous source and the current one must be both visible.
+            mController.onStateChanged(state3);
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+        });
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    @Test
     public void testCaptionInsetsStateAssemble() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mController.onFrameChanged(new Rect(0, 0, 100, 300));
@@ -698,15 +793,15 @@
     @Test
     public void testRequestedState() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            final InsetsState state = mTestHost.getRequestedState();
+            final InsetsVisibilities request = mTestHost.getRequestedVisibilities();
 
             mController.hide(statusBars() | navigationBars());
-            assertFalse(state.getSourceOrDefaultVisibility(ITYPE_STATUS_BAR));
-            assertFalse(state.getSourceOrDefaultVisibility(ITYPE_NAVIGATION_BAR));
+            assertFalse(request.getVisibility(ITYPE_STATUS_BAR));
+            assertFalse(request.getVisibility(ITYPE_NAVIGATION_BAR));
 
             mController.show(statusBars() | navigationBars());
-            assertTrue(state.getSourceOrDefaultVisibility(ITYPE_STATUS_BAR));
-            assertTrue(state.getSourceOrDefaultVisibility(ITYPE_NAVIGATION_BAR));
+            assertTrue(request.getVisibility(ITYPE_STATUS_BAR));
+            assertTrue(request.getVisibility(ITYPE_NAVIGATION_BAR));
         });
     }
 
@@ -837,20 +932,20 @@
 
     public static class TestHost extends ViewRootInsetsControllerHost {
 
-        private final InsetsState mRequestedState = new InsetsState();
+        private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
 
         TestHost(ViewRootImpl viewRoot) {
             super(viewRoot);
         }
 
         @Override
-        public void onInsetsModified(InsetsState insetsState) {
-            mRequestedState.set(insetsState, true);
-            super.onInsetsModified(insetsState);
+        public void updateRequestedVisibilities(InsetsVisibilities visibilities) {
+            mRequestedVisibilities.set(visibilities);
+            super.updateRequestedVisibilities(visibilities);
         }
 
-        public InsetsState getRequestedState() {
-            return mRequestedState;
+        public InsetsVisibilities getRequestedVisibilities() {
+            return mRequestedVisibilities;
         }
     }
 }
diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 3bd2939..bf8bb76 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -216,9 +216,9 @@
         mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(0, 0, 100, 300));
         mState.getSource(ITYPE_CAPTION_BAR).setVisible(true);
 
-        Rect visibleInsets = mState.calculateVisibleInsets(
+        Insets visibleInsets = mState.calculateVisibleInsets(
                 new Rect(0, 0, 100, 400), SOFT_INPUT_ADJUST_NOTHING);
-        assertEquals(new Rect(0, 300, 0, 0), visibleInsets);
+        assertEquals(Insets.of(0, 300, 0, 0), visibleInsets);
     }
 
     @Test
@@ -226,9 +226,9 @@
         mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(0, 0, 100, 300));
         mState.getSource(ITYPE_CAPTION_BAR).setVisible(true);
 
-        Rect visibleInsets = mState.calculateVisibleInsets(
+        Insets visibleInsets = mState.calculateVisibleInsets(
                 new Rect(0, 0, 150, 400), SOFT_INPUT_ADJUST_NOTHING);
-        assertEquals(new Rect(0, 300, 0, 0), visibleInsets);
+        assertEquals(Insets.of(0, 300, 0, 0), visibleInsets);
     }
 
     @Test
@@ -413,9 +413,9 @@
         // Make sure bottom gestures are ignored
         mState.getSource(ITYPE_BOTTOM_GESTURES).setFrame(new Rect(0, 100, 100, 300));
         mState.getSource(ITYPE_BOTTOM_GESTURES).setVisible(true);
-        Rect visibleInsets = mState.calculateVisibleInsets(
+        Insets visibleInsets = mState.calculateVisibleInsets(
                 new Rect(0, 0, 100, 300), SOFT_INPUT_ADJUST_PAN);
-        assertEquals(new Rect(0, 100, 0, 100), visibleInsets);
+        assertEquals(Insets.of(0, 100, 0, 100), visibleInsets);
     }
 
     @Test
@@ -428,9 +428,9 @@
         // Make sure bottom gestures are ignored
         mState.getSource(ITYPE_BOTTOM_GESTURES).setFrame(new Rect(0, 100, 100, 300));
         mState.getSource(ITYPE_BOTTOM_GESTURES).setVisible(true);
-        Rect visibleInsets = mState.calculateVisibleInsets(
+        Insets visibleInsets = mState.calculateVisibleInsets(
                 new Rect(0, 0, 100, 300), SOFT_INPUT_ADJUST_NOTHING);
-        assertEquals(new Rect(0, 100, 0, 0), visibleInsets);
+        assertEquals(Insets.of(0, 100, 0, 0), visibleInsets);
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/view/InsetsVisibilitiesTest.java b/core/tests/coretests/src/android/view/InsetsVisibilitiesTest.java
new file mode 100644
index 0000000..5664e0b
--- /dev/null
+++ b/core/tests/coretests/src/android/view/InsetsVisibilitiesTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.InsetsState.FIRST_TYPE;
+import static android.view.InsetsState.LAST_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+import android.view.InsetsState.InternalInsetsType;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link InsetsVisibilities}.
+ *
+ * <p>Build/Install/Run:
+ *  atest FrameworksCoreTests:InsetsVisibilities
+ *
+ * <p>This test class is a part of Window Manager Service tests and specified in
+ * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class InsetsVisibilitiesTest {
+
+    @Test
+    public void testEquals() {
+        final InsetsVisibilities v1 = new InsetsVisibilities();
+        final InsetsVisibilities v2 = new InsetsVisibilities();
+        final InsetsVisibilities v3 = new InsetsVisibilities();
+        assertEquals(v1, v2);
+        assertEquals(v1, v3);
+
+        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+            v1.setVisibility(type, false);
+            v2.setVisibility(type, false);
+        }
+        assertEquals(v1, v2);
+        assertNotEquals(v1, v3);
+
+        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+            v1.setVisibility(type, true);
+            v2.setVisibility(type, true);
+        }
+        assertEquals(v1, v2);
+        assertNotEquals(v1, v3);
+    }
+
+    @Test
+    public void testSet() {
+        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+            final InsetsVisibilities v1 = new InsetsVisibilities();
+            final InsetsVisibilities v2 = new InsetsVisibilities();
+
+            v1.setVisibility(type, true);
+            assertNotEquals(v1, v2);
+
+            v2.set(v1);
+            assertEquals(v1, v2);
+
+            v2.setVisibility(type, false);
+            assertNotEquals(v1, v2);
+
+            v1.set(v2);
+            assertEquals(v1, v2);
+        }
+    }
+
+    @Test
+    public void testCopyConstructor() {
+        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+            final InsetsVisibilities v1 = new InsetsVisibilities();
+            v1.setVisibility(type, true);
+            final InsetsVisibilities v2 = new InsetsVisibilities(v1);
+            assertEquals(v1, v2);
+
+            v2.setVisibility(type, false);
+            assertNotEquals(v1, v2);
+        }
+    }
+
+    @Test
+    public void testGetterAndSetter() {
+        final InsetsVisibilities v1 = new InsetsVisibilities();
+        final InsetsVisibilities v2 = new InsetsVisibilities();
+
+        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+            assertEquals(InsetsState.getDefaultVisibility(type), v1.getVisibility(type));
+        }
+
+        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+            v1.setVisibility(type, true);
+            assertTrue(v1.getVisibility(type));
+
+            v2.setVisibility(type, false);
+            assertFalse(v2.getVisibility(type));
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/view/WindowInfoTest.java b/core/tests/coretests/src/android/view/WindowInfoTest.java
index 05e8bd8..0a99b08 100644
--- a/core/tests/coretests/src/android/view/WindowInfoTest.java
+++ b/core/tests/coretests/src/android/view/WindowInfoTest.java
@@ -25,6 +25,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 
+import android.app.ActivityTaskManager;
 import android.os.IBinder;
 import android.os.Parcel;
 import android.platform.test.annotations.Presubmit;
@@ -83,6 +84,7 @@
         assertEquals(0, w.layer);
         assertEquals(AccessibilityNodeInfo.UNDEFINED_NODE_ID, w.accessibilityIdOfAnchor);
         assertEquals(Display.INVALID_DISPLAY, w.displayId);
+        assertEquals(ActivityTaskManager.INVALID_TASK_ID, w.taskId);
         assertNull(w.title);
         assertNull(w.token);
         assertNull(w.childTokens);
@@ -123,6 +125,7 @@
         windowInfo.displayId = 2;
         windowInfo.layer = 3;
         windowInfo.accessibilityIdOfAnchor = 4L;
+        windowInfo.taskId = 5;
         windowInfo.title = "title";
         windowInfo.token = mock(IBinder.class);
         windowInfo.childTokens = new ArrayList<>();
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 8643a37..b71d814 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -166,4 +166,7 @@
 
     public void logTrace(long timestamp, String where, String callingParams, int processId,
             long threadId, int callingUid, Bundle callingStack) {}
+
+    public void logTrace(long timestamp, String where, long loggingTypes, String callingParams,
+            int processId, long threadId, int callingUid, Bundle serializedCallingStackInBundle) {}
 }
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
index ddb6729..4b19391 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
@@ -39,9 +39,10 @@
     public void testConstructorAdditionalFlags() {
         final ComponentName componentName = new ComponentName("component", "name");
         final IBinder token = new Binder();
+        final IBinder windowToken = new Binder();
         final ContentCaptureContext ctx = new ContentCaptureContext(/* clientContext= */ null,
                 new ActivityId(/* taskId= */ 666, token), componentName, /* displayId= */
-                42, /* flags= */ 1);
+                42, windowToken, /* flags= */ 1);
         final ContentCaptureContext newCtx = new ContentCaptureContext(ctx, /* extraFlags= */ 2);
         assertThat(newCtx.getFlags()).isEqualTo(3);
         assertThat(newCtx.getActivityComponent()).isEqualTo(componentName);
@@ -50,6 +51,7 @@
         assertThat(activityId.getTaskId()).isEqualTo(666);
         assertThat(activityId.getToken()).isEqualTo(token);
         assertThat(newCtx.getDisplayId()).isEqualTo(42);
+        assertThat(newCtx.getWindowToken()).isEqualTo(windowToken);
         assertThat(newCtx.getExtras()).isNull();
         assertThat(newCtx.getLocusId()).isNull();
         assertThat(newCtx.getParentSessionId()).isNull();
diff --git a/core/tests/coretests/src/android/window/WindowContextControllerTest.java b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
index 073e468..a6e351d 100644
--- a/core/tests/coretests/src/android/window/WindowContextControllerTest.java
+++ b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
@@ -22,6 +22,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
@@ -66,7 +67,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mController = new WindowContextController(mMockToken, mMockWms);
-        doNothing().when(mMockToken).onConfigurationChanged(any(), anyInt());
+        doNothing().when(mMockToken).onConfigurationChanged(any(), anyInt(), anyBoolean());
         doReturn(new Configuration()).when(mMockWms).attachWindowContextToDisplayArea(any(),
                 anyInt(), anyInt(), any());
     }
@@ -92,7 +93,8 @@
                 null /* options */);
 
         assertThat(mController.mAttachedToDisplayArea).isTrue();
-        verify(mMockToken).onConfigurationChanged(any(), eq(DEFAULT_DISPLAY));
+        verify(mMockToken).onConfigurationChanged(any(), eq(DEFAULT_DISPLAY),
+                eq(false) /* shouldReportConfigChange */);
 
         mController.detachIfNeeded();
 
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 96b4316..7cd8197 100644
--- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
@@ -23,6 +23,7 @@
 import static com.android.internal.jank.FrameTracker.SurfaceControlWrapper;
 import static com.android.internal.jank.FrameTracker.ViewRootWrapper;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_WALLPAPER_TRANSITION;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -50,6 +51,7 @@
 import com.android.internal.jank.FrameTracker.ChoreographerWrapper;
 import com.android.internal.jank.FrameTracker.FrameMetricsWrapper;
 import com.android.internal.jank.FrameTracker.ThreadedRendererWrapper;
+import com.android.internal.jank.InteractionJankMonitor.Configuration;
 import com.android.internal.jank.InteractionJankMonitor.Session;
 
 import org.junit.Before;
@@ -69,7 +71,6 @@
     public ActivityTestRule<ViewAttachTestActivity> mRule =
             new ActivityTestRule<>(ViewAttachTestActivity.class);
 
-    private FrameTracker mTracker;
     private ThreadedRendererWrapper mRenderer;
     private FrameMetricsWrapper mWrapper;
     private SurfaceControlWrapper mSurfaceControlWrapper;
@@ -85,7 +86,6 @@
         View view = mActivity.getWindow().getDecorView();
         assertThat(view.isAttachedToWindow()).isTrue();
 
-        Handler handler = mRule.getActivity().getMainThreadHandler();
         mWrapper = Mockito.spy(new FrameMetricsWrapper());
         mRenderer = Mockito.spy(new ThreadedRendererWrapper(view.getThreadedRenderer()));
         doNothing().when(mRenderer).addObserver(any());
@@ -103,229 +103,355 @@
                 mListenerCapture.capture());
 
         mChoreographer = mock(ChoreographerWrapper.class);
+    }
 
-        Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
-        mTracker = Mockito.spy(
+    private FrameTracker spyFrameTracker(int cuj, String postfix, boolean surfaceOnly) {
+        Handler handler = mRule.getActivity().getMainThreadHandler();
+        Session session = new Session(cuj, postfix);
+        Configuration config = mock(Configuration.class);
+        when(config.isSurfaceOnly()).thenReturn(surfaceOnly);
+        when(config.getSurfaceControl()).thenReturn(mSurfaceControl);
+        FrameTracker frameTracker = Mockito.spy(
                 new FrameTracker(session, handler, mRenderer, mViewRootWrapper,
                         mSurfaceControlWrapper, mChoreographer, mWrapper,
-                        /*traceThresholdMissedFrames=*/ 1, /*traceThresholdFrameTimeMillis=*/ -1,
-                        null));
-        doNothing().when(mTracker).triggerPerfetto();
-        doNothing().when(mTracker).postTraceStartMarker();
+                        /* traceThresholdMissedFrames= */ 1,
+                        /* traceThresholdFrameTimeMillis= */ -1,
+                        /* FrameTrackerListener= */ null, config));
+        doNothing().when(frameTracker).triggerPerfetto();
+        doNothing().when(frameTracker).postTraceStartMarker();
+        return frameTracker;
     }
 
     @Test
     public void testOnlyFirstWindowFrameOverThreshold() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         // Just provide current timestamp anytime mWrapper asked for VSYNC_TIMESTAMP
         when(mWrapper.getMetric(FrameMetrics.VSYNC_TIMESTAMP))
                 .then(unusedInvocation -> System.nanoTime());
 
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer, only()).addObserver(any());
 
         // send first frame with a long duration - should not be taken into account
-        sendFirstWindowFrame(100, JANK_APP_DEADLINE_MISSED, 100L);
+        sendFirstWindowFrame(tracker, 100, JANK_APP_DEADLINE_MISSED, 100L);
 
         // send another frame with a short duration - should not be considered janky
-        sendFirstWindowFrame(5, JANK_NONE, 101L);
+        sendFirstWindowFrame(tracker, 5, JANK_NONE, 101L);
 
         // end the trace session, the last janky frame is after the end() so is discarded.
         when(mChoreographer.getVsyncId()).thenReturn(102L);
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
-        sendFrame(5, JANK_NONE, 102L);
-        sendFrame(500, JANK_APP_DEADLINE_MISSED, 103L);
+        tracker.end(FrameTracker.REASON_END_NORMAL);
+        sendFrame(tracker, 5, JANK_NONE, 102L);
+        sendFrame(tracker, 500, JANK_APP_DEADLINE_MISSED, 103L);
 
-        verify(mTracker).removeObservers();
-        verify(mTracker, never()).triggerPerfetto();
+        verify(tracker).removeObservers();
+        verify(tracker, never()).triggerPerfetto();
     }
 
     @Test
     public void testSfJank() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer, only()).addObserver(any());
 
         // send first frame - not janky
-        sendFrame(4, JANK_NONE, 100L);
+        sendFrame(tracker, 4, JANK_NONE, 100L);
 
         // send another frame - should be considered janky
-        sendFrame(40, JANK_SURFACEFLINGER_DEADLINE_MISSED, 101L);
+        sendFrame(tracker, 40, JANK_SURFACEFLINGER_DEADLINE_MISSED, 101L);
 
         // end the trace session
         when(mChoreographer.getVsyncId()).thenReturn(102L);
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
-        sendFrame(4, JANK_NONE, 102L);
+        tracker.end(FrameTracker.REASON_END_NORMAL);
+        sendFrame(tracker, 4, JANK_NONE, 102L);
 
-        verify(mTracker).removeObservers();
+        verify(tracker).removeObservers();
 
         // We detected a janky frame - trigger Perfetto
-        verify(mTracker).triggerPerfetto();
+        verify(tracker).triggerPerfetto();
     }
 
     @Test
     public void testFirstFrameJankyNoTrigger() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer, only()).addObserver(any());
 
         // send first frame - janky
-        sendFrame(40, JANK_APP_DEADLINE_MISSED, 100L);
+        sendFrame(tracker, 40, JANK_APP_DEADLINE_MISSED, 100L);
 
         // send another frame - not jank
-        sendFrame(4, JANK_NONE, 101L);
+        sendFrame(tracker, 4, JANK_NONE, 101L);
 
         // end the trace session
         when(mChoreographer.getVsyncId()).thenReturn(102L);
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
-        sendFrame(4, JANK_NONE, 102L);
+        tracker.end(FrameTracker.REASON_END_NORMAL);
+        sendFrame(tracker, 4, JANK_NONE, 102L);
 
-        verify(mTracker).removeObservers();
+        verify(tracker).removeObservers();
 
         // We detected a janky frame - trigger Perfetto
-        verify(mTracker, never()).triggerPerfetto();
+        verify(tracker, never()).triggerPerfetto();
     }
 
     @Test
     public void testOtherFrameOverThreshold() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer, only()).addObserver(any());
 
         // send first frame - not janky
-        sendFrame(4, JANK_NONE, 100L);
+        sendFrame(tracker, 4, JANK_NONE, 100L);
 
         // send another frame - should be considered janky
-        sendFrame(40, JANK_APP_DEADLINE_MISSED, 101L);
+        sendFrame(tracker, 40, JANK_APP_DEADLINE_MISSED, 101L);
 
         // end the trace session
         when(mChoreographer.getVsyncId()).thenReturn(102L);
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
-        sendFrame(4, JANK_NONE, 102L);
+        tracker.end(FrameTracker.REASON_END_NORMAL);
+        sendFrame(tracker, 4, JANK_NONE, 102L);
 
-        verify(mTracker).removeObservers();
+        verify(tracker).removeObservers();
 
         // We detected a janky frame - trigger Perfetto
-        verify(mTracker).triggerPerfetto();
+        verify(tracker).triggerPerfetto();
     }
 
     @Test
     public void testLastFrameOverThresholdBeforeEnd() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer, only()).addObserver(any());
 
         // send first frame - not janky
-        sendFrame(4, JANK_NONE, 100L);
+        sendFrame(tracker, 4, JANK_NONE, 100L);
 
         // send another frame - not janky
-        sendFrame(4, JANK_NONE, 101L);
+        sendFrame(tracker, 4, JANK_NONE, 101L);
 
         // end the trace session, simulate one more valid callback came after the end call.
         when(mChoreographer.getVsyncId()).thenReturn(102L);
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
-        sendFrame(50, JANK_APP_DEADLINE_MISSED, 102L);
+        tracker.end(FrameTracker.REASON_END_NORMAL);
+        sendFrame(tracker, 50, JANK_APP_DEADLINE_MISSED, 102L);
 
         // One more callback with VSYNC after the end() vsync id.
-        sendFrame(4, JANK_NONE, 103L);
+        sendFrame(tracker, 4, JANK_NONE, 103L);
 
-        verify(mTracker).removeObservers();
+        verify(tracker).removeObservers();
 
         // We detected a janky frame - trigger Perfetto
-        verify(mTracker).triggerPerfetto();
+        verify(tracker).triggerPerfetto();
     }
 
     @Test
     public void testBeginCancel() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer).addObserver(any());
 
         // First frame - not janky
-        sendFrame(4, JANK_NONE, 100L);
+        sendFrame(tracker, 4, JANK_NONE, 100L);
 
         // normal frame - not janky
-        sendFrame(4, JANK_NONE, 101L);
+        sendFrame(tracker, 4, JANK_NONE, 101L);
 
         // a janky frame
-        sendFrame(50, JANK_APP_DEADLINE_MISSED, 102L);
+        sendFrame(tracker, 50, JANK_APP_DEADLINE_MISSED, 102L);
 
-        mTracker.cancel(FrameTracker.REASON_CANCEL_NORMAL);
-        verify(mTracker).removeObservers();
+        tracker.cancel(FrameTracker.REASON_CANCEL_NORMAL);
+        verify(tracker).removeObservers();
         // Since the tracker has been cancelled, shouldn't trigger perfetto.
-        verify(mTracker, never()).triggerPerfetto();
+        verify(tracker, never()).triggerPerfetto();
     }
 
     @Test
     public void testCancelIfEndVsyncIdEqualsToBeginVsyncId() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer, only()).addObserver(any());
 
         // end the trace session
         when(mChoreographer.getVsyncId()).thenReturn(101L);
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
+        tracker.end(FrameTracker.REASON_END_NORMAL);
 
         // Since the begin vsync id (101) equals to the end vsync id (101), will be treat as cancel.
-        verify(mTracker).cancel(FrameTracker.REASON_CANCEL_SAME_VSYNC);
+        verify(tracker).cancel(FrameTracker.REASON_CANCEL_SAME_VSYNC);
 
         // Observers should be removed in this case, or FrameTracker object will be leaked.
-        verify(mTracker).removeObservers();
+        verify(tracker).removeObservers();
 
         // Should never trigger Perfetto since it is a cancel.
-        verify(mTracker, never()).triggerPerfetto();
+        verify(tracker, never()).triggerPerfetto();
     }
 
     @Test
     public void testCancelIfEndVsyncIdLessThanBeginVsyncId() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
         when(mChoreographer.getVsyncId()).thenReturn(100L);
-        mTracker.begin();
+        tracker.begin();
         verify(mRenderer, only()).addObserver(any());
 
         // end the trace session at the same vsync id, end vsync id will less than the begin one.
         // Because the begin vsync id is supposed to the next frame,
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
+        tracker.end(FrameTracker.REASON_END_NORMAL);
 
         // The begin vsync id (101) is larger than the end one (100), will be treat as cancel.
-        verify(mTracker).cancel(FrameTracker.REASON_CANCEL_SAME_VSYNC);
+        verify(tracker).cancel(FrameTracker.REASON_CANCEL_SAME_VSYNC);
 
         // Observers should be removed in this case, or FrameTracker object will be leaked.
-        verify(mTracker).removeObservers();
+        verify(tracker).removeObservers();
 
         // Should never trigger Perfetto since it is a cancel.
-        verify(mTracker, never()).triggerPerfetto();
+        verify(tracker, never()).triggerPerfetto();
     }
 
     @Test
     public void testCancelWhenSessionNeverBegun() {
-        mTracker.cancel(FrameTracker.REASON_CANCEL_NORMAL);
-        verify(mTracker).removeObservers();
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
+        tracker.cancel(FrameTracker.REASON_CANCEL_NORMAL);
+        verify(tracker).removeObservers();
     }
 
     @Test
     public void testEndWhenSessionNeverBegun() {
-        mTracker.end(FrameTracker.REASON_END_NORMAL);
-        verify(mTracker).removeObservers();
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX, /* surfaceOnly= */ false);
+
+        tracker.end(FrameTracker.REASON_END_NORMAL);
+        verify(tracker).removeObservers();
     }
 
-    private void sendFirstWindowFrame(long durationMillis,
+    @Test
+    public void testSurfaceOnlyOtherFrameJanky() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_WALLPAPER_TRANSITION, CUJ_POSTFIX, /* surfaceOnly= */ true);
+
+        when(mChoreographer.getVsyncId()).thenReturn(100L);
+        tracker.begin();
+        verify(mSurfaceControlWrapper).addJankStatsListener(any(), any());
+
+        // First frame - not janky
+        sendFrame(tracker, JANK_NONE, 100L);
+        // normal frame - not janky
+        sendFrame(tracker, JANK_NONE, 101L);
+        // a janky frame
+        sendFrame(tracker, JANK_APP_DEADLINE_MISSED, 102L);
+
+        when(mChoreographer.getVsyncId()).thenReturn(102L);
+        tracker.end(FrameTracker.REASON_CANCEL_NORMAL);
+
+        // an extra frame to trigger finish
+        sendFrame(tracker, JANK_NONE, 103L);
+
+        verify(mSurfaceControlWrapper).removeJankStatsListener(any());
+        verify(tracker).triggerPerfetto();
+    }
+
+    @Test
+    public void testSurfaceOnlyFirstFrameJanky() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_WALLPAPER_TRANSITION, CUJ_POSTFIX, /* surfaceOnly= */ true);
+
+        when(mChoreographer.getVsyncId()).thenReturn(100L);
+        tracker.begin();
+        verify(mSurfaceControlWrapper).addJankStatsListener(any(), any());
+
+        // First frame - janky
+        sendFrame(tracker, JANK_APP_DEADLINE_MISSED, 100L);
+        // normal frame - not janky
+        sendFrame(tracker, JANK_NONE, 101L);
+        // normal frame - not janky
+        sendFrame(tracker, JANK_NONE, 102L);
+
+        when(mChoreographer.getVsyncId()).thenReturn(102L);
+        tracker.end(FrameTracker.REASON_CANCEL_NORMAL);
+
+        // an extra frame to trigger finish
+        sendFrame(tracker, JANK_NONE, 103L);
+
+        verify(mSurfaceControlWrapper).removeJankStatsListener(any());
+        verify(tracker, never()).triggerPerfetto();
+    }
+
+    @Test
+    public void testSurfaceOnlyLastFrameJanky() {
+        FrameTracker tracker = spyFrameTracker(
+                CUJ_WALLPAPER_TRANSITION, CUJ_POSTFIX, /* surfaceOnly= */ true);
+
+        when(mChoreographer.getVsyncId()).thenReturn(100L);
+        tracker.begin();
+        verify(mSurfaceControlWrapper).addJankStatsListener(any(), any());
+
+        // First frame - not janky
+        sendFrame(tracker, JANK_NONE, 100L);
+        // normal frame - not janky
+        sendFrame(tracker, JANK_NONE, 101L);
+        // normal frame - not janky
+        sendFrame(tracker, JANK_NONE, 102L);
+
+        when(mChoreographer.getVsyncId()).thenReturn(102L);
+        tracker.end(FrameTracker.REASON_CANCEL_NORMAL);
+
+        // janky frame, should be ignored, trigger finish
+        sendFrame(tracker, JANK_APP_DEADLINE_MISSED, 103L);
+
+        verify(mSurfaceControlWrapper).removeJankStatsListener(any());
+        verify(tracker, never()).triggerPerfetto();
+    }
+
+    private void sendFirstWindowFrame(FrameTracker tracker, long durationMillis,
             @JankType int jankType, long vsyncId) {
-        sendFrame(durationMillis, jankType, vsyncId, true /* firstWindowFrame */);
+        sendFrame(tracker, durationMillis, jankType, vsyncId, /* firstWindowFrame= */ true);
     }
 
-    private void sendFrame(long durationMillis,
+    private void sendFrame(FrameTracker tracker, long durationMillis,
             @JankType int jankType, long vsyncId) {
-        sendFrame(durationMillis, jankType, vsyncId, false /* firstWindowFrame */);
+        sendFrame(tracker, durationMillis, jankType, vsyncId, /* firstWindowFrame= */ false);
     }
 
-    private void sendFrame(long durationMillis,
+    /**
+     * Used for surface only test.
+     */
+    private void sendFrame(FrameTracker tracker, @JankType int jankType, long vsyncId) {
+        sendFrame(tracker, /* durationMillis= */ -1,
+                jankType, vsyncId, /* firstWindowFrame= */ false);
+    }
+
+    private void sendFrame(FrameTracker tracker, long durationMillis,
             @JankType int jankType, long vsyncId, boolean firstWindowFrame) {
-        when(mWrapper.getTiming()).thenReturn(new long[] { 0, vsyncId });
-        doReturn(firstWindowFrame ? 1L : 0L).when(mWrapper)
-                .getMetric(FrameMetrics.FIRST_DRAW_FRAME);
-        doReturn(TimeUnit.MILLISECONDS.toNanos(durationMillis))
-                .when(mWrapper).getMetric(FrameMetrics.TOTAL_DURATION);
-        mTracker.onFrameMetricsAvailable(0);
+        if (!tracker.mSurfaceOnly) {
+            when(mWrapper.getTiming()).thenReturn(new long[]{0, vsyncId});
+            doReturn(firstWindowFrame ? 1L : 0L).when(mWrapper)
+                    .getMetric(FrameMetrics.FIRST_DRAW_FRAME);
+            doReturn(TimeUnit.MILLISECONDS.toNanos(durationMillis))
+                    .when(mWrapper).getMetric(FrameMetrics.TOTAL_DURATION);
+            tracker.onFrameMetricsAvailable(0);
+        }
         mListenerCapture.getValue().onJankDataAvailable(new JankData[] {
                 new JankData(vsyncId, jankType)
         });
diff --git a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index c153b38..d7a5e26 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -16,8 +16,6 @@
 
 package com.android.internal.jank;
 
-import static com.android.internal.jank.FrameTracker.SurfaceControlWrapper;
-import static com.android.internal.jank.FrameTracker.ViewRootWrapper;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_TO_STATSD_INTERACTION_TYPE;
 
@@ -25,17 +23,17 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.Message;
 import android.provider.DeviceConfig;
 import android.view.View;
 import android.view.ViewAttachTestActivity;
@@ -43,8 +41,12 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
 
+import com.android.internal.jank.FrameTracker.ChoreographerWrapper;
 import com.android.internal.jank.FrameTracker.FrameMetricsWrapper;
+import com.android.internal.jank.FrameTracker.SurfaceControlWrapper;
 import com.android.internal.jank.FrameTracker.ThreadedRendererWrapper;
+import com.android.internal.jank.FrameTracker.ViewRootWrapper;
+import com.android.internal.jank.InteractionJankMonitor.Configuration;
 import com.android.internal.jank.InteractionJankMonitor.Session;
 
 import org.junit.Before;
@@ -92,12 +94,15 @@
         verify(mWorker).start();
 
         Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
+        Configuration config = mock(Configuration.class);
+        when(config.isSurfaceOnly()).thenReturn(false);
         FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
                 new ThreadedRendererWrapper(mView.getThreadedRenderer()),
-                new ViewRootWrapper(mView.getViewRootImpl()), new SurfaceControlWrapper(),
-                mock(FrameTracker.ChoreographerWrapper.class),
-                new FrameMetricsWrapper(), /*traceThresholdMissedFrames=*/ 1,
-                /*traceThresholdFrameTimeMillis=*/ -1, null));
+                new ViewRootWrapper(mView.getViewRootImpl()),
+                new SurfaceControlWrapper(), mock(ChoreographerWrapper.class),
+                new FrameMetricsWrapper(),
+                /* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
+                /* FrameTrackerListener */ null, config));
         doReturn(tracker).when(monitor).createFrameTracker(any(), any());
         doNothing().when(tracker).triggerPerfetto();
         doNothing().when(tracker).postTraceStartMarker();
@@ -138,28 +143,30 @@
     public void testBeginCancel() {
         InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
 
-        ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
+        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
 
         Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
+        Configuration config = mock(Configuration.class);
+        when(config.isSurfaceOnly()).thenReturn(false);
         FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
                 new ThreadedRendererWrapper(mView.getThreadedRenderer()),
-                new ViewRootWrapper(mView.getViewRootImpl()), new SurfaceControlWrapper(),
-                mock(FrameTracker.ChoreographerWrapper.class),
-                new FrameMetricsWrapper(), /*traceThresholdMissedFrames=*/ 1,
-                /*traceThresholdFrameTimeMillis=*/ -1, null));
+                new ViewRootWrapper(mView.getViewRootImpl()),
+                new SurfaceControlWrapper(), mock(FrameTracker.ChoreographerWrapper.class),
+                new FrameMetricsWrapper(),
+                /* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
+                /* FrameTrackerListener */ null, config));
         doReturn(tracker).when(monitor).createFrameTracker(any(), any());
         doNothing().when(tracker).triggerPerfetto();
         doNothing().when(tracker).postTraceStartMarker();
 
         assertThat(monitor.begin(mView, session.getCuj())).isTrue();
         verify(tracker).begin();
-        verify(mWorker.getThreadHandler(), atLeastOnce()).sendMessageAtTime(captor.capture(),
-                anyLong());
-        Runnable runnable = captor.getValue().getCallback();
+        verify(monitor).scheduleTimeoutAction(anyInt(), anyLong(), captor.capture());
+        Runnable runnable = captor.getValue();
         assertThat(runnable).isNotNull();
         mWorker.getThreadHandler().removeCallbacks(runnable);
         runnable.run();
-        verify(tracker).cancel(FrameTracker.REASON_CANCEL_NORMAL);
+        verify(tracker).cancel(FrameTracker.REASON_CANCEL_TIMEOUT);
     }
 
     @Test
diff --git a/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java b/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
index 79f7a5c..130f552 100644
--- a/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.BatteryConsumer;
@@ -36,26 +38,28 @@
 
     @Rule
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
-            .setAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY, 10.0);
+            .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0, 10.0)
+            .setNumDisplays(1);
 
     @Test
     public void testMeasuredEnergyBasedModel() {
         mStatsRule.initMeasuredEnergyStatsLocked();
         BatteryStatsImpl stats = mStatsRule.getBatteryStats();
 
-        stats.updateDisplayMeasuredEnergyStatsLocked(300_000_000, Display.STATE_ON, 0);
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{300_000_000},
+                new int[]{Display.STATE_ON}, 0);
 
-        stats.noteScreenStateLocked(Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
                 30 * MINUTE_IN_MS);
 
-        stats.updateDisplayMeasuredEnergyStatsLocked(200_000_000, Display.STATE_DOZE,
-                30 * MINUTE_IN_MS);
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{200_000_000},
+                new int[]{Display.STATE_DOZE}, 30 * MINUTE_IN_MS);
 
-        stats.noteScreenStateLocked(Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
                 120 * MINUTE_IN_MS);
 
-        stats.updateDisplayMeasuredEnergyStatsLocked(100_000_000, Display.STATE_OFF,
-                120 * MINUTE_IN_MS);
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{100_000_000},
+                new int[]{Display.STATE_OFF}, 120 * MINUTE_IN_MS);
 
         AmbientDisplayPowerCalculator calculator =
                 new AmbientDisplayPowerCalculator(mStatsRule.getPowerProfile());
@@ -73,12 +77,73 @@
     }
 
     @Test
+    public void testMeasuredEnergyBasedModel_multiDisplay() {
+        mStatsRule.initMeasuredEnergyStatsLocked()
+                .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 1, 20.0)
+                .setNumDisplays(2);
+        BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+
+
+        final int[] screenStates = new int[] {Display.STATE_OFF, Display.STATE_OFF};
+
+        stats.noteScreenStateLocked(0, screenStates[0], 0, 0, 0);
+        stats.noteScreenStateLocked(1, screenStates[1], 0, 0, 0);
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{300, 400}, screenStates, 0);
+
+        // Switch display0 to doze
+        screenStates[0] = Display.STATE_DOZE;
+        stats.noteScreenStateLocked(0, screenStates[0], 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
+                30 * MINUTE_IN_MS);
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{200, 300},
+                screenStates, 30 * MINUTE_IN_MS);
+
+        // Switch display1 to doze
+        screenStates[1] = Display.STATE_DOZE;
+        stats.noteScreenStateLocked(1, Display.STATE_DOZE, 90 * MINUTE_IN_MS, 90 * MINUTE_IN_MS,
+                90 * MINUTE_IN_MS);
+        // 100,000,000 uC should be attributed to display 0 doze here.
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{100_000_000, 700_000_000},
+                screenStates, 90 * MINUTE_IN_MS);
+
+        // Switch display0 to off
+        screenStates[0] = Display.STATE_OFF;
+        stats.noteScreenStateLocked(0, screenStates[0], 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
+                120 * MINUTE_IN_MS);
+        // 40,000,000 and 70,000,000 uC should be attributed to display 0 and 1 doze here.
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{40_000_000, 70_000_000},
+                screenStates, 120 * MINUTE_IN_MS);
+
+        // Switch display1 to off
+        screenStates[1] = Display.STATE_OFF;
+        stats.noteScreenStateLocked(1, screenStates[1], 150 * MINUTE_IN_MS, 150 * MINUTE_IN_MS,
+                150 * MINUTE_IN_MS);
+        stats.updateDisplayMeasuredEnergyStatsLocked(new long[]{100, 90_000_000}, screenStates,
+                150 * MINUTE_IN_MS);
+        // 90,000,000 uC should be attributed to display 1 doze here.
+
+        AmbientDisplayPowerCalculator calculator =
+                new AmbientDisplayPowerCalculator(mStatsRule.getPowerProfile());
+
+        mStatsRule.apply(calculator);
+
+        BatteryConsumer consumer = mStatsRule.getDeviceBatteryConsumer();
+        assertThat(consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY))
+                .isEqualTo(120 * MINUTE_IN_MS);
+        // 100,000,000 + 40,000,000 + 70,000,000 + 90,000,000 uC / 1000 (micro-/milli-) / 3600
+        // (seconds/hour) = 27.777778 mAh
+        assertThat(consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY))
+                .isWithin(PRECISION).of(83.33333);
+        assertThat(consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
+    }
+
+    @Test
     public void testPowerProfileBasedModel() {
         BatteryStatsImpl stats = mStatsRule.getBatteryStats();
 
-        stats.noteScreenStateLocked(Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
                 30 * MINUTE_IN_MS);
-        stats.noteScreenStateLocked(Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
                 120 * MINUTE_IN_MS);
 
         AmbientDisplayPowerCalculator calculator =
@@ -94,4 +159,36 @@
         assertThat(consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY))
                 .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
     }
+
+    @Test
+    public void testPowerProfileBasedModel_multiDisplay() {
+        mStatsRule.setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 1, 20.0)
+                .setNumDisplays(2);
+
+        BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+
+        stats.noteScreenStateLocked(1, Display.STATE_OFF, 0, 0, 0);
+        stats.noteScreenStateLocked(0, Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
+                30 * MINUTE_IN_MS);
+        stats.noteScreenStateLocked(1, Display.STATE_DOZE, 90 * MINUTE_IN_MS, 90 * MINUTE_IN_MS,
+                90 * MINUTE_IN_MS);
+        stats.noteScreenStateLocked(0, Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
+                120 * MINUTE_IN_MS);
+        stats.noteScreenStateLocked(1, Display.STATE_OFF, 150 * MINUTE_IN_MS, 150 * MINUTE_IN_MS,
+                150 * MINUTE_IN_MS);
+
+        AmbientDisplayPowerCalculator calculator =
+                new AmbientDisplayPowerCalculator(mStatsRule.getPowerProfile());
+
+        mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+        BatteryConsumer consumer = mStatsRule.getDeviceBatteryConsumer();
+        // Duration should only be the union of
+        assertThat(consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY))
+                .isEqualTo(120 * MINUTE_IN_MS);
+        assertThat(consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY))
+                .isWithin(PRECISION).of(35.0);
+        assertThat(consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
index d4799a8..3e2885a 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
@@ -16,9 +16,13 @@
 
 package com.android.internal.os;
 
+import static android.os.BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS;
 import static android.os.BatteryStats.STATS_SINCE_CHARGED;
 import static android.os.BatteryStats.WAKE_TYPE_PARTIAL;
 
+import static com.android.internal.os.BatteryStatsImpl.ExternalStatsSync.UPDATE_CPU;
+import static com.android.internal.os.BatteryStatsImpl.ExternalStatsSync.UPDATE_DISPLAY;
+
 import android.app.ActivityManager;
 import android.os.BatteryStats;
 import android.os.BatteryStats.HistoryItem;
@@ -37,8 +41,10 @@
 
 import junit.framework.TestCase;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.IntConsumer;
 
 /**
  * Test various BatteryStatsImpl noteStart methods.
@@ -317,18 +323,130 @@
     public void testNoteScreenStateLocked() throws Exception {
         final MockClocks clocks = new MockClocks(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+        bi.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
 
         bi.updateTimeBasesLocked(true, Display.STATE_ON, 0, 0);
-        bi.noteScreenStateLocked(Display.STATE_ON);
-        bi.noteScreenStateLocked(Display.STATE_DOZE);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
         assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
-        assertEquals(bi.getScreenState(), Display.STATE_DOZE);
-        bi.noteScreenStateLocked(Display.STATE_ON);
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
         assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
-        assertEquals(bi.getScreenState(), Display.STATE_ON);
-        bi.noteScreenStateLocked(Display.STATE_OFF);
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
         assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
-        assertEquals(bi.getScreenState(), Display.STATE_OFF);
+        assertEquals(Display.STATE_OFF, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE_SUSPEND);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE_SUSPEND, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_VR note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_VR);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_ON_SUSPEND note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_ON_SUSPEND);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        // Transition from ON to ON state should not cause an External Sync
+        assertEquals(0, bi.getAndClearExternalStatsSyncFlags());
+    }
+
+    /**
+     * Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly for
+     * multi display devices
+     */
+    @SmallTest
+    public void testNoteScreenStateLocked_multiDisplay() throws Exception {
+        final MockClocks clocks = new MockClocks(); // holds realtime and uptime in ms
+        MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+        bi.setDisplayCountLocked(2);
+        bi.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
+
+        bi.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0);
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        bi.noteScreenStateLocked(1, Display.STATE_OFF);
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_OFF, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE_SUSPEND);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE_SUSPEND, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_VR note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_VR);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_ON_SUSPEND note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_ON_SUSPEND);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        // Transition from ON to ON state should not cause an External Sync
+        assertEquals(0, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(1, Display.STATE_DOZE);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        // Should remain STATE_ON since display0 is still on.
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        // Overall screen state did not change, so no need to sync CPU stats.
+        assertEquals(UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE_SUSPEND);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        // Overall screen state did not change, so no need to sync CPU stats.
+        assertEquals(UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_VR);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON_SUSPEND);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(0, bi.getAndClearExternalStatsSyncFlags());
     }
 
     /*
@@ -352,32 +470,317 @@
         bi.updateTimeBasesLocked(true, Display.STATE_UNKNOWN, 100_000, 100_000);
         // Turn on display at 200us
         clocks.realtime = clocks.uptime = 200;
-        bi.noteScreenStateLocked(Display.STATE_ON);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
         assertEquals(150_000, bi.computeBatteryRealtime(250_000, STATS_SINCE_CHARGED));
         assertEquals(100_000, bi.computeBatteryScreenOffRealtime(250_000, STATS_SINCE_CHARGED));
         assertEquals(50_000, bi.getScreenOnTime(250_000, STATS_SINCE_CHARGED));
         assertEquals(0, bi.getScreenDozeTime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(50_000, bi.getDisplayScreenOnTime(0, 250_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 250_000));
 
         clocks.realtime = clocks.uptime = 310;
-        bi.noteScreenStateLocked(Display.STATE_OFF);
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
         assertEquals(250_000, bi.computeBatteryRealtime(350_000, STATS_SINCE_CHARGED));
         assertEquals(140_000, bi.computeBatteryScreenOffRealtime(350_000, STATS_SINCE_CHARGED));
         assertEquals(110_000, bi.getScreenOnTime(350_000, STATS_SINCE_CHARGED));
         assertEquals(0, bi.getScreenDozeTime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 350_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 350_000));
 
         clocks.realtime = clocks.uptime = 400;
-        bi.noteScreenStateLocked(Display.STATE_DOZE);
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
         assertEquals(400_000, bi.computeBatteryRealtime(500_000, STATS_SINCE_CHARGED));
         assertEquals(290_000, bi.computeBatteryScreenOffRealtime(500_000, STATS_SINCE_CHARGED));
         assertEquals(110_000, bi.getScreenOnTime(500_000, STATS_SINCE_CHARGED));
         assertEquals(100_000, bi.getScreenDozeTime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 500_000));
+        assertEquals(100_000, bi.getDisplayScreenDozeTime(0, 500_000));
 
         clocks.realtime = clocks.uptime = 1000;
-        bi.noteScreenStateLocked(Display.STATE_OFF);
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
         assertEquals(1400_000, bi.computeBatteryRealtime(1500_000, STATS_SINCE_CHARGED));
         assertEquals(1290_000, bi.computeBatteryScreenOffRealtime(1500_000, STATS_SINCE_CHARGED));
         assertEquals(110_000, bi.getScreenOnTime(1500_000, STATS_SINCE_CHARGED));
         assertEquals(600_000, bi.getScreenDozeTime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 1500_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1500_000));
+    }
+
+    /*
+     * Test BatteryStatsImpl.noteScreenStateLocked updates timers correctly for multi display
+     * devices.
+     */
+    @SmallTest
+    public void testNoteScreenStateTimersLocked_multiDisplay() throws Exception {
+        final MockClocks clocks = new MockClocks(); // holds realtime and uptime in ms
+        MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+        bi.setDisplayCountLocked(2);
+
+        clocks.realtime = clocks.uptime = 100;
+        // Device startup, setOnBatteryLocked calls updateTimebases
+        bi.updateTimeBasesLocked(true, Display.STATE_UNKNOWN, 100_000, 100_000);
+        // Turn on display at 200us
+        clocks.realtime = clocks.uptime = 200;
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        bi.noteScreenStateLocked(1, Display.STATE_OFF);
+        assertEquals(150_000, bi.computeBatteryRealtime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(100_000, bi.computeBatteryScreenOffRealtime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(50_000, bi.getScreenOnTime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(0, bi.getScreenDozeTime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(50_000, bi.getDisplayScreenOnTime(0, 250_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 250_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 250_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 250_000));
+
+        clocks.realtime = clocks.uptime = 310;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(250_000, bi.computeBatteryRealtime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(140_000, bi.computeBatteryScreenOffRealtime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(0, bi.getScreenDozeTime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 350_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 350_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 350_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 350_000));
+
+        clocks.realtime = clocks.uptime = 400;
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertEquals(400_000, bi.computeBatteryRealtime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(290_000, bi.computeBatteryScreenOffRealtime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(100_000, bi.getScreenDozeTime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 500_000));
+        assertEquals(100_000, bi.getDisplayScreenDozeTime(0, 500_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 500_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 500_000));
+
+        clocks.realtime = clocks.uptime = 1000;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(1000_000, bi.computeBatteryRealtime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(890_000, bi.computeBatteryScreenOffRealtime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(600_000, bi.getScreenDozeTime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 1100_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1100_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1100_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 1100_000));
+
+        clocks.realtime = clocks.uptime = 1200;
+        // Change state of second display to doze
+        bi.noteScreenStateLocked(1, Display.STATE_DOZE);
+        assertEquals(1150_000, bi.computeBatteryRealtime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(1040_000, bi.computeBatteryScreenOffRealtime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(650_000, bi.getScreenDozeTime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 1250_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1250_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1250_000));
+        assertEquals(50_000, bi.getDisplayScreenDozeTime(1, 1250_000));
+
+        clocks.realtime = clocks.uptime = 1310;
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertEquals(1250_000, bi.computeBatteryRealtime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(1100_000, bi.computeBatteryScreenOffRealtime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(150_000, bi.getScreenOnTime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(710_000, bi.getScreenDozeTime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(150_000, bi.getDisplayScreenOnTime(0, 1350_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1350_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1350_000));
+        assertEquals(150_000, bi.getDisplayScreenDozeTime(1, 1350_000));
+
+        clocks.realtime = clocks.uptime = 1400;
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertEquals(1400_000, bi.computeBatteryRealtime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(1200_000, bi.computeBatteryScreenOffRealtime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getScreenOnTime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(810_000, bi.getScreenDozeTime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getDisplayScreenOnTime(0, 1500_000));
+        assertEquals(700_000, bi.getDisplayScreenDozeTime(0, 1500_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1500_000));
+        assertEquals(300_000, bi.getDisplayScreenDozeTime(1, 1500_000));
+
+        clocks.realtime = clocks.uptime = 2000;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(2000_000, bi.computeBatteryRealtime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(1800_000, bi.computeBatteryScreenOffRealtime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getScreenOnTime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(1410_000, bi.getScreenDozeTime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getDisplayScreenOnTime(0, 2100_000));
+        assertEquals(1200_000, bi.getDisplayScreenDozeTime(0, 2100_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 2100_000));
+        assertEquals(900_000, bi.getDisplayScreenDozeTime(1, 2100_000));
+
+
+        clocks.realtime = clocks.uptime = 2200;
+        // Change state of second display to on
+        bi.noteScreenStateLocked(1, Display.STATE_ON);
+        assertEquals(2150_000, bi.computeBatteryRealtime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(250_000, bi.getScreenOnTime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getDisplayScreenOnTime(0, 2250_000));
+        assertEquals(1200_000, bi.getDisplayScreenDozeTime(0, 2250_000));
+        assertEquals(50_000, bi.getDisplayScreenOnTime(1, 2250_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 2250_000));
+
+        clocks.realtime = clocks.uptime = 2310;
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertEquals(2250_000, bi.computeBatteryRealtime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(350_000, bi.getScreenOnTime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(240_000, bi.getDisplayScreenOnTime(0, 2350_000));
+        assertEquals(1200_000, bi.getDisplayScreenDozeTime(0, 2350_000));
+        assertEquals(150_000, bi.getDisplayScreenOnTime(1, 2350_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 2350_000));
+
+        clocks.realtime = clocks.uptime = 2400;
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertEquals(2400_000, bi.computeBatteryRealtime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(500_000, bi.getScreenOnTime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(290_000, bi.getDisplayScreenOnTime(0, 2500_000));
+        assertEquals(1300_000, bi.getDisplayScreenDozeTime(0, 2500_000));
+        assertEquals(300_000, bi.getDisplayScreenOnTime(1, 2500_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 2500_000));
+
+        clocks.realtime = clocks.uptime = 3000;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(3000_000, bi.computeBatteryRealtime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(1100_000, bi.getScreenOnTime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(290_000, bi.getDisplayScreenOnTime(0, 3100_000));
+        assertEquals(1800_000, bi.getDisplayScreenDozeTime(0, 3100_000));
+        assertEquals(900_000, bi.getDisplayScreenOnTime(1, 3100_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 3100_000));
+    }
+
+
+    /**
+     * Test BatteryStatsImpl.noteScreenBrightnessLocked updates timers correctly.
+     */
+    @SmallTest
+    public void testScreenBrightnessLocked_multiDisplay() throws Exception {
+        final MockClocks clocks = new MockClocks(); // holds realtime and uptime in ms
+        MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+
+        final int numDisplay = 2;
+        bi.setDisplayCountLocked(numDisplay);
+
+
+        final long[] overallExpected = new long[NUM_SCREEN_BRIGHTNESS_BINS];
+        final long[][] perDisplayExpected = new long[numDisplay][NUM_SCREEN_BRIGHTNESS_BINS];
+        class Bookkeeper {
+            public long currentTimeMs = 100;
+            public int overallActiveBin = -1;
+            public int[] perDisplayActiveBin = new int[numDisplay];
+        }
+        final Bookkeeper bk = new Bookkeeper();
+        Arrays.fill(bk.perDisplayActiveBin, -1);
+
+        IntConsumer incrementTime = inc -> {
+            bk.currentTimeMs += inc;
+            if (bk.overallActiveBin >= 0) {
+                overallExpected[bk.overallActiveBin] += inc;
+            }
+            for (int i = 0; i < numDisplay; i++) {
+                final int bin = bk.perDisplayActiveBin[i];
+                if (bin >= 0) {
+                    perDisplayExpected[i][bin] += inc;
+                }
+            }
+            clocks.realtime = clocks.uptime = bk.currentTimeMs;
+        };
+
+        bi.updateTimeBasesLocked(true, Display.STATE_ON, 0, 0);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        bi.noteScreenStateLocked(1, Display.STATE_ON);
+
+        incrementTime.accept(100);
+        bi.noteScreenBrightnessLocked(0, 25);
+        bi.noteScreenBrightnessLocked(1, 25);
+        // floor(25/256*5) = bin 0
+        bk.overallActiveBin = 0;
+        bk.perDisplayActiveBin[0] = 0;
+        bk.perDisplayActiveBin[1] = 0;
+
+        incrementTime.accept(50);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(13);
+        bi.noteScreenBrightnessLocked(0, 100);
+        // floor(25/256*5) = bin 1
+        bk.overallActiveBin = 1;
+        bk.perDisplayActiveBin[0] = 1;
+
+        incrementTime.accept(44);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(22);
+        bi.noteScreenBrightnessLocked(1, 200);
+        // floor(200/256*5) = bin 3
+        bk.overallActiveBin = 3;
+        bk.perDisplayActiveBin[1] = 3;
+
+        incrementTime.accept(33);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(77);
+        bi.noteScreenBrightnessLocked(0, 150);
+        // floor(150/256*5) = bin 2
+        // Overall active bin should not change
+        bk.perDisplayActiveBin[0] = 2;
+
+        incrementTime.accept(88);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(11);
+        bi.noteScreenStateLocked(1, Display.STATE_OFF);
+        // Display 1 should timers should stop incrementing
+        // Overall active bin should fallback to display 0's bin
+        bk.overallActiveBin = 2;
+        bk.perDisplayActiveBin[1] = -1;
+
+        incrementTime.accept(99);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(200);
+        bi.noteScreenBrightnessLocked(0, 255);
+        // floor(150/256*5) = bin 4
+        bk.overallActiveBin = 4;
+        bk.perDisplayActiveBin[0] = 4;
+
+        incrementTime.accept(300);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(200);
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        // No displays are on. No brightness timers should be active.
+        bk.overallActiveBin = -1;
+        bk.perDisplayActiveBin[0] = -1;
+
+        incrementTime.accept(300);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(400);
+        bi.noteScreenStateLocked(1, Display.STATE_ON);
+        // Display 1 turned back on.
+        bk.overallActiveBin = 3;
+        bk.perDisplayActiveBin[1] = 3;
+
+        incrementTime.accept(500);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(600);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        // Display 0 turned back on.
+        bk.overallActiveBin = 4;
+        bk.perDisplayActiveBin[0] = 4;
+
+        incrementTime.accept(700);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
     }
 
     @SmallTest
@@ -595,7 +998,7 @@
         bi.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
 
         clocks.realtime = 0;
-        int screen = Display.STATE_OFF;
+        int[] screen = new int[]{Display.STATE_OFF};
         boolean battery = false;
 
         final int uid1 = 10500;
@@ -605,35 +1008,35 @@
         long globalDoze = 0;
 
         // Case A: uid1 off, uid2 off, battery off, screen off
-        bi.updateTimeBasesLocked(battery, screen, clocks.realtime*1000, 0);
+        bi.updateTimeBasesLocked(battery, screen[0], clocks.realtime * 1000, 0);
         bi.setOnBatteryInternal(battery);
-        bi.updateDisplayMeasuredEnergyStatsLocked(500_000, screen, clocks.realtime);
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{500_000}, screen, clocks.realtime);
         checkMeasuredCharge("A", uid1, blame1, uid2, blame2, globalDoze, bi);
 
         // Case B: uid1 off, uid2 off, battery ON,  screen off
         clocks.realtime += 17;
         battery = true;
-        bi.updateTimeBasesLocked(battery, screen, clocks.realtime*1000, 0);
+        bi.updateTimeBasesLocked(battery, screen[0], clocks.realtime * 1000, 0);
         bi.setOnBatteryInternal(battery);
         clocks.realtime += 19;
-        bi.updateDisplayMeasuredEnergyStatsLocked(510_000, screen, clocks.realtime);
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{510_000}, screen, clocks.realtime);
         checkMeasuredCharge("B", uid1, blame1, uid2, blame2, globalDoze, bi);
 
         // Case C: uid1 ON,  uid2 off, battery on,  screen off
         clocks.realtime += 18;
         setFgState(uid1, true, bi);
         clocks.realtime += 18;
-        bi.updateDisplayMeasuredEnergyStatsLocked(520_000, screen, clocks.realtime);
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{520_000}, screen, clocks.realtime);
         checkMeasuredCharge("C", uid1, blame1, uid2, blame2, globalDoze, bi);
 
         // Case D: uid1 on,  uid2 off, battery on,  screen ON
         clocks.realtime += 17;
-        screen = Display.STATE_ON;
-        bi.updateDisplayMeasuredEnergyStatsLocked(521_000, screen, clocks.realtime);
+        screen[0] = Display.STATE_ON;
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{521_000}, screen, clocks.realtime);
         blame1 += 0; // Screen had been off during the measurement period
         checkMeasuredCharge("D.1", uid1, blame1, uid2, blame2, globalDoze, bi);
         clocks.realtime += 101;
-        bi.updateDisplayMeasuredEnergyStatsLocked(530_000, screen, clocks.realtime);
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{530_000}, screen, clocks.realtime);
         blame1 += 530_000;
         checkMeasuredCharge("D.2", uid1, blame1, uid2, blame2, globalDoze, bi);
 
@@ -641,33 +1044,33 @@
         clocks.realtime += 20;
         setFgState(uid2, true, bi);
         clocks.realtime += 40;
-        bi.updateDisplayMeasuredEnergyStatsLocked(540_000, screen, clocks.realtime);
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{540_000}, screen, clocks.realtime);
         // In the past 60ms, sum of fg is 20+40+40=100ms. uid1 is blamed for 60/100; uid2 for 40/100
         blame1 += 540_000 * (20 + 40) / (20 + 40 + 40);
-        blame2 += 540_000 * ( 0 + 40) / (20 + 40 + 40);
+        blame2 += 540_000 * (0 + 40) / (20 + 40 + 40);
         checkMeasuredCharge("E", uid1, blame1, uid2, blame2, globalDoze, bi);
 
         // Case F: uid1 on,  uid2 OFF, battery on,  screen on
         clocks.realtime += 40;
         setFgState(uid2, false, bi);
         clocks.realtime += 120;
-        bi.updateDisplayMeasuredEnergyStatsLocked(550_000, screen, clocks.realtime);
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{550_000}, screen, clocks.realtime);
         // In the past 160ms, sum f fg is 200ms. uid1 is blamed for 40+120 of it; uid2 for 40 of it.
         blame1 += 550_000 * (40 + 120) / (40 + 40 + 120);
-        blame2 += 550_000 * (40 + 0  ) / (40 + 40 + 120);
+        blame2 += 550_000 * (40 + 0) / (40 + 40 + 120);
         checkMeasuredCharge("F", uid1, blame1, uid2, blame2, globalDoze, bi);
 
         // Case G: uid1 on,  uid2 off,  battery on, screen DOZE
         clocks.realtime += 5;
-        screen = Display.STATE_DOZE;
-        bi.updateDisplayMeasuredEnergyStatsLocked(570_000, screen, clocks.realtime);
+        screen[0] = Display.STATE_DOZE;
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{570_000}, screen, clocks.realtime);
         blame1 += 570_000; // All of this pre-doze time is blamed on uid1.
         checkMeasuredCharge("G", uid1, blame1, uid2, blame2, globalDoze, bi);
 
         // Case H: uid1 on,  uid2 off,  battery on, screen ON
         clocks.realtime += 6;
-        screen = Display.STATE_ON;
-        bi.updateDisplayMeasuredEnergyStatsLocked(580_000, screen, clocks.realtime);
+        screen[0] = Display.STATE_ON;
+        bi.updateDisplayMeasuredEnergyStatsLocked(new long[]{580_000}, screen, clocks.realtime);
         blame1 += 0; // The screen had been doze during the energy period
         globalDoze += 580_000;
         checkMeasuredCharge("H", uid1, blame1, uid2, blame2, globalDoze, bi);
@@ -822,4 +1225,19 @@
 
         assertEquals("Wrong uid2 blame in bucket 1 for Case " + caseName, blame2B, actualUid2[1]);
     }
+
+    private void checkScreenBrightnesses(long[] overallExpected, long[][] perDisplayExpected,
+            BatteryStatsImpl bi, long currentTimeMs) {
+        final int numDisplay = bi.getDisplayCount();
+        for (int bin = 0; bin < NUM_SCREEN_BRIGHTNESS_BINS; bin++) {
+            for (int display = 0; display < numDisplay; display++) {
+                assertEquals("Failure for display " + display + " screen brightness bin " + bin,
+                        perDisplayExpected[display][bin] * 1000,
+                        bi.getDisplayScreenBrightnessTime(display, bin, currentTimeMs * 1000));
+            }
+            assertEquals("Failure for overall screen brightness bin " + bin,
+                    overallExpected[bin] * 1000,
+                    bi.getScreenBrightnessTime(bin, currentTimeMs * 1000, STATS_SINCE_CHARGED));
+        }
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java
index 083090c..ac87806 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java
@@ -110,6 +110,20 @@
         return this;
     }
 
+    public BatteryUsageStatsRule setAveragePowerForOrdinal(String group, int ordinal,
+            double value) {
+        when(mPowerProfile.getAveragePowerForOrdinal(group, ordinal)).thenReturn(value);
+        when(mPowerProfile.getAveragePowerForOrdinal(eq(group), eq(ordinal),
+                anyDouble())).thenReturn(value);
+        return this;
+    }
+
+    public BatteryUsageStatsRule setNumDisplays(int value) {
+        when(mPowerProfile.getNumDisplays()).thenReturn(value);
+        mBatteryStats.setDisplayCountLocked(value);
+        return this;
+    }
+
     /** Call only after setting the power profile information. */
     public BatteryUsageStatsRule initMeasuredEnergyStatsLocked() {
         return initMeasuredEnergyStatsLocked(new String[0]);
diff --git a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
index cee1a03..cfecf15 100644
--- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
+++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
@@ -39,13 +39,14 @@
     public BatteryStatsImpl.Clocks clocks;
     public boolean mForceOnBattery;
     private NetworkStats mNetworkStats;
+    private DummyExternalStatsSync mExternalStatsSync = new DummyExternalStatsSync();
 
     MockBatteryStatsImpl(Clocks clocks) {
         super(clocks);
         this.clocks = mClocks;
         initTimersAndCounters();
 
-        setExternalStatsSyncLocked(new DummyExternalStatsSync());
+        setExternalStatsSyncLocked(mExternalStatsSync);
         informThatAllExternalStatsAreFlushed();
 
         // A no-op handler.
@@ -182,7 +183,15 @@
         return mPendingUids;
     }
 
+    public int getAndClearExternalStatsSyncFlags() {
+        final int flags = mExternalStatsSync.flags;
+        mExternalStatsSync.flags = 0;
+        return flags;
+    }
+
     private class DummyExternalStatsSync implements ExternalStatsSync {
+        public int flags = 0;
+
         @Override
         public Future<?> scheduleSync(String reason, int flags) {
             return null;
@@ -211,8 +220,9 @@
         }
 
         @Override
-        public Future<?> scheduleSyncDueToScreenStateChange(
-                int flag, boolean onBattery, boolean onBatteryScreenOff, int screenState) {
+        public Future<?> scheduleSyncDueToScreenStateChange(int flag, boolean onBattery,
+                boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
+            flags |= flag;
             return null;
         }
 
diff --git a/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java b/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
index 5862368..88ee405 100644
--- a/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
@@ -17,6 +17,10 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_ON;
+
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
@@ -53,7 +57,12 @@
         assertEquals(4, mProfile.getNumSpeedStepsInCpuCluster(1));
         assertEquals(60.0, mProfile.getAveragePowerForCpuCore(1, 3));
         assertEquals(3000.0, mProfile.getBatteryCapacity());
-        assertEquals(0.5, mProfile.getAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY));
+        assertEquals(0.5,
+                mProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0));
+        assertEquals(100.0,
+                mProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0));
+        assertEquals(800.0,
+                mProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0));
         assertEquals(100.0, mProfile.getAveragePower(PowerProfile.POWER_AUDIO));
         assertEquals(150.0, mProfile.getAveragePower(PowerProfile.POWER_VIDEO));
     }
diff --git a/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java b/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
index c695fc9..eee5d57 100644
--- a/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
@@ -16,6 +16,9 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_ON;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.ActivityManager;
@@ -39,24 +42,27 @@
     private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 43;
     private static final long MINUTE_IN_MS = 60 * 1000;
     private static final long MINUTE_IN_US = 60 * 1000 * 1000;
+    private static final long HOUR_IN_MS = 60 * MINUTE_IN_MS;
 
     @Rule
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
-            .setAveragePower(PowerProfile.POWER_SCREEN_ON, 36.0)
-            .setAveragePower(PowerProfile.POWER_SCREEN_FULL, 48.0);
+            .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0, 36.0)
+            .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0, 48.0)
+            .setNumDisplays(1);
 
     @Test
     public void testMeasuredEnergyBasedModel() {
         mStatsRule.initMeasuredEnergyStatsLocked();
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.noteScreenStateLocked(Display.STATE_ON, 0, 0, 0);
-        batteryStats.updateDisplayMeasuredEnergyStatsLocked(0, Display.STATE_ON, 0);
+        batteryStats.noteScreenStateLocked(0, Display.STATE_ON, 0, 0, 0);
+        batteryStats.updateDisplayMeasuredEnergyStatsLocked(new long[]{0},
+                new int[]{Display.STATE_ON}, 0);
         setProcState(APP_UID1, ActivityManager.PROCESS_STATE_TOP, true,
                 0, 0);
 
-        batteryStats.updateDisplayMeasuredEnergyStatsLocked(200_000_000, Display.STATE_ON,
-                15 * MINUTE_IN_MS);
+        batteryStats.updateDisplayMeasuredEnergyStatsLocked(new long[]{200_000_000},
+                new int[]{Display.STATE_ON}, 15 * MINUTE_IN_MS);
 
         setProcState(APP_UID1, ActivityManager.PROCESS_STATE_CACHED_EMPTY, false,
                 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
@@ -64,16 +70,16 @@
         setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP, true,
                 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
 
-        batteryStats.updateDisplayMeasuredEnergyStatsLocked(300_000_000, Display.STATE_ON,
-                60 * MINUTE_IN_MS);
+        batteryStats.updateDisplayMeasuredEnergyStatsLocked(new long[]{300_000_000},
+                new int[]{Display.STATE_ON}, 60 * MINUTE_IN_MS);
 
-        batteryStats.noteScreenStateLocked(Display.STATE_OFF,
+        batteryStats.noteScreenStateLocked(0, Display.STATE_OFF,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
         setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP_SLEEPING, false,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
 
-        batteryStats.updateDisplayMeasuredEnergyStatsLocked(100_000_000, Display.STATE_DOZE,
-                120 * MINUTE_IN_MS);
+        batteryStats.updateDisplayMeasuredEnergyStatsLocked(new long[]{100_000_000},
+                new int[]{Display.STATE_DOZE}, 120 * MINUTE_IN_MS);
 
         mStatsRule.setTime(120 * MINUTE_IN_US, 120 * MINUTE_IN_US);
 
@@ -126,24 +132,122 @@
                 .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
     }
 
+
     @Test
-    public void testPowerProfileBasedModel() {
+    public void testMeasuredEnergyBasedModel_multiDisplay() {
+        mStatsRule.initMeasuredEnergyStatsLocked()
+                .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 1, 60.0)
+                .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 1, 100.0)
+                .setNumDisplays(2);
+
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.noteScreenStateLocked(Display.STATE_ON, 0, 0, 0);
-        batteryStats.noteScreenBrightnessLocked(255, 0, 0);
-        setProcState(APP_UID1, ActivityManager.PROCESS_STATE_TOP, true,
-                0, 0);
+        final int[] screenStates = new int[]{Display.STATE_ON, Display.STATE_OFF};
 
-        batteryStats.noteScreenBrightnessLocked(100, 5 * MINUTE_IN_MS, 5 * MINUTE_IN_MS);
-        batteryStats.noteScreenBrightnessLocked(200, 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+        batteryStats.noteScreenStateLocked(0, screenStates[0], 0, 0, 0);
+        batteryStats.noteScreenStateLocked(1, screenStates[1], 0, 0, 0);
+        batteryStats.noteScreenBrightnessLocked(0, 255, 0, 0);
+        setProcState(APP_UID1, ActivityManager.PROCESS_STATE_TOP, true, 0, 0);
+        batteryStats.updateDisplayMeasuredEnergyStatsLocked(new long[]{300, 400}, screenStates, 0);
+
+        batteryStats.noteScreenBrightnessLocked(0, 100, 5 * MINUTE_IN_MS, 5 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(0, 200, 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
 
         setProcState(APP_UID1, ActivityManager.PROCESS_STATE_CACHED_EMPTY, false,
                 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
         setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP, true,
                 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
 
-        batteryStats.noteScreenStateLocked(Display.STATE_OFF,
+        screenStates[0] = Display.STATE_OFF;
+        screenStates[1] = Display.STATE_ON;
+        batteryStats.noteScreenStateLocked(0, screenStates[0],
+                80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        batteryStats.noteScreenStateLocked(1, screenStates[1], 80 * MINUTE_IN_MS,
+                80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        batteryStats.updateDisplayMeasuredEnergyStatsLocked(new long[]{600_000_000, 500},
+                screenStates, 80 * MINUTE_IN_MS);
+
+        batteryStats.noteScreenBrightnessLocked(1, 25, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(1, 250, 86 * MINUTE_IN_MS, 86 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(1, 75, 98 * MINUTE_IN_MS, 98 * MINUTE_IN_MS);
+
+        screenStates[1] = Display.STATE_OFF;
+        batteryStats.noteScreenStateLocked(1, screenStates[1], 110 * MINUTE_IN_MS,
+                110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+        batteryStats.updateDisplayMeasuredEnergyStatsLocked(new long[]{700, 800_000_000},
+                screenStates, 110 * MINUTE_IN_MS);
+
+        setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP_SLEEPING, false,
+                110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+
+        mStatsRule.setTime(120 * MINUTE_IN_US, 120 * MINUTE_IN_US);
+
+        ScreenPowerCalculator calculator =
+                new ScreenPowerCalculator(mStatsRule.getPowerProfile());
+
+        mStatsRule.apply(calculator);
+
+        BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+        assertThat(deviceConsumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(110 * MINUTE_IN_MS);
+        // (600000000 + 800000000) uAs * (1 mA / 1000 uA) * (1 h / 3600 s)  = 166.66666 mAh
+        assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(388.88888);
+        assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
+
+        UidBatteryConsumer uid1 = mStatsRule.getUidBatteryConsumer(APP_UID1);
+        assertThat(uid1.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(20 * MINUTE_IN_MS);
+
+        // Uid1 ran for 20 out of 80 min during the first Display update.
+        // It also ran for 5 out of 45 min during the second Display update:
+        // Uid1 charge = 20 / 80 * 600000000 mAs = 41.66666 mAh
+        assertThat(uid1.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(41.66666);
+        assertThat(uid1.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
+
+        UidBatteryConsumer uid2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+        assertThat(uid2.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(90 * MINUTE_IN_MS);
+
+        // Uid2 ran for 60 out of 80 min during the first Display update.
+        // It also ran for all of the second Display update:
+        // Uid1 charge = 60 / 80 * 600000000 + 800000000 mAs = 347.22222 mAh
+        assertThat(uid2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(347.22222);
+        assertThat(uid2.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
+
+        BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
+        assertThat(appsConsumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(110 * MINUTE_IN_MS);
+        assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(388.88888);
+        assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
+
+    }
+
+    @Test
+    public void testPowerProfileBasedModel() {
+        BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+
+        batteryStats.noteScreenStateLocked(0, Display.STATE_ON, 0, 0, 0);
+        batteryStats.noteScreenBrightnessLocked(0, 255, 0, 0);
+        setProcState(APP_UID1, ActivityManager.PROCESS_STATE_TOP, true,
+                0, 0);
+
+        batteryStats.noteScreenBrightnessLocked(0, 100, 5 * MINUTE_IN_MS, 5 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(0, 200, 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+
+        setProcState(APP_UID1, ActivityManager.PROCESS_STATE_CACHED_EMPTY, false,
+                20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+        setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP, true,
+                20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+
+        batteryStats.noteScreenStateLocked(0, Display.STATE_OFF,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
         setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP_SLEEPING, false,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
@@ -194,6 +298,95 @@
                 .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
     }
 
+
+    @Test
+    public void testPowerProfileBasedModel_multiDisplay() {
+        mStatsRule.setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 1, 60.0)
+                .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 1, 100.0)
+                .setNumDisplays(2);
+
+        BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+
+        batteryStats.noteScreenStateLocked(0, Display.STATE_ON, 0, 0, 0);
+        batteryStats.noteScreenStateLocked(1, Display.STATE_OFF, 0, 0, 0);
+        batteryStats.noteScreenBrightnessLocked(0, 255, 0, 0);
+        setProcState(APP_UID1, ActivityManager.PROCESS_STATE_TOP, true,
+                0, 0);
+
+        batteryStats.noteScreenBrightnessLocked(0, 100, 5 * MINUTE_IN_MS, 5 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(0, 200, 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+
+        setProcState(APP_UID1, ActivityManager.PROCESS_STATE_CACHED_EMPTY, false,
+                20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+        setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP, true,
+                20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+
+        batteryStats.noteScreenStateLocked(0, Display.STATE_OFF,
+                80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        batteryStats.noteScreenStateLocked(1, Display.STATE_ON, 80 * MINUTE_IN_MS,
+                80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(1, 20, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+
+        batteryStats.noteScreenBrightnessLocked(1, 250, 86 * MINUTE_IN_MS, 86 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(1, 75, 98 * MINUTE_IN_MS, 98 * MINUTE_IN_MS);
+        batteryStats.noteScreenStateLocked(1, Display.STATE_OFF, 110 * MINUTE_IN_MS,
+                110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+
+        setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP_SLEEPING, false,
+                110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+
+        mStatsRule.setTime(120 * MINUTE_IN_US, 120 * MINUTE_IN_US);
+        ScreenPowerCalculator calculator =
+                new ScreenPowerCalculator(mStatsRule.getPowerProfile());
+
+        mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+        BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+        assertThat(deviceConsumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(110 * MINUTE_IN_MS);
+        // First display consumed 92 mAh.
+        // Second display ran for 0.5 hours at a base drain rate of 60 mA.
+        // 6 minutes (0.1 hours) spent in the first brightness level which drains an extra 10 mA.
+        // 12 minutes (0.2 hours) spent in the fifth brightness level which drains an extra 90 mA.
+        // 12 minutes (0.2 hours) spent in the second brightness level which drains an extra 30 mA.
+        // 92 + 60 * 0.5 + 10 * 0.1 + 90 * 0.2 + 30 * 0.2 = 147
+        assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(147);
+        assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        UidBatteryConsumer uid1 = mStatsRule.getUidBatteryConsumer(APP_UID1);
+        assertThat(uid1.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(20 * MINUTE_IN_MS);
+
+        // Uid1 took 20 out of the total of 110 min of foreground activity
+        // Uid1 charge = 20 / 110 * 147.0 = 23.0 mAh
+        assertThat(uid1.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(26.72727);
+        assertThat(uid1.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        UidBatteryConsumer uid2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+        assertThat(uid2.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(90 * MINUTE_IN_MS);
+
+        // Uid2 took 90 out of the total of 110 min of foreground activity
+        // Uid2 charge = 90 / 110 * 92.0 = 69.0 mAh
+        assertThat(uid2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(120.272727);
+        assertThat(uid2.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
+        assertThat(appsConsumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(110 * MINUTE_IN_MS);
+        assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isWithin(PRECISION).of(147);
+        assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_SCREEN))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+    }
+
     private void setProcState(int uid, int procState, boolean resumed, long realtimeMs,
             long uptimeMs) {
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
diff --git a/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java b/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
index 272f228..0f05be0 100644
--- a/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
+++ b/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
@@ -24,6 +24,7 @@
 import android.os.Parcel;
 import android.os.UserHandle;
 import android.util.ArrayMap;
+import android.view.InsetsVisibilities;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -59,7 +60,8 @@
                 new Binder() /* imeToken */,
                 true /* navbarColorManagedByIme */,
                 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
-                true /* appFullscreen */,
+                new InsetsVisibilities() /* requestedVisibilities */,
+                "test" /* packageName */,
                 new int[0] /* transientBarTypes */);
 
         final RegisterStatusBarResult copy = clone(original);
@@ -79,7 +81,8 @@
         assertThat(copy.mImeToken).isSameInstanceAs(original.mImeToken);
         assertThat(copy.mNavbarColorManagedByIme).isEqualTo(original.mNavbarColorManagedByIme);
         assertThat(copy.mBehavior).isEqualTo(original.mBehavior);
-        assertThat(copy.mAppFullscreen).isEqualTo(original.mAppFullscreen);
+        assertThat(copy.mRequestedVisibilities).isEqualTo(original.mRequestedVisibilities);
+        assertThat(copy.mPackageName).isEqualTo(original.mPackageName);
         assertThat(copy.mTransientBarTypes).isEqualTo(original.mTransientBarTypes);
     }
 
diff --git a/core/tests/mockingcoretests/src/android/window/ConfigurationHelperTest.java b/core/tests/mockingcoretests/src/android/window/ConfigurationHelperTest.java
new file mode 100644
index 0000000..996d7b4
--- /dev/null
+++ b/core/tests/mockingcoretests/src/android/window/ConfigurationHelperTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+
+import android.app.ResourcesManager;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+/**
+ * Tests for {@link ConfigurationHelper}
+ *
+ * <p>Build/Install/Run:
+ *  atest FrameworksMockingCoreTests:ConfigurationHelperTest
+ *
+ * <p>This test class is a part of Window Manager Service tests and specified in
+ * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class ConfigurationHelperTest {
+    MockitoSession mMockitoSession;
+    ResourcesManager mResourcesManager;
+
+    @Before
+    public void setUp() {
+        mMockitoSession = mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(ResourcesManager.class)
+                .startMocking();
+        doReturn(mock(ResourcesManager.class)).when(ResourcesManager::getInstance);
+        mResourcesManager = ResourcesManager.getInstance();
+    }
+
+    @After
+    public void tearDown() {
+        mMockitoSession.finishMocking();
+    }
+
+    @Test
+    public void testShouldUpdateResources_NullConfig_ReturnsTrue() {
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), null /* config */,
+                new Configuration(), new Configuration(), false /* displayChanged */,
+                null /* configChanged */)).isTrue();
+    }
+
+    @Test
+    public void testShouldUpdateResources_DisplayChanged_ReturnsTrue() {
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), new Configuration(),
+                new Configuration(), new Configuration(), true /* displayChanged */,
+                null /* configChanged */)).isTrue();
+    }
+
+    @Test
+    public void testShouldUpdateResources_DifferentResources_ReturnsTrue() {
+        doReturn(false).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any());
+
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), new Configuration(),
+                new Configuration(), new Configuration(), false /* displayChanged */,
+                null /* configChanged */)).isTrue();
+    }
+
+    @Test
+    public void testShouldUpdateResources_DifferentBounds_ReturnsTrue() {
+        doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any());
+
+        final Configuration config = new Configuration();
+        config.windowConfiguration.setBounds(new Rect(0, 0, 10, 10));
+        config.windowConfiguration.setMaxBounds(new Rect(0, 0, 20, 20));
+
+        final Configuration newConfig = new Configuration();
+        newConfig.windowConfiguration.setBounds(new Rect(0, 0, 20, 20));
+        newConfig.windowConfiguration.setMaxBounds(new Rect(0, 0, 20, 20));
+
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig,
+                new Configuration(), false /* displayChanged */, null /* configChanged */))
+                .isTrue();
+    }
+
+    @Test
+    public void testShouldUpdateResources_SameConfig_ReturnsFalse() {
+        doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any());
+
+        final Configuration config = new Configuration();
+        final Configuration newConfig = new Configuration();
+
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig,
+                new Configuration(), false /* displayChanged */, null /* configChanged */))
+                .isFalse();
+    }
+
+    @Test
+    public void testShouldUpdateResources_DifferentConfig_ReturnsTrue() {
+        doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any());
+
+        final Configuration config = new Configuration();
+        final Configuration newConfig = new Configuration();
+        newConfig.setToDefaults();
+
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig,
+                new Configuration(), false /* displayChanged */, null /* configChanged */))
+                .isTrue();
+    }
+
+    @Test
+    public void testShouldUpdateResources_DifferentNonPublicConfig_ReturnsTrue() {
+        doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any());
+
+        final Configuration config = new Configuration();
+        final Configuration newConfig = new Configuration();
+        newConfig.windowConfiguration.setAppBounds(new Rect(0, 0, 10, 10));
+
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig,
+                new Configuration(), false /* displayChanged */, null /* configChanged */))
+                .isTrue();
+    }
+
+    @Test
+    public void testShouldUpdateResources_OverrideConfigChanged_ReturnsFalse() {
+        doReturn(true).when(mResourcesManager).isSameResourcesOverrideConfig(any(), any());
+
+        final Configuration config = new Configuration();
+        final Configuration newConfig = new Configuration();
+        final boolean configChanged = true;
+
+        assertThat(ConfigurationHelper.shouldUpdateResources(new Binder(), config, newConfig,
+                new Configuration(), false /* displayChanged */, configChanged))
+                .isEqualTo(configChanged);
+    }
+}
diff --git a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
new file mode 100644
index 0000000..fa4aa80
--- /dev/null
+++ b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import static android.content.pm.ActivityInfo.CONFIG_LOCALE;
+import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
+import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
+import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
+import static android.content.res.Configuration.SCREENLAYOUT_COMPAT_NEEDED;
+import static android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_LTR;
+import static android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_RTL;
+import static android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_UNDEFINED;
+import static android.content.res.Configuration.SCREENLAYOUT_LONG_NO;
+import static android.content.res.Configuration.SCREENLAYOUT_LONG_YES;
+import static android.content.res.Configuration.SCREENLAYOUT_ROUND_NO;
+import static android.content.res.Configuration.SCREENLAYOUT_ROUND_UNDEFINED;
+import static android.content.res.Configuration.SCREENLAYOUT_ROUND_YES;
+import static android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE;
+import static android.content.res.Configuration.SCREENLAYOUT_SIZE_NORMAL;
+import static android.content.res.Configuration.SCREENLAYOUT_SIZE_SMALL;
+import static android.content.res.Configuration.SCREENLAYOUT_SIZE_XLARGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.res.Configuration;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link SizeConfigurationBuckets}
+ *
+ * Build/Install/Run:
+ *  atest FrameworksMockingCoreTests:SizeConfigurationBucketsTest
+ */
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class SizeConfigurationBucketsTest {
+
+    /**
+     * Tests that a change in any of the non-size-related screen layout fields results in
+     * {@link SizeConfigurationBuckets#areNonSizeLayoutFieldsUnchanged} returning false.
+     */
+    @Test
+    public void testNonSizeRelatedScreenLayoutFields() {
+        // Test layout direction
+        assertEquals(true, SizeConfigurationBuckets
+                .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_LAYOUTDIR_UNDEFINED));
+        assertEquals(false, SizeConfigurationBuckets
+                .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_LAYOUTDIR_LTR));
+        assertEquals(false, SizeConfigurationBuckets
+                .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_LAYOUTDIR_RTL));
+
+        // Test layout roundness
+        assertEquals(true, SizeConfigurationBuckets
+                .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_ROUND_UNDEFINED));
+        assertEquals(false, SizeConfigurationBuckets
+                .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_ROUND_NO));
+        assertEquals(false, SizeConfigurationBuckets
+                .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_ROUND_YES));
+
+        // Test layout compat needed
+        assertEquals(false, SizeConfigurationBuckets
+                .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_COMPAT_NEEDED));
+    }
+
+    /**
+     * Tests that null size configuration buckets unflips the correct configuration flags.
+     */
+    @Test
+    public void testNullSizeConfigurationBuckets() {
+        // Check that all 3 size configurations are filtered out of the diff if the buckets are null
+        // and non-size attributes of screen layout are unchanged. Add a non-size related config
+        // change (i.e. CONFIG_LOCALE) to test that the diff is not set to zero.
+        final int diff = CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE | CONFIG_SCREEN_LAYOUT
+                | CONFIG_LOCALE;
+        final int filteredDiffNonSizeLayoutUnchanged = SizeConfigurationBuckets.filterDiff(diff,
+                Configuration.EMPTY, Configuration.EMPTY, null);
+        assertEquals(CONFIG_LOCALE, filteredDiffNonSizeLayoutUnchanged);
+
+        // Check that only screen size and smallest screen size are filtered out of the diff if the
+        // buckets are null and non-size attributes of screen layout are changed.
+        final Configuration newConfig = new Configuration();
+        newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES;
+        final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff,
+                Configuration.EMPTY, newConfig, null);
+        assertEquals(CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE, filteredDiffNonSizeLayoutChanged);
+    }
+
+    /**
+     * Tests that {@link SizeConfigurationBuckets.crossesSizeThreshold()} correctly checks whether
+     * the bucket thresholds have or have not been crossed. This test includes boundary checks
+     * to ensure that arithmetic is inclusive and exclusive in the right places.
+     */
+    @Test
+    public void testCrossesSizeThreshold() {
+        final int[] thresholds = new int[] { 360, 600 };
+        final int nThresholds = thresholds.length;
+        for (int i = -1; i < nThresholds; i++) {
+            final int minValueInBucket = i < 0 ? 0 : thresholds[i];
+            final int maxValueInBucket = i < nThresholds - 1
+                    ? thresholds[i + 1] - 1 : Integer.MAX_VALUE;
+            final int bucketRange = maxValueInBucket - minValueInBucket;
+            // Set old value to 1/4 in between the two thresholds.
+            final int oldValue = (int) (minValueInBucket + bucketRange * 0.25);
+            // Test 3 values of new value spread across bucket range: minValueInBucket, bucket
+            // midpoint, and max value in bucket. In all 3 cases, the bucket has not changed so
+            // {@link SizeConfigurationBuckets#crossedSizeThreshold()} should return false.
+            checkCrossesSizeThreshold(thresholds, oldValue, minValueInBucket, false);
+            checkCrossesSizeThreshold(thresholds, oldValue,
+                    (int) (minValueInBucket + bucketRange * 0.5), false);
+            checkCrossesSizeThreshold(thresholds, oldValue, maxValueInBucket, false);
+            // Test 4 values of size spread outside of bucket range: more than 1 less than min
+            // value, 1 less than min value, 1 more than max value, and more than 1 more than max
+            // value. In all 4 cases, the bucket has changed so
+            // {@link SizeConfigurationBuckets#crossedSizeThreshold()} should return true.
+            // Only test less than min value if min value > 0.
+            if (minValueInBucket > 0) {
+                checkCrossesSizeThreshold(thresholds, oldValue, minValueInBucket - 20, true);
+                checkCrossesSizeThreshold(thresholds, oldValue, minValueInBucket - 1, true);
+            }
+            // Only test greater than max value if not in highest bucket.
+            if (i < nThresholds - 1) {
+                checkCrossesSizeThreshold(thresholds, oldValue, maxValueInBucket + 1, true);
+                checkCrossesSizeThreshold(thresholds, oldValue, maxValueInBucket + 20, true);
+            }
+        }
+    }
+
+    /**
+     * Tests that if screen layout size changed but did not cross a threshold, the filtered diff
+     * does not include screen layout.
+     */
+    @Test
+    public void testScreenLayoutFilteredIfSizeDidNotCrossThreshold() {
+        // Set only small and large sizes
+        final Configuration[] sizeConfigs = new Configuration[2];
+        sizeConfigs[0] = new Configuration();
+        sizeConfigs[0].screenLayout |= SCREENLAYOUT_SIZE_SMALL;
+        sizeConfigs[1] = new Configuration();
+        sizeConfigs[1].screenLayout |= SCREENLAYOUT_SIZE_LARGE;
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs);
+
+        // Change screen layout size from small to normal and check that screen layout flag is
+        // not part of the diff because a threshold was not crossed.
+        final int diff = CONFIG_SCREEN_LAYOUT;
+        final Configuration oldConfig = new Configuration();
+        oldConfig.screenLayout |= SCREENLAYOUT_SIZE_SMALL;
+        final Configuration newConfig = new Configuration();
+        newConfig.screenLayout |= SCREENLAYOUT_SIZE_NORMAL;
+        final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig,
+                sizeBuckets);
+        assertEquals(0, filteredDiff);
+
+        // If a non-size attribute of screen layout changed, then screen layout should not be
+        // filtered from the diff.
+        newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES;
+        final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff,
+                oldConfig, newConfig, sizeBuckets);
+        assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiffNonSizeLayoutChanged);
+    }
+
+    /**
+     * Tests that if screen layout size changed and did cross a threshold, the filtered diff
+     * includes screen layout.
+     */
+    @Test
+    public void testScreenLayoutNotFilteredIfSizeCrossedThreshold() {
+        // Set only small and normal sizes
+        final Configuration[] sizeConfigs = new Configuration[2];
+        sizeConfigs[0] = new Configuration();
+        sizeConfigs[0].screenLayout |= SCREENLAYOUT_SIZE_SMALL;
+        sizeConfigs[1] = new Configuration();
+        sizeConfigs[1].screenLayout |= SCREENLAYOUT_SIZE_NORMAL;
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs);
+
+        // Change screen layout size from small to normal and check that screen layout flag is
+        // still part of the diff because a threshold was crossed.
+        final int diff = CONFIG_SCREEN_LAYOUT;
+        final Configuration oldConfig = new Configuration();
+        oldConfig.screenLayout |= SCREENLAYOUT_SIZE_SMALL;
+        final Configuration newConfig = new Configuration();
+        newConfig.screenLayout |= SCREENLAYOUT_SIZE_NORMAL;
+        final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig,
+                sizeBuckets);
+        assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiff);
+    }
+
+    /**
+     * Tests that anytime screen layout size is decreased, the filtered diff still includes screen
+     * layout.
+     */
+    @Test
+    public void testScreenLayoutNotFilteredIfSizeDecreased() {
+        // The size thresholds can be anything, but can't be null
+        final int[] horizontalThresholds = new int[] { 360, 600 };
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(
+                horizontalThresholds, null /* vertical */, null /* smallest */,
+                null /* screenLayoutSize */, false /* screenLayoutLongSet */);
+        final int[] sizeValuesInOrder = new int[] {
+                SCREENLAYOUT_SIZE_SMALL, SCREENLAYOUT_SIZE_NORMAL, SCREENLAYOUT_SIZE_LARGE,
+                SCREENLAYOUT_SIZE_XLARGE
+        };
+        final int nSizes = sizeValuesInOrder.length;
+        for (int larger = nSizes - 1; larger > 0; larger--) {
+            for (int smaller = larger - 1; smaller >= 0; smaller--) {
+                final Configuration oldConfig = new Configuration();
+                oldConfig.screenLayout |= sizeValuesInOrder[larger];
+                final Configuration newConfig = new Configuration();
+                newConfig.screenLayout |= sizeValuesInOrder[smaller];
+                assertTrue(String.format("oldSize=%d, newSize=%d", oldConfig.screenLayout,
+                        newConfig.screenLayout),
+                        sizeBuckets.crossesScreenLayoutSizeThreshold(oldConfig, newConfig));
+            }
+        }
+    }
+
+    /**
+     * Tests that if screen layout long changed but did not cross a threshold, the filtered diff
+     * does not include screen layout.
+     */
+    @Test
+    public void testScreenLayoutFilteredIfLongDidNotCrossThreshold() {
+        // Do not set any long threshold
+        final Configuration[] sizeConfigs = new Configuration[1];
+        sizeConfigs[0] = Configuration.EMPTY;
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs);
+
+        // Change screen layout long from not long to long and check that screen layout flag is
+        // not part of the diff because a threshold was not crossed.
+        final int diff = CONFIG_SCREEN_LAYOUT;
+        final Configuration oldConfig = new Configuration();
+        oldConfig.screenLayout |= SCREENLAYOUT_LONG_NO;
+        final Configuration newConfig = new Configuration();
+        newConfig.screenLayout |= SCREENLAYOUT_LONG_YES;
+        final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig,
+                sizeBuckets);
+        assertEquals(0, filteredDiff);
+
+        // If a non-size attribute of screen layout changed, then screen layout should not be
+        // filtered from the diff.
+        newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES;
+        final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff,
+                oldConfig, newConfig, sizeBuckets);
+        assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiffNonSizeLayoutChanged);
+    }
+
+    /**
+     * Tests that if screen layout long changed and did cross a threshold, the filtered diff
+     * includes screen layout.
+     */
+    @Test
+    public void testScreenLayoutNotFilteredIfLongCrossedThreshold() {
+        // Set only small and normal sizes
+        final Configuration[] sizeConfigs = new Configuration[1];
+        sizeConfigs[0] = new Configuration();
+        sizeConfigs[0].screenLayout |= SCREENLAYOUT_LONG_NO;
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs);
+
+        // Change screen layout long from not long to long and check that screen layout flag is
+        // still part of the diff because a threshold was crossed.
+        final int diff = CONFIG_SCREEN_LAYOUT;
+        final Configuration oldConfig = new Configuration();
+        oldConfig.screenLayout |= SCREENLAYOUT_LONG_NO;
+        final Configuration newConfig = new Configuration();
+        newConfig.screenLayout |= SCREENLAYOUT_LONG_YES;
+        final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig,
+                sizeBuckets);
+        assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiff);
+    }
+
+    /**
+     * Tests that horizontal buckets are correctly checked in
+     * {@link SizeConfigurationBuckets#filterDiff()}.
+     */
+    @Test
+    public void testHorizontalSizeThresholds() {
+        final int[] horizontalThresholds = new int[] { 360, 600 };
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(
+                horizontalThresholds, null /* vertical */, null /* smallest */,
+                null /* screenLayoutSize */, false /* screenLayoutLongSet */);
+
+        final Configuration oldConfig = new Configuration();
+        final Configuration newConfig = new Configuration();
+
+        oldConfig.screenWidthDp = 480;
+        // Test that value within bucket filters out screen size config
+        newConfig.screenWidthDp = 520;
+        assertEquals(0, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE, oldConfig,
+                newConfig, sizeBuckets));
+        // Test that value outside bucket does not filter out screen size config
+        newConfig.screenWidthDp = 640;
+        assertEquals(CONFIG_SCREEN_SIZE, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE,
+                oldConfig, newConfig, sizeBuckets));
+    }
+
+    /**
+     * Tests that vertical buckets are correctly checked in
+     * {@link SizeConfigurationBuckets#filterDiff()}.
+     */
+    @Test
+    public void testVerticalSizeThresholds() {
+        final int[] verticalThresholds = new int[] { 360, 600 };
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(
+                null, verticalThresholds /* vertical */, null /* smallest */,
+                null /* screenLayoutSize */, false /* screenLayoutLongSet */);
+
+        final Configuration oldConfig = new Configuration();
+        final Configuration newConfig = new Configuration();
+
+        oldConfig.screenHeightDp = 480;
+        // Test that value within bucket filters out screen size config
+        newConfig.screenHeightDp = 520;
+        assertEquals(0, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE, oldConfig,
+                newConfig, sizeBuckets));
+        // Test that value outside bucket does not filter out screen size config
+        newConfig.screenHeightDp = 640;
+        assertEquals(CONFIG_SCREEN_SIZE, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE,
+                oldConfig, newConfig, sizeBuckets));
+    }
+
+    /**
+     * Tests that smallest width buckets are correctly checked in
+     * {@link SizeConfigurationBuckets#filterDiff()}.
+     */
+    @Test
+    public void testSmallestWidthSizeThresholds() {
+        final int[] smallestWidthThresholds = new int[] { 360, 600 };
+        final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(
+                null, null /* vertical */, smallestWidthThresholds /* smallest */,
+                null /* screenLayoutSize */, false /* screenLayoutLongSet */);
+
+        final Configuration oldConfig = new Configuration();
+        final Configuration newConfig = new Configuration();
+
+        oldConfig.smallestScreenWidthDp = 480;
+        // Test that value within bucket filters out smallest screen size config
+        newConfig.smallestScreenWidthDp = 520;
+        assertEquals(0, SizeConfigurationBuckets.filterDiff(CONFIG_SMALLEST_SCREEN_SIZE, oldConfig,
+                newConfig, sizeBuckets));
+        // Test that value outside bucket does not filter out smallest screen size config
+        newConfig.smallestScreenWidthDp = 640;
+        assertEquals(CONFIG_SMALLEST_SCREEN_SIZE, SizeConfigurationBuckets.filterDiff(
+                CONFIG_SMALLEST_SCREEN_SIZE, oldConfig, newConfig, sizeBuckets));
+    }
+
+    private void checkCrossesSizeThreshold(int[] thresholds, int oldValue, int newValue,
+            boolean expected) {
+        final String errorString = String.format(
+                "thresholds=%s, oldValue=%d, newValue=%d, expected=%b", Arrays.toString(thresholds),
+                oldValue, newValue, expected);
+        final boolean actual = SizeConfigurationBuckets.crossesSizeThreshold(thresholds, oldValue,
+                newValue);
+        assertEquals(errorString, expected, actual);
+    }
+}
diff --git a/data/etc/car/Android.bp b/data/etc/car/Android.bp
index 084e1db..d1e4322 100644
--- a/data/etc/car/Android.bp
+++ b/data/etc/car/Android.bp
@@ -130,6 +130,13 @@
 }
 
 prebuilt_etc {
+    name: "allowed_privapp_com.google.android.car.adaslocation",
+    sub_dir: "permissions",
+    src: "com.google.android.car.adaslocation.xml",
+    filename_from_src: true,
+}
+
+prebuilt_etc {
     name: "allowed_privapp_com.google.android.car.kitchensink",
     sub_dir: "permissions",
     src: "com.google.android.car.kitchensink.xml",
diff --git a/data/etc/car/com.android.car.carlauncher.xml b/data/etc/car/com.android.car.carlauncher.xml
index abde232..53d02a4 100644
--- a/data/etc/car/com.android.car.carlauncher.xml
+++ b/data/etc/car/com.android.car.carlauncher.xml
@@ -17,11 +17,13 @@
 <permissions>
     <privapp-permissions package="com.android.car.carlauncher">
         <permission name="android.permission.ACTIVITY_EMBEDDING"/>
+        <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
         <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
         <permission name="android.permission.MANAGE_USERS"/>
         <permission name="android.permission.MEDIA_CONTENT_CONTROL"/>
         <permission name="android.permission.PACKAGE_USAGE_STATS"/>
         <permission name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"/>
+        <permission name="android.car.permission.CONTROL_CAR_APP_LAUNCH"/>
     </privapp-permissions>
 </permissions>
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/data/etc/car/com.google.android.car.adaslocation.xml
similarity index 67%
copy from packages/SystemUI/res/drawable/media_output_dialog_background.xml
copy to data/etc/car/com.google.android.car.adaslocation.xml
index 3ceb0f6..cc1ef3c 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_background.xml
+++ b/data/etc/car/com.google.android.car.adaslocation.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
+  ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,10 +14,8 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android">
-    <shape android:shape="rectangle">
-        <corners android:radius="8dp" />
-        <solid android:color="?android:attr/colorBackground" />
-    </shape>
-</inset>
+<permissions>
+    <privapp-permissions package="com.google.android.car.adaslocation">
+        <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+    </privapp-permissions>
+</permissions>
diff --git a/data/etc/car/com.google.android.car.kitchensink.xml b/data/etc/car/com.google.android.car.kitchensink.xml
index eeb65ae..ab162dd5 100644
--- a/data/etc/car/com.google.android.car.kitchensink.xml
+++ b/data/etc/car/com.google.android.car.kitchensink.xml
@@ -74,6 +74,8 @@
         <permission name="android.car.permission.CAR_TEST_SERVICE"/>
         <permission name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"/>
         <permission name="android.car.permission.CAR_VENDOR_EXTENSION"/>
+        <!-- use for AndroidCarApiTest -->
+        <permission name="android.car.permission.CONTROL_CAR_APP_LAUNCH"/>
         <permission name="android.car.permission.CONTROL_CAR_CLIMATE"/>
         <permission name="android.car.permission.CONTROL_CAR_DOORS"/>
         <permission name="android.car.permission.CONTROL_CAR_EXTERIOR_LIGHTS"/>
@@ -92,5 +94,6 @@
         <permission name="android.car.permission.CONTROL_CAR_EVS_ACTIVITY" />
         <permission name="android.car.permission.USE_CAR_EVS_CAMERA" />
         <permission name="android.car.permission.MONITOR_CAR_EVS_STATUS" />
+        <permission name="android.car.permission.USE_CAR_TELEMETRY_SERVICE" />
     </privapp-permissions>
 </permissions>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index b67988e..9573607 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -79,12 +79,6 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
-    "-2029985709": {
-      "message": "setFocusedTask: taskId=%d",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_FOCUS",
-      "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
-    },
     "-2024464438": {
       "message": "app-onAnimationFinished(): mOuter=%s",
       "level": "DEBUG",
@@ -103,11 +97,11 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
-    "-2006946193": {
-      "message": "setClientVisible: %s clientVisible=%b Callers=%s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_APP_TRANSITIONS",
-      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+    "-2010331310": {
+      "message": "resumeTopActivity: Top activity resumed (dontWaitForPause) %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
     "-2002500255": {
       "message": "Defer removing snapshot surface in %dms",
@@ -181,6 +175,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-1924376693": {
+      "message": " Setting Ready-group to %b. group=%s from %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "-1918702467": {
       "message": "onSyncFinishedDrawing %s",
       "level": "VERBOSE",
@@ -205,12 +205,24 @@
       "group": "WM_DEBUG_SYNC_ENGINE",
       "at": "com\/android\/server\/wm\/BLASTSyncEngine.java"
     },
+    "-1898316768": {
+      "message": "Unable to retrieve window container to start layer mirroring for display %d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-1895337367": {
       "message": "Delete root task display=%d winMode=%d",
       "level": "VERBOSE",
       "group": "WM_DEBUG_WINDOW_ORGANIZER",
       "at": "com\/android\/server\/wm\/TaskOrganizerController.java"
     },
+    "-1886145147": {
+      "message": "resumeTopActivity: Going to sleep and all paused",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "-1884933373": {
       "message": "enableScreenAfterBoot: mDisplayEnabled=%b mForceDisplayEnabled=%b mShowingBootMessages=%b mSystemBooted=%b. %s",
       "level": "INFO",
@@ -247,12 +259,6 @@
       "group": "WM_DEBUG_APP_TRANSITIONS_ANIM",
       "at": "com\/android\/server\/wm\/AppTransition.java"
     },
-    "-1861864501": {
-      "message": "resumeTopActivityLocked: Going to sleep and all paused",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-1844540996": {
       "message": "  Initial targets: %s",
       "level": "VERBOSE",
@@ -325,12 +331,6 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimationController.java"
     },
-    "-1768090656": {
-      "message": "Re-launching after pause: %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-1750384749": {
       "message": "Launch on display check: allow launch on public display",
       "level": "DEBUG",
@@ -415,12 +415,6 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "-1655805455": {
-      "message": "Enqueue pending stop if needed: %s wasStopping=%b visibleRequested=%b",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-1647332198": {
       "message": "remove RecentTask %s when finishing user %d",
       "level": "INFO",
@@ -433,6 +427,12 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/ResetTargetTaskHelper.java"
     },
+    "-1633115609": {
+      "message": "Key dispatch not paused for screen off",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "-1632122349": {
       "message": "Changing surface while display frozen: %s",
       "level": "VERBOSE",
@@ -487,6 +487,12 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
+    "-1564228464": {
+      "message": "App died while pausing: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "-1559645910": {
       "message": "Looking for task of type=%s, taskAffinity=%s, intent=%s, info=%s, preferredTDA=%s",
       "level": "DEBUG",
@@ -499,6 +505,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-1556507536": {
+      "message": "Passing transform hint %d for window %s%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-1554521902": {
       "message": "showInsets(ime) was requested by different window: %s ",
       "level": "WARN",
@@ -541,6 +553,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "-1501564055": {
+      "message": "Organized TaskFragment is not ready= %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_APP_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/AppTransitionController.java"
+    },
     "-1499134947": {
       "message": "Removing starting %s from %s",
       "level": "VERBOSE",
@@ -565,11 +583,11 @@
       "group": "WM_DEBUG_CONFIGURATION",
       "at": "com\/android\/server\/wm\/ActivityStarter.java"
     },
-    "-1492696222": {
-      "message": "App died during pause, not stopping: %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
+    "-1483435730": {
+      "message": "InsetsSource setWin %s for type %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WINDOW_INSETS",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
     },
     "-1480772131": {
       "message": "No app or window is requesting an orientation, return %d for display id=%d",
@@ -577,6 +595,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "-1478175541": {
+      "message": "No longer animating wallpaper targets!",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "-1474602871": {
       "message": "Launch on display check: disallow launch on virtual display for not-embedded activity.",
       "level": "DEBUG",
@@ -613,6 +637,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-1442613680": {
+      "message": " Creating Ready-group for Transition %d with root=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "-1438175584": {
       "message": "Input focus has changed to %s display=%d",
       "level": "VERBOSE",
@@ -637,12 +667,24 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-1421296808": {
+      "message": "Moving to RESUMED: %s (in existing)",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "-1419762046": {
       "message": "moveRootTaskToDisplay: moving taskId=%d to displayId=%d",
       "level": "DEBUG",
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
     },
+    "-1419461256": {
+      "message": "resumeTopActivity: Resumed %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "-1413901262": {
       "message": "startRecentsActivity(): intent=%s",
       "level": "DEBUG",
@@ -655,6 +697,12 @@
       "group": "WM_DEBUG_IME",
       "at": "com\/android\/server\/wm\/ImeInsetsSourceProvider.java"
     },
+    "-1394745488": {
+      "message": "ControlAdapter onAnimationCancelled mSource: %s mControlTarget: %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_WINDOW_INSETS",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "-1391944764": {
       "message": "SURFACE DESTROY: %s. %s",
       "level": "INFO",
@@ -703,11 +751,11 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "-1312861660": {
-      "message": "notifyInsetsControlChanged for %s ",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/WindowState.java"
+    "-1311436264": {
+      "message": "Unregister task fragment organizer=%s uid=%d pid=%d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
     },
     "-1305966693": {
       "message": "Sending position change to %s, onTop: %b",
@@ -805,6 +853,18 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-1187377055": {
+      "message": "Enqueue pending stop if needed: %s wasStopping=%b visibleRequested=%b",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
+    "-1185473319": {
+      "message": "ControlAdapter startAnimation mSource: %s controlTarget: %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_WINDOW_INSETS",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "-1176488860": {
       "message": "SURFACE isSecure=%b: %s",
       "level": "INFO",
@@ -919,12 +979,6 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimation.java"
     },
-    "-1066383762": {
-      "message": "Sleep still waiting to pause %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-1060365734": {
       "message": "Attempted to add QS dialog window with bad token %s.  Aborting.",
       "level": "WARN",
@@ -985,6 +1039,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "-957060823": {
+      "message": "Moving to PAUSING: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "-951939129": {
       "message": "Unregister task organizer=%s uid=%d",
       "level": "VERBOSE",
@@ -1051,6 +1111,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-904499590": {
+      "message": "Provided surface for layer mirroring on display %d is not present, so do not update the surface",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-883738232": {
       "message": "Adding more than one toast window for UID at a time.",
       "level": "WARN",
@@ -1153,6 +1219,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-779535710": {
+      "message": "Transition %d: Set %s as transient-launch",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "-775004869": {
       "message": "Not a match: %s",
       "level": "DEBUG",
@@ -1201,6 +1273,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-706481945": {
+      "message": "TaskFragment parent info changed name=%s parentTaskId=%d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "-705939410": {
       "message": "Waiting for pause to complete...",
       "level": "VERBOSE",
@@ -1213,6 +1291,12 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
+    "-702650156": {
+      "message": "Override with TaskFragment remote animation for transit=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_APP_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/AppTransitionController.java"
+    },
     "-701167286": {
       "message": "applyAnimation: transit=%s, enter=%b, wc=%s",
       "level": "VERBOSE",
@@ -1237,12 +1321,6 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
-    "-672228342": {
-      "message": "resumeTopActivityLocked: Top activity resumed %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-668956537": {
       "message": "  THUMBNAIL %s: CREATE",
       "level": "INFO",
@@ -1255,6 +1333,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "-663411559": {
+      "message": "Going ahead with updating layer mirroring for display %d to new bounds %s and\/or orientation %d.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-655104359": {
       "message": "Frontmost changed immersion: %s",
       "level": "DEBUG",
@@ -1267,11 +1351,11 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
-    "-650261962": {
-      "message": "Sleep needs to pause %s",
+    "-648891906": {
+      "message": "Activity not running or entered PiP, resuming next.",
       "level": "VERBOSE",
       "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
     "-641258376": {
       "message": "realStartActivityLocked: Skipping start of r=%s some activities pausing...",
@@ -1309,12 +1393,6 @@
       "group": "WM_DEBUG_BOOT",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "-606328116": {
-      "message": "resumeTopActivityLocked: Top activity resumed (dontWaitForPause) %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-597091183": {
       "message": "Delete TaskDisplayArea uid=%d",
       "level": "VERBOSE",
@@ -1375,11 +1453,11 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowAnimator.java"
     },
-    "-533690126": {
-      "message": "resumeTopActivityLocked: Resumed %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
+    "-542756093": {
+      "message": "TaskFragment vanished name=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
     },
     "-532081937": {
       "message": "  Commit activity becoming invisible: %s",
@@ -1387,11 +1465,11 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
-    "-527683022": {
-      "message": "resumeTopActivityLocked: Skip resume: some activity pausing.",
+    "-521613870": {
+      "message": "App died during pause, not stopping: %s",
       "level": "VERBOSE",
       "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
     "-519504830": {
       "message": "applyAnimation: anim=%s nextAppTransition=ANIM_CUSTOM transit=%s isEntrance=%b Callers=%s",
@@ -1477,18 +1555,6 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/DisplayRotation.java"
     },
-    "-427457280": {
-      "message": "App died while pausing: %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
-    "-417514857": {
-      "message": "Key dispatch not paused for screen off",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-415865166": {
       "message": "findFocusedWindow: Found new focus @ %s",
       "level": "VERBOSE",
@@ -1519,12 +1585,6 @@
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
-    "-395922585": {
-      "message": "InsetsSource setWin %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
-    },
     "-393505149": {
       "message": "unable to update pointer icon",
       "level": "WARN",
@@ -1537,6 +1597,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-384564722": {
+      "message": "Unable to start layer mirroring for display %d since the surface is not available.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-381475323": {
       "message": "DisplayContent: boot is waiting for window of type %d to be drawn",
       "level": "DEBUG",
@@ -1561,6 +1627,12 @@
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "-360208282": {
+      "message": "Animating wallpapers: old: %s hidden=%b new: %s hidden=%b",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "-354571697": {
       "message": "Existence Changed in transition %d: %s",
       "level": "VERBOSE",
@@ -1597,11 +1669,29 @@
       "group": "WM_DEBUG_LOCKTASK",
       "at": "com\/android\/server\/wm\/LockTaskController.java"
     },
-    "-303497363": {
-      "message": "reparent: moving activity=%s to task=%d at %d",
+    "-312353598": {
+      "message": "Executing finish of activity: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
+    "-310337305": {
+      "message": "Activity config changed during resume: %s, new next: %s",
       "level": "INFO",
-      "group": "WM_DEBUG_ADD_REMOVE",
-      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
+    "-309399422": {
+      "message": "Display %d state is now (%d), so update layer mirroring?",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
+    "-304728471": {
+      "message": "New wallpaper: target=%s prev=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
     },
     "-302468788": {
       "message": "Expected target rootTask=%s to be top most but found rootTask=%s",
@@ -1621,11 +1711,11 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "-279436615": {
-      "message": "Moving to PAUSING: %s",
+    "-275077723": {
+      "message": "New animation: %s old animation: %s",
       "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
     },
     "-262984451": {
       "message": "Relaunch failed %s",
@@ -1639,6 +1729,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-248761393": {
+      "message": "startPausing: taskFrag =%s mResumedActivity=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "-240296576": {
       "message": "handleAppTransitionReady: displayId=%d appTransition={%s} openingApps=[%s] closingApps=[%s] transit=%s",
       "level": "VERBOSE",
@@ -1651,12 +1747,6 @@
       "group": "WM_DEBUG_CONFIGURATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "-234244777": {
-      "message": "Activity config changed during resume: %s, new next: %s",
-      "level": "INFO",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-230587670": {
       "message": "SyncGroup %d:  Unfinished container: %s",
       "level": "VERBOSE",
@@ -1675,6 +1765,18 @@
       "group": "WM_DEBUG_WINDOW_MOVEMENT",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-190034097": {
+      "message": "Unable to retrieve window container to update layer mirroring for display %d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
+    "-182877285": {
+      "message": "Wallpaper layer changed: assigning layers + relayout",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-177040661": {
       "message": "Start rotation animation. customAnim=%s, mCurRotation=%s, mOriginalRotation=%s",
       "level": "DEBUG",
@@ -1723,24 +1825,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "-118786523": {
-      "message": "Resume failed; resetting state to %s: %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "-116086365": {
       "message": "******************** ENABLING SCREEN!",
       "level": "INFO",
       "group": "WM_DEBUG_SCREEN_ON",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "-112805366": {
-      "message": "InsetsSource updateVisibility serverVisible: %s clientVisible: %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
-    },
     "-108977760": {
       "message": "Sandbox max bounds for uid %s to bounds %s. config to never sandbox = %s, config to always sandbox = %s, letterboxing from mismatch with parent bounds = %s, has mCompatDisplayInsets = %s, should create compatDisplayInsets = %s",
       "level": "DEBUG",
@@ -1777,6 +1867,30 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/Session.java"
     },
+    "-80004683": {
+      "message": "Resume failed; resetting state to %s: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
+    "-79877120": {
+      "message": "Display %d has content (%b) so disable layer mirroring",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
+    "-70719599": {
+      "message": "Unregister remote animations for organizer=%s uid=%d pid=%d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
+    "-55185509": {
+      "message": "setFocusedTask: taskId=%d touchedActivity=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_FOCUS",
+      "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
+    },
     "-50336993": {
       "message": "moveFocusableActivityToTop: activity=%s",
       "level": "DEBUG",
@@ -1831,12 +1945,6 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimationController.java"
     },
-    "29780972": {
-      "message": "InsetsSource Control %s for target %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
-    },
     "35398067": {
       "message": "goodToGo(): onAnimationStart, transit=%s, apps=%d, wallpapers=%d, nonApps=%d",
       "level": "DEBUG",
@@ -1879,24 +1987,12 @@
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
-    "73987756": {
-      "message": "ControlAdapter onAnimationCancelled mSource: %s mControlTarget: %s",
-      "level": "INFO",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
-    },
     "74885950": {
       "message": "Waiting for top state to be released by %s",
       "level": "VERBOSE",
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java"
     },
-    "75707221": {
-      "message": "ControlAdapter startAnimation mSource: %s controlTarget: %s",
-      "level": "INFO",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
-    },
     "83950285": {
       "message": "removeAnimation(%d)",
       "level": "DEBUG",
@@ -1909,12 +2005,6 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowContextListenerController.java"
     },
-    "94402792": {
-      "message": "Moving to RESUMED: %s (in existing)",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "95216706": {
       "message": "hideIme target: %s ",
       "level": "DEBUG",
@@ -1933,12 +2023,24 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/AppTransitionController.java"
     },
+    "102618780": {
+      "message": "resumeTopActivity: Pausing %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "108170907": {
       "message": "Add starting %s: startingData=%s",
       "level": "VERBOSE",
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "114070759": {
+      "message": "New wallpaper target: %s prevTarget: %s caller=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "115358443": {
       "message": "Focus changing: %s -> %s",
       "level": "INFO",
@@ -2173,6 +2275,18 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "327461496": {
+      "message": "Complete pause: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
+    "341055768": {
+      "message": "resumeTopActivity: Skip resume: need to start pausing",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "342460966": {
       "message": "DRAG %s: pos=(%d,%d)",
       "level": "INFO",
@@ -2191,6 +2305,12 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "352982444": {
+      "message": " allReady query: used=%b override=%b states=[%s]",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "355720268": {
       "message": "stopFreezingDisplayLocked: Unfreezing now",
       "level": "DEBUG",
@@ -2227,11 +2347,11 @@
       "group": "WM_DEBUG_BOOT",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "391189028": {
-      "message": "pauseBackTasks: task=%s mResumedActivity=%s",
-      "level": "DEBUG",
+    "378825104": {
+      "message": "Enqueueing pending pause: %s",
+      "level": "VERBOSE",
       "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/TaskDisplayArea.java"
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
     "397105698": {
       "message": "grantEmbeddedWindowFocus remove request for win=%s dropped since no candidate was found",
@@ -2251,12 +2371,24 @@
       "group": "WM_SHOW_SURFACE_ALLOC",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "416924848": {
+      "message": "InsetsSource Control %s for target %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WINDOW_INSETS",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "417311568": {
       "message": "onResize: Resizing %s",
       "level": "DEBUG",
       "group": "WM_DEBUG_RESIZE",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "422634333": {
+      "message": "First draw done in potential wallpaper target %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "424524729": {
       "message": "Attempted to add wallpaper window with unknown token %s.  Aborting.",
       "level": "WARN",
@@ -2281,12 +2413,6 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "457951957": {
-      "message": "\tNot visible=%s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_REMOTE_ANIMATIONS",
-      "at": "com\/android\/server\/wm\/WallpaperAnimationAdapter.java"
-    },
     "463993897": {
       "message": "Aborted waiting for drawn: %s",
       "level": "WARN",
@@ -2311,6 +2437,12 @@
       "group": "WM_DEBUG_WINDOW_ORGANIZER",
       "at": "com\/android\/server\/wm\/DisplayAreaOrganizerController.java"
     },
+    "504397469": {
+      "message": "Unable to update layer mirroring for display %d to new bounds %s and\/or orientation %d, since the surface is not available.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "508887531": {
       "message": "applyAnimation voice: anim=%s transit=%s isEntrance=%b Callers=%s",
       "level": "VERBOSE",
@@ -2329,6 +2461,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowContainerThumbnail.java"
     },
+    "535103992": {
+      "message": "Wallpaper may change!  Adjusting",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/RootWindowContainer.java"
+    },
     "539077569": {
       "message": "Clear freezing of %s force=%b",
       "level": "VERBOSE",
@@ -2365,6 +2503,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
     },
+    "573582981": {
+      "message": "reparent: moving activity=%s to new task fragment in task=%d at %d",
+      "level": "INFO",
+      "group": "WM_DEBUG_ADD_REMOVE",
+      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+    },
     "579298675": {
       "message": "Moving to DESTROYED: %s (removed from history)",
       "level": "VERBOSE",
@@ -2467,6 +2611,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "660908897": {
+      "message": "Auto-PIP allowed, entering PIP mode directly: %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "662572728": {
       "message": "Attempted to add a toast window with bad token %s.  Aborting.",
       "level": "WARN",
@@ -2485,12 +2635,24 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "669361121": {
+      "message": "Sleep still need to stop %d activities",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "674932310": {
       "message": "Setting Intent of %s to target %s",
       "level": "VERBOSE",
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/Task.java"
     },
+    "675705156": {
+      "message": "resumeTopActivity: Top activity resumed %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "685047360": {
       "message": "Resizing window %s",
       "level": "VERBOSE",
@@ -2521,12 +2683,6 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java"
     },
-    "709500946": {
-      "message": "resumeTopActivityLocked: Skip resume: need to start pausing",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "715749922": {
       "message": "Allowlisting %d:%s",
       "level": "WARN",
@@ -2539,12 +2695,24 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "733466617": {
+      "message": "Wallpaper token %s visible=%b",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperWindowToken.java"
+    },
     "736692676": {
       "message": "Config is relaunching %s",
       "level": "VERBOSE",
       "group": "WM_DEBUG_CONFIGURATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "743418423": {
+      "message": "Sending TaskFragment error exception=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "744171317": {
       "message": "      SKIP: %s",
       "level": "VERBOSE",
@@ -2629,12 +2797,6 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
-    "897964776": {
-      "message": "Complete pause: %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "898863925": {
       "message": "Attempted to add QS dialog window with unknown token %s.  Aborting.",
       "level": "WARN",
@@ -2659,6 +2821,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/WindowStateAnimator.java"
     },
+    "935418348": {
+      "message": "resumeTopActivity: Skip resume: some activity pausing.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "950074526": {
       "message": "setLockTaskMode: Can't lock due to auth",
       "level": "WARN",
@@ -2707,11 +2875,11 @@
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java"
     },
-    "988389910": {
-      "message": "resumeTopActivityLocked: Pausing %s",
-      "level": "DEBUG",
+    "987903142": {
+      "message": "Sleep needs to pause %s",
+      "level": "VERBOSE",
       "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
     "996960396": {
       "message": "Starting Transition %d",
@@ -2719,24 +2887,36 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
-    "1001509841": {
-      "message": "Auto-PIP allowed, entering PIP mode directly: %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "1001904964": {
       "message": "***** BOOT TIMEOUT: forcing display enabled",
       "level": "WARN",
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1011462000": {
+      "message": "Re-launching after pause: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
+    "1022095595": {
+      "message": "TaskFragment info changed name=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "1023413388": {
       "message": "Finish waiting for pause of: %s",
       "level": "VERBOSE",
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "1030898920": {
+      "message": "notifyInsetsControlChanged for %s ",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WINDOW_INSETS",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "1033274509": {
       "message": "moveWindowTokenToDisplay: Attempted to move non-existing token: %s",
       "level": "WARN",
@@ -2755,6 +2935,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1047505501": {
+      "message": "notifyInsetsChanged for %s ",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WINDOW_INSETS",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "1047769218": {
       "message": "Finishing activity r=%s, result=%d, data=%s, reason=%s",
       "level": "VERBOSE",
@@ -2851,6 +3037,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "1178653181": {
+      "message": "Old wallpaper still the target.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "1186730970": {
       "message": "          no common mode yet, so set it",
       "level": "VERBOSE",
@@ -2875,6 +3067,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1210037962": {
+      "message": "Register remote animations for organizer=%s uid=%d pid=%d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "1219600119": {
       "message": "addWindow: win=%s Callers=%s",
       "level": "DEBUG",
@@ -2917,6 +3115,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "1284122013": {
+      "message": "TaskFragment appeared name=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "1288731814": {
       "message": "WindowState.hideLw: setting mFocusMayChange true",
       "level": "INFO",
@@ -3013,6 +3217,12 @@
       "group": "WM_DEBUG_SCREEN_ON",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1407569006": {
+      "message": "Display %d was already layer mirroring, so apply transformations if necessary",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "1417601133": {
       "message": "Enqueueing ADD_STARTING",
       "level": "VERBOSE",
@@ -3073,6 +3283,12 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
+    "1494644409": {
+      "message": "  Rejecting as detached: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "1495525537": {
       "message": "createWallpaperAnimations()",
       "level": "DEBUG",
@@ -3133,12 +3349,6 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "1533154777": {
-      "message": "notifyInsetsChanged for %s ",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_IME",
-      "at": "com\/android\/server\/wm\/WindowState.java"
-    },
     "1557732761": {
       "message": "For Intent %s bringing to top: %s",
       "level": "DEBUG",
@@ -3163,12 +3373,6 @@
       "group": "WM_DEBUG_APP_TRANSITIONS_ANIM",
       "at": "com\/android\/server\/wm\/WindowContainer.java"
     },
-    "1585450696": {
-      "message": "resumeTopActivityLocked: Restarting %s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "1589610525": {
       "message": "applyAnimation NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS: anim=%s transit=%s isEntrance=true Callers=%s",
       "level": "VERBOSE",
@@ -3217,6 +3421,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "1653025361": {
+      "message": "Register task fragment organizer=%s uid=%d pid=%d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_ORGANIZER",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "1653210583": {
       "message": "Removing app %s delayed=%b animation=%s animating=%b",
       "level": "VERBOSE",
@@ -3229,6 +3439,12 @@
       "group": "WM_DEBUG_IME",
       "at": "com\/android\/server\/wm\/InsetsStateController.java"
     },
+    "1670933628": {
+      "message": " Setting allReady override",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "1671994402": {
       "message": "Nulling last startingData",
       "level": "VERBOSE",
@@ -3253,6 +3469,12 @@
       "group": "WM_DEBUG_CONFIGURATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "1687376052": {
+      "message": "Display %d has no content and is on, so start layer mirroring for state %d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_LAYER_MIRRORING",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "1689989893": {
       "message": "SyncGroup %d: Set ready",
       "level": "VERBOSE",
@@ -3337,6 +3559,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1805116444": {
+      "message": "We don't support remote animation for Task with multiple TaskFragmentOrganizers.",
+      "level": "ERROR",
+      "group": "WM_DEBUG_APP_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/AppTransitionController.java"
+    },
     "1810019902": {
       "message": "TRANSIT_FLAG_OPEN_BEHIND,  adding %s to mOpeningApps",
       "level": "DEBUG",
@@ -3385,18 +3613,6 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "1837992242": {
-      "message": "Executing finish of activity: %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
-    "1847414670": {
-      "message": "Activity not running or entered PiP, resuming next.",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "1853793312": {
       "message": "Notify removed startingWindow %s",
       "level": "VERBOSE",
@@ -3409,6 +3625,12 @@
       "group": "WM_DEBUG_FOCUS",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "1856783490": {
+      "message": "resumeTopActivity: Restarting %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "1865125884": {
       "message": "finishScreenTurningOn: mAwake=%b, mScreenOnEarly=%b, mScreenOnFully=%b, mKeyguardDrawComplete=%b, mWindowManagerDrawComplete=%b",
       "level": "DEBUG",
@@ -3421,30 +3643,24 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "1884961873": {
-      "message": "Sleep still need to stop %d activities",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "1891501279": {
       "message": "cancelAnimation(): reason=%s",
       "level": "DEBUG",
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
-    "1894239744": {
-      "message": "Enqueueing pending pause: %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "1903353011": {
       "message": "notifyAppStopped: %s",
       "level": "VERBOSE",
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "1912291550": {
+      "message": "Sleep still waiting to pause %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/TaskFragment.java"
+    },
     "1918448345": {
       "message": "Task appeared taskId=%d",
       "level": "VERBOSE",
@@ -3505,6 +3721,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowAnimator.java"
     },
+    "1984843251": {
+      "message": "Hiding wallpaper %s from %s target=%s prev=%s callers=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "1995093920": {
       "message": "Checking to restart %s: changed=0x%s, handles=0x%s, mLastReportedConfiguration=%s",
       "level": "VERBOSE",
@@ -3535,6 +3757,12 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
+    "2024493888": {
+      "message": "\tWallpaper of display=%s is not visible",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_REMOTE_ANIMATIONS",
+      "at": "com\/android\/server\/wm\/WallpaperAnimationAdapter.java"
+    },
     "2028163120": {
       "message": "applyAnimation: anim=%s nextAppTransition=ANIM_SCALE_UP transit=%s isEntrance=%s Callers=%s",
       "level": "VERBOSE",
@@ -3559,18 +3787,18 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/AppTransitionController.java"
     },
-    "2057434754": {
-      "message": "\tvisible=%s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_REMOTE_ANIMATIONS",
-      "at": "com\/android\/server\/wm\/WallpaperAnimationAdapter.java"
-    },
     "2060978050": {
       "message": "moveWindowTokenToDisplay: Attempted to move token: %s to non-exiting displayId=%d",
       "level": "WARN",
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "2070726247": {
+      "message": "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WINDOW_INSETS",
+      "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
+    },
     "2083556954": {
       "message": "Set mOrientationChanging of %s",
       "level": "VERBOSE",
@@ -3663,6 +3891,9 @@
     "WM_DEBUG_KEEP_SCREEN_ON": {
       "tag": "WindowManager"
     },
+    "WM_DEBUG_LAYER_MIRRORING": {
+      "tag": "WindowManager"
+    },
     "WM_DEBUG_LOCKTASK": {
       "tag": "WindowManager"
     },
@@ -3696,6 +3927,12 @@
     "WM_DEBUG_TASKS": {
       "tag": "WindowManager"
     },
+    "WM_DEBUG_WALLPAPER": {
+      "tag": "WindowManager"
+    },
+    "WM_DEBUG_WINDOW_INSETS": {
+      "tag": "WindowManager"
+    },
     "WM_DEBUG_WINDOW_MOVEMENT": {
       "tag": "WindowManager"
     },
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
deleted file mode 100644
index b7a6039..0000000
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.extensions;
-
-import android.content.Context;
-
-/**
- * Provider class that will instantiate the library implementation. It must be included in the
- * vendor library, and the vendor implementation must match the signature of this class.
- */
-public class ExtensionProvider {
-    /**
-     * Provides a simple implementation of {@link ExtensionInterface} that can be replaced by
-     * an OEM by overriding this method.
-     */
-    public static ExtensionInterface getExtensionImpl(Context context) {
-        return new SampleExtensionImpl(context);
-    }
-
-    /**
-     * The support library will use this method to check API version compatibility.
-     * @return API version string in MAJOR.MINOR.PATCH-description format.
-     */
-    public static String getApiVersion() {
-        return "1.0.0-settings_sample";
-    }
-}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java
deleted file mode 100644
index 6a53efe..0000000
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.extensions;
-
-import android.app.Activity;
-
-import androidx.annotation.NonNull;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Basic implementation of the {@link ExtensionInterface}. An OEM can choose to use it as the base
- * class for their implementation.
- */
-abstract class StubExtension implements ExtensionInterface {
-
-    private ExtensionCallback mExtensionCallback;
-    private final Set<Activity> mWindowLayoutChangeListenerActivities = new HashSet<>();
-
-    StubExtension() {
-    }
-
-    @Override
-    public void setExtensionCallback(@NonNull ExtensionCallback extensionCallback) {
-        this.mExtensionCallback = extensionCallback;
-    }
-
-    @Override
-    public void onWindowLayoutChangeListenerAdded(@NonNull Activity activity) {
-        this.mWindowLayoutChangeListenerActivities.add(activity);
-        this.onListenersChanged();
-    }
-
-    @Override
-    public void onWindowLayoutChangeListenerRemoved(@NonNull Activity activity) {
-        this.mWindowLayoutChangeListenerActivities.remove(activity);
-        this.onListenersChanged();
-    }
-
-    void updateWindowLayout(@NonNull Activity activity,
-            @NonNull ExtensionWindowLayoutInfo newLayout) {
-        if (this.mExtensionCallback != null) {
-            mExtensionCallback.onWindowLayoutChanged(activity, newLayout);
-        }
-    }
-
-    @NonNull
-    Set<Activity> getActivitiesListeningForLayoutChanges() {
-        return mWindowLayoutChangeListenerActivities;
-    }
-
-    protected boolean hasListeners() {
-        return !mWindowLayoutChangeListenerActivities.isEmpty();
-    }
-
-    protected abstract void onListenersChanged();
-}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
new file mode 100644
index 0000000..bdf703c
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import android.app.ActivityThread;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
+import androidx.window.extensions.embedding.SplitController;
+import androidx.window.extensions.layout.WindowLayoutComponent;
+import androidx.window.extensions.layout.WindowLayoutComponentImpl;
+
+/**
+ * The reference implementation of {@link WindowExtensions} that implements the initial API version.
+ */
+public class WindowExtensionsImpl implements WindowExtensions {
+
+    private final Object mLock = new Object();
+    private volatile WindowLayoutComponent mWindowLayoutComponent;
+    private volatile SplitController mSplitController;
+
+    @Override
+    public int getVendorApiLevel() {
+        return 1;
+    }
+
+    /**
+     * Returns a reference implementation of {@link WindowLayoutComponent} if available,
+     * {@code null} otherwise. The implementation must match the API level reported in
+     * {@link WindowExtensions#getWindowLayoutComponent()}.
+     * @return {@link WindowLayoutComponent} OEM implementation
+     */
+    @Override
+    public WindowLayoutComponent getWindowLayoutComponent() {
+        if (mWindowLayoutComponent == null) {
+            synchronized (mLock) {
+                if (mWindowLayoutComponent == null) {
+                    Context context = ActivityThread.currentApplication();
+                    mWindowLayoutComponent = new WindowLayoutComponentImpl(context);
+                }
+            }
+        }
+        return mWindowLayoutComponent;
+    }
+
+    /**
+     * Returns a reference implementation of {@link ActivityEmbeddingComponent} if available,
+     * {@code null} otherwise. The implementation must match the API level reported in
+     * {@link WindowExtensions#getWindowLayoutComponent()}.
+     * @return {@link ActivityEmbeddingComponent} OEM implementation.
+     */
+    @NonNull
+    public ActivityEmbeddingComponent getActivityEmbeddingComponent() {
+        if (mSplitController == null) {
+            synchronized (mLock) {
+                if (mSplitController == null) {
+                    mSplitController = new SplitController();
+                }
+            }
+        }
+        return mSplitController;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java
new file mode 100644
index 0000000..f9e1f07
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import android.annotation.NonNull;
+
+/**
+ * Provides the OEM implementation of {@link WindowExtensions}.
+ */
+public class WindowExtensionsProvider {
+
+    private static final WindowExtensions sWindowExtensions = new WindowExtensionsImpl();
+
+    /**
+     * Returns the OEM implementation of {@link WindowExtensions}. This method is implemented in
+     * the library provided on the device and overwrites one in the Jetpack library included in
+     * apps.
+     * @return the OEM implementation of {@link WindowExtensions}
+     */
+    @NonNull
+    public static WindowExtensions getWindowExtensions() {
+        return sWindowExtensions;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
new file mode 100644
index 0000000..85ef270
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import android.app.Activity;
+import android.app.WindowConfiguration.WindowingMode;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.window.TaskFragmentAppearedInfo;
+import android.window.TaskFragmentCreationParams;
+import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOrganizer;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Platform default Extensions implementation of {@link TaskFragmentOrganizer} to organize
+ * task fragments.
+ *
+ * All calls into methods of this class are expected to be on the UI thread.
+ */
+class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
+
+    /** Mapping from the client assigned unique token to the {@link TaskFragmentInfo}. */
+    private final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>();
+
+    /** Mapping from the client assigned unique token to the TaskFragment {@link SurfaceControl}. */
+    private final Map<IBinder, SurfaceControl> mFragmentLeashes = new ArrayMap<>();
+
+    /**
+     * Mapping from the client assigned unique token to the TaskFragment parent
+     * {@link Configuration}.
+     */
+    final Map<IBinder, Configuration> mFragmentParentConfigs = new ArrayMap<>();
+
+    private final TaskFragmentCallback mCallback;
+    private TaskFragmentAnimationController mAnimationController;
+
+    /**
+     * Callback that notifies the controller about changes to task fragments.
+     */
+    interface TaskFragmentCallback {
+        void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo);
+        void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
+                @NonNull Configuration parentConfig);
+    }
+
+    /**
+     * @param executor  callbacks from WM Core are posted on this executor. It should be tied to the
+     *                  UI thread that all other calls into methods of this class are also on.
+     */
+    JetpackTaskFragmentOrganizer(@NonNull Executor executor, TaskFragmentCallback callback) {
+        super(executor);
+        mCallback = callback;
+    }
+
+    @Override
+    public void registerOrganizer() {
+        if (mAnimationController != null) {
+            throw new IllegalStateException("Must unregister the organizer before re-register.");
+        }
+        super.registerOrganizer();
+        mAnimationController = new TaskFragmentAnimationController(this);
+        mAnimationController.registerRemoteAnimations();
+    }
+
+    @Override
+    public void unregisterOrganizer() {
+        if (mAnimationController != null) {
+            mAnimationController.unregisterRemoteAnimations();
+            mAnimationController = null;
+        }
+        super.unregisterOrganizer();
+    }
+
+    /**
+     * Starts a new Activity and puts it into split with an existing Activity side-by-side.
+     * @param launchingFragmentToken    token for the launching TaskFragment. If it exists, it will
+     *                                  be resized based on {@param launchingFragmentBounds}.
+     *                                  Otherwise, we will create a new TaskFragment with the given
+     *                                  token for the {@param launchingActivity}.
+     * @param launchingFragmentBounds   the initial bounds for the launching TaskFragment.
+     * @param launchingActivity the Activity to put on the left hand side of the split as the
+     *                          primary.
+     * @param secondaryFragmentToken    token to create the secondary TaskFragment with.
+     * @param secondaryFragmentBounds   the initial bounds for the secondary TaskFragment
+     * @param activityIntent    Intent to start the secondary Activity with.
+     * @param activityOptions   ActivityOptions to start the secondary Activity with.
+     */
+    void startActivityToSide(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds,
+            @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken,
+            @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent,
+            @Nullable Bundle activityOptions, @NonNull SplitRule rule) {
+        final IBinder ownerToken = launchingActivity.getActivityToken();
+
+        // Create or resize the launching TaskFragment.
+        if (mFragmentInfos.containsKey(launchingFragmentToken)) {
+            resizeTaskFragment(wct, launchingFragmentToken, launchingFragmentBounds);
+        } else {
+            createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken,
+                    launchingFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, launchingActivity);
+        }
+
+        // Create a TaskFragment for the secondary activity.
+        createTaskFragmentAndStartActivity(wct, secondaryFragmentToken, ownerToken,
+                secondaryFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, activityIntent,
+                activityOptions);
+
+        // Set adjacent to each other so that the containers below will be invisible.
+        setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule);
+    }
+
+    /**
+     * Expands an existing TaskFragment to fill parent.
+     * @param wct WindowContainerTransaction in which the task fragment should be resized.
+     * @param fragmentToken token of an existing TaskFragment.
+     */
+    void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+        resizeTaskFragment(wct, fragmentToken, new Rect());
+        setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
+    }
+
+    /**
+     * Expands an existing TaskFragment to fill parent.
+     * @param fragmentToken token of an existing TaskFragment.
+     */
+    void expandTaskFragment(IBinder fragmentToken) {
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        expandTaskFragment(wct, fragmentToken);
+        applyTransaction(wct);
+    }
+
+    /**
+     * Expands an Activity to fill parent by moving it to a new TaskFragment.
+     * @param fragmentToken token to create new TaskFragment with.
+     * @param activity      activity to move to the fill-parent TaskFragment.
+     */
+    void expandActivity(IBinder fragmentToken, Activity activity) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        createTaskFragmentAndReparentActivity(
+                wct, fragmentToken, activity.getActivityToken(), new Rect(),
+                WINDOWING_MODE_UNDEFINED, activity);
+        applyTransaction(wct);
+    }
+
+    /**
+     * @param ownerToken The token of the activity that creates this task fragment. It does not
+     *                   have to be a child of this task fragment, but must belong to the same task.
+     */
+    void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
+            IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
+        final TaskFragmentCreationParams fragmentOptions =
+                createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
+        wct.createTaskFragment(fragmentOptions);
+    }
+
+    /**
+     * @param ownerToken The token of the activity that creates this task fragment. It does not
+     *                   have to be a child of this task fragment, but must belong to the same task.
+     */
+    private void createTaskFragmentAndReparentActivity(
+            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
+            @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) {
+        createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
+        wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
+    }
+
+    /**
+     * @param ownerToken The token of the activity that creates this task fragment. It does not
+     *                   have to be a child of this task fragment, but must belong to the same task.
+     */
+    private void createTaskFragmentAndStartActivity(
+            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
+            @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent,
+            @Nullable Bundle activityOptions) {
+        createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
+        wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions);
+    }
+
+    void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) {
+        WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null;
+        final boolean finishSecondaryWithPrimary =
+                splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
+        final boolean finishPrimaryWithSecondary =
+                splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
+        if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) {
+            adjacentParams = new WindowContainerTransaction.TaskFragmentAdjacentParams();
+            adjacentParams.setShouldDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary);
+            adjacentParams.setShouldDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary);
+        }
+        wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
+    }
+
+    TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken,
+            Rect bounds, @WindowingMode int windowingMode) {
+        if (mFragmentInfos.containsKey(fragmentToken)) {
+            throw new IllegalArgumentException(
+                    "There is an existing TaskFragment with fragmentToken=" + fragmentToken);
+        }
+
+        return new TaskFragmentCreationParams.Builder(
+                getOrganizerToken(),
+                fragmentToken,
+                ownerToken)
+                .setInitialBounds(bounds)
+                .setWindowingMode(windowingMode)
+                .build();
+    }
+
+    void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
+            @Nullable Rect bounds) {
+        if (!mFragmentInfos.containsKey(fragmentToken)) {
+            throw new IllegalArgumentException(
+                    "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
+        }
+        if (bounds == null) {
+            bounds = new Rect();
+        }
+        wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds);
+    }
+
+    void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+        if (!mFragmentInfos.containsKey(fragmentToken)) {
+            throw new IllegalArgumentException(
+                    "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
+        }
+        wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken());
+    }
+
+    @Override
+    public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) {
+        final TaskFragmentInfo info = taskFragmentAppearedInfo.getTaskFragmentInfo();
+        final IBinder fragmentToken = info.getFragmentToken();
+        final SurfaceControl leash = taskFragmentAppearedInfo.getLeash();
+        mFragmentInfos.put(fragmentToken, info);
+        mFragmentLeashes.put(fragmentToken, leash);
+
+        if (mCallback != null) {
+            mCallback.onTaskFragmentAppeared(taskFragmentAppearedInfo);
+        }
+    }
+
+    @Override
+    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+        final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
+        mFragmentInfos.put(fragmentToken, taskFragmentInfo);
+
+        if (mCallback != null) {
+            mCallback.onTaskFragmentInfoChanged(taskFragmentInfo);
+        }
+    }
+
+    @Override
+    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+        mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
+        mFragmentLeashes.remove(taskFragmentInfo.getFragmentToken());
+        mFragmentParentConfigs.remove(taskFragmentInfo.getFragmentToken());
+
+        if (mCallback != null) {
+            mCallback.onTaskFragmentVanished(taskFragmentInfo);
+        }
+    }
+
+    @Override
+    public void onTaskFragmentParentInfoChanged(
+            @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) {
+        mFragmentParentConfigs.put(fragmentToken, parentConfig);
+
+        if (mCallback != null) {
+            mCallback.onTaskFragmentParentInfoChanged(fragmentToken, parentConfig);
+        }
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
new file mode 100644
index 0000000..06e7d14
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+
+/**
+ * Client-side descriptor of a split that holds two containers.
+ */
+class SplitContainer {
+    private final TaskFragmentContainer mPrimaryContainer;
+    private final TaskFragmentContainer mSecondaryContainer;
+    private final SplitRule mSplitRule;
+
+    SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
+            @NonNull Activity primaryActivity,
+            @NonNull TaskFragmentContainer secondaryContainer,
+            @NonNull SplitRule splitRule) {
+        mPrimaryContainer = primaryContainer;
+        mSecondaryContainer = secondaryContainer;
+        mSplitRule = splitRule;
+
+        if (shouldFinishPrimaryWithSecondary(splitRule)) {
+            if (mPrimaryContainer.getRunningActivityCount() == 1
+                    && mPrimaryContainer.hasActivity(primaryActivity.getActivityToken())) {
+                mSecondaryContainer.addContainerToFinishOnExit(mPrimaryContainer);
+            } else {
+                // Only adding the activity to be finished vs. the entire TaskFragment while
+                // the secondary container exits because there are other unrelated activities in the
+                // primary TaskFragment.
+                mSecondaryContainer.addActivityToFinishOnExit(primaryActivity);
+            }
+        }
+        if (shouldFinishSecondaryWithPrimary(splitRule)) {
+            mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer);
+        }
+    }
+
+    @NonNull
+    TaskFragmentContainer getPrimaryContainer() {
+        return mPrimaryContainer;
+    }
+
+    @NonNull
+    TaskFragmentContainer getSecondaryContainer() {
+        return mSecondaryContainer;
+    }
+
+    @NonNull
+    SplitRule getSplitRule() {
+        return mSplitRule;
+    }
+
+    boolean isPlaceholderContainer() {
+        return (mSplitRule instanceof SplitPlaceholderRule);
+    }
+
+    static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
+        final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
+        final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule)
+                && ((SplitPairRule) splitRule).shouldFinishPrimaryWithSecondary();
+        return shouldFinishPrimaryWithSecondary || isPlaceholderContainer;
+    }
+
+    static boolean shouldFinishSecondaryWithPrimary(@NonNull SplitRule splitRule) {
+        final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
+        final boolean shouldFinishSecondaryWithPrimary = (splitRule instanceof SplitPairRule)
+                && ((SplitPairRule) splitRule).shouldFinishSecondaryWithPrimary();
+        return shouldFinishSecondaryWithPrimary || isPlaceholderContainer;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
new file mode 100644
index 0000000..42b4380
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -0,0 +1,806 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityClient;
+import android.app.ActivityOptions;
+import android.app.ActivityThread;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.window.TaskFragmentAppearedInfo;
+import android.window.TaskFragmentInfo;
+import android.window.WindowContainerTransaction;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Main controller class that manages split states and presentation.
+ */
+public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
+        ActivityEmbeddingComponent {
+
+    private final SplitPresenter mPresenter;
+
+    // Currently applied split configuration.
+    private final List<EmbeddingRule> mSplitRules = new ArrayList<>();
+    private final List<TaskFragmentContainer> mContainers = new ArrayList<>();
+    private final List<SplitContainer> mSplitContainers = new ArrayList<>();
+
+    // Callback to Jetpack to notify about changes to split states.
+    private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback;
+    private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
+
+    public SplitController() {
+        mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
+        ActivityThread activityThread = ActivityThread.currentActivityThread();
+        // Register a callback to be notified about activities being created.
+        activityThread.getApplication().registerActivityLifecycleCallbacks(
+                new LifecycleCallbacks());
+        // Intercept activity starts to route activities to new containers if necessary.
+        Instrumentation instrumentation = activityThread.getInstrumentation();
+        instrumentation.addMonitor(new ActivityStartMonitor());
+    }
+
+    /** Updates the embedding rules applied to future activity launches. */
+    @Override
+    public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) {
+        mSplitRules.clear();
+        mSplitRules.addAll(rules);
+    }
+
+    @NonNull
+    public List<EmbeddingRule> getSplitRules() {
+        return mSplitRules;
+    }
+
+    /**
+     * Starts an activity to side of the launchingActivity with the provided split config.
+     */
+    public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
+            @Nullable Bundle options, @NonNull SplitRule sideRule,
+            @NonNull Consumer<Exception> failureCallback) {
+        try {
+            mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule);
+        } catch (Exception e) {
+            failureCallback.accept(e);
+        }
+    }
+
+    /**
+     * Registers the split organizer callback to notify about changes to active splits.
+     */
+    @Override
+    public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) {
+        mEmbeddingCallback = callback;
+        updateCallbackIfNecessary();
+    }
+
+    @Override
+    public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) {
+        TaskFragmentContainer container = getContainer(
+                taskFragmentAppearedInfo.getTaskFragmentInfo().getFragmentToken());
+        if (container == null) {
+            return;
+        }
+
+        container.setInfo(taskFragmentAppearedInfo.getTaskFragmentInfo());
+        if (container.isFinished()) {
+            mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+        }
+        updateCallbackIfNecessary();
+    }
+
+    @Override
+    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+        TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
+        if (container == null) {
+            return;
+        }
+
+        container.setInfo(taskFragmentInfo);
+        // Check if there are no running activities - consider the container empty if there are no
+        // non-finishing activities left.
+        if (!taskFragmentInfo.hasRunningActivity()) {
+            // Do not finish the dependents if this TaskFragment was cleared due to launching
+            // activity in the Task.
+            final boolean shouldFinishDependent =
+                    !taskFragmentInfo.isTaskClearedForReuse();
+            mPresenter.cleanupContainer(container, shouldFinishDependent);
+        }
+        updateCallbackIfNecessary();
+    }
+
+    @Override
+    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+        TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
+        if (container == null) {
+            return;
+        }
+
+        mPresenter.cleanupContainer(container, true /* shouldFinishDependent */);
+        updateCallbackIfNecessary();
+    }
+
+    @Override
+    public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
+            @NonNull Configuration parentConfig) {
+        TaskFragmentContainer container = getContainer(fragmentToken);
+        if (container != null) {
+            mPresenter.updateContainer(container);
+            updateCallbackIfNecessary();
+        }
+    }
+
+    void onActivityCreated(@NonNull Activity launchedActivity) {
+        handleActivityCreated(launchedActivity);
+        updateCallbackIfNecessary();
+    }
+
+    /**
+     * Checks if the activity start should be routed to a particular container. It can create a new
+     * container for the activity and a new split container if necessary.
+     */
+    // TODO(b/190433398): Break down into smaller functions.
+    void handleActivityCreated(@NonNull Activity launchedActivity) {
+        final List<EmbeddingRule> splitRules = getSplitRules();
+        final TaskFragmentContainer currentContainer = getContainerWithActivity(
+                launchedActivity.getActivityToken());
+
+        // Check if the activity is configured to always be expanded.
+        if (shouldExpand(launchedActivity, null, splitRules)) {
+            if (shouldContainerBeExpanded(currentContainer)) {
+                // Make sure that the existing container is expanded
+                mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken());
+            } else {
+                // Put activity into a new expanded container
+                final TaskFragmentContainer newContainer = newContainer(launchedActivity);
+                mPresenter.expandActivity(newContainer.getTaskFragmentToken(),
+                        launchedActivity);
+            }
+            return;
+        }
+
+        // Check if activity requires a placeholder
+        if (launchPlaceholderIfNecessary(launchedActivity)) {
+            return;
+        }
+
+        // TODO(b/190433398): Check if it is a placeholder and there is already another split
+        // created by the primary activity. This is necessary for the case when the primary activity
+        // launched another secondary in the split, but the placeholder was still launched by the
+        // logic above. We didn't prevent the placeholder launcher because we didn't know that
+        // another secondary activity is coming up.
+
+        // Check if the activity should form a split with the activity below in the same task
+        // fragment.
+        Activity activityBelow = null;
+        if (currentContainer != null) {
+            final List<Activity> containerActivities = currentContainer.collectActivities();
+            final int index = containerActivities.indexOf(launchedActivity);
+            if (index > 0) {
+                activityBelow = containerActivities.get(index - 1);
+            }
+        }
+        if (activityBelow == null) {
+            IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow(
+                    launchedActivity.getActivityToken());
+            if (belowToken != null) {
+                activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken);
+            }
+        }
+        if (activityBelow == null) {
+            return;
+        }
+
+        // Check if the split is already set.
+        final TaskFragmentContainer activityBelowContainer = getContainerWithActivity(
+                activityBelow.getActivityToken());
+        if (currentContainer != null && activityBelowContainer != null) {
+            final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer,
+                    activityBelowContainer);
+            if (existingSplit != null) {
+                // There is already an active split with the activity below.
+                return;
+            }
+        }
+
+        final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity,
+                splitRules);
+        if (splitPairRule == null) {
+            return;
+        }
+
+        mPresenter.createNewSplitContainer(activityBelow, launchedActivity,
+                splitPairRule);
+    }
+
+    private void onActivityConfigurationChanged(@NonNull Activity activity) {
+        final TaskFragmentContainer currentContainer = getContainerWithActivity(
+                activity.getActivityToken());
+
+        if (currentContainer != null) {
+            // Changes to activities in controllers are handled in
+            // onTaskFragmentParentInfoChanged
+            return;
+        }
+
+        // Check if activity requires a placeholder
+        launchPlaceholderIfNecessary(activity);
+    }
+
+    /**
+     * Returns a container that this activity is registered with. An activity can only belong to one
+     * container, or no container at all.
+     */
+    @Nullable
+    TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
+        for (TaskFragmentContainer container : mContainers) {
+            if (container.hasActivity(activityToken)) {
+                return container;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates and registers a new organized container with an optional activity that will be
+     * re-parented to it in a WCT.
+     */
+    TaskFragmentContainer newContainer(@Nullable Activity activity) {
+        TaskFragmentContainer container = new TaskFragmentContainer(activity);
+        mContainers.add(container);
+        return container;
+    }
+
+    /**
+     * Creates and registers a new split with the provided containers and configuration. Finishes
+     * existing secondary containers if found for the given primary container.
+     */
+    void registerSplit(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity,
+            @NonNull TaskFragmentContainer secondaryContainer,
+            @NonNull SplitRule splitRule) {
+        if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) {
+            removeExistingSecondaryContainers(wct, primaryContainer);
+        }
+        SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity,
+                secondaryContainer, splitRule);
+        mSplitContainers.add(splitContainer);
+    }
+
+    /**
+     * Removes the container from bookkeeping records.
+     */
+    void removeContainer(@NonNull TaskFragmentContainer container) {
+        // Remove all split containers that included this one
+        mContainers.remove(container);
+        List<SplitContainer> containersToRemove = new ArrayList<>();
+        for (SplitContainer splitContainer : mSplitContainers) {
+            if (container.equals(splitContainer.getSecondaryContainer())
+                    || container.equals(splitContainer.getPrimaryContainer())) {
+                containersToRemove.add(splitContainer);
+            }
+        }
+        mSplitContainers.removeAll(containersToRemove);
+    }
+
+    /**
+     * Removes a secondary container for the given primary container if an existing split is
+     * already registered.
+     */
+    void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer primaryContainer) {
+        // If the primary container was already in a split - remove the secondary container that
+        // is now covered by the new one that replaced it.
+        final SplitContainer existingSplitContainer = getActiveSplitForContainer(
+                primaryContainer);
+        if (existingSplitContainer == null
+                || primaryContainer == existingSplitContainer.getSecondaryContainer()) {
+            return;
+        }
+
+        existingSplitContainer.getSecondaryContainer().finish(
+                false /* shouldFinishDependent */, mPresenter, wct, this);
+    }
+
+    /**
+     * Returns the topmost not finished container.
+     */
+    @Nullable
+    TaskFragmentContainer getTopActiveContainer() {
+        for (int i = mContainers.size() - 1; i >= 0; i--) {
+            TaskFragmentContainer container = mContainers.get(i);
+            if (!container.isFinished()) {
+                return container;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Updates the presentation of the container. If the container is part of the split or should
+     * have a placeholder, it will also update the other part of the split.
+     */
+    void updateContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container) {
+        if (launchPlaceholderIfNecessary(container)) {
+            // Placeholder was launched, the positions will be updated when the activity is added
+            // to the secondary container.
+            return;
+        }
+        if (shouldContainerBeExpanded(container)) {
+            if (container.getInfo() != null) {
+                mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+            }
+            // If the info is not available yet the task fragment will be expanded when it's ready
+            return;
+        }
+        SplitContainer splitContainer = getActiveSplitForContainer(container);
+        if (splitContainer == null) {
+            return;
+        }
+        if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) {
+            // Skip position update - it isn't the topmost split.
+            return;
+        }
+        if (splitContainer.getPrimaryContainer().isEmpty()
+                || splitContainer.getSecondaryContainer().isEmpty()) {
+            // Skip position update - one or both containers are empty.
+            return;
+        }
+        if (dismissPlaceholderIfNecessary(splitContainer)) {
+            // Placeholder was finished, the positions will be updated when its container is emptied
+            return;
+        }
+        mPresenter.updateSplitContainer(splitContainer, container, wct);
+    }
+
+    /**
+     * Returns the top active split container that has the provided container, if available.
+     */
+    @Nullable
+    private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) {
+        for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+            SplitContainer splitContainer = mSplitContainers.get(i);
+            if (container.equals(splitContainer.getSecondaryContainer())
+                    || container.equals(splitContainer.getPrimaryContainer())) {
+                return splitContainer;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the active split that has the provided containers as primary and secondary or as
+     * secondary and primary, if available.
+     */
+    @Nullable
+    private SplitContainer getActiveSplitForContainers(
+            @NonNull TaskFragmentContainer firstContainer,
+            @NonNull TaskFragmentContainer secondContainer) {
+        for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+            SplitContainer splitContainer = mSplitContainers.get(i);
+            final TaskFragmentContainer primary = splitContainer.getPrimaryContainer();
+            final TaskFragmentContainer secondary = splitContainer.getSecondaryContainer();
+            if ((firstContainer == secondary && secondContainer == primary)
+                    || (firstContainer == primary && secondContainer == secondary)) {
+                return splitContainer;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Checks if the container requires a placeholder and launches it if necessary.
+     */
+    private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) {
+        final Activity topActivity = container.getTopNonFinishingActivity();
+        if (topActivity == null) {
+            return false;
+        }
+
+        return launchPlaceholderIfNecessary(topActivity);
+    }
+
+    boolean launchPlaceholderIfNecessary(@NonNull Activity activity) {
+        final  TaskFragmentContainer container = getContainerWithActivity(
+                activity.getActivityToken());
+
+        SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container)
+                : null;
+        if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
+            // Don't launch placeholder in primary split container
+            return false;
+        }
+
+        // Check if there is enough space for launch
+        final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity);
+        if (placeholderRule == null || !mPresenter.shouldShowSideBySide(
+                mPresenter.getParentContainerBounds(activity), placeholderRule)) {
+            return false;
+        }
+
+        // TODO(b/190433398): Handle failed request
+        startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null,
+                placeholderRule, null);
+        return true;
+    }
+
+    private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) {
+        if (!splitContainer.isPlaceholderContainer()) {
+            return false;
+        }
+
+        if (mPresenter.shouldShowSideBySide(splitContainer)) {
+            return false;
+        }
+
+        mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(),
+                false /* shouldFinishDependent */);
+        return true;
+    }
+
+    /**
+     * Returns the rule to launch a placeholder for the activity with the provided component name
+     * if it is configured in the split config.
+     */
+    private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) {
+        for (EmbeddingRule rule : mSplitRules) {
+            if (!(rule instanceof SplitPlaceholderRule)) {
+                continue;
+            }
+            SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) rule;
+            if (placeholderRule.matchesActivity(activity)) {
+                return placeholderRule;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Notifies listeners about changes to split states if necessary.
+     */
+    private void updateCallbackIfNecessary() {
+        if (mEmbeddingCallback == null) {
+            return;
+        }
+        if (!allActivitiesCreated()) {
+            return;
+        }
+        List<SplitInfo> currentSplitStates = getActiveSplitStates();
+        if (mLastReportedSplitStates.equals(currentSplitStates)) {
+            return;
+        }
+        mLastReportedSplitStates.clear();
+        mLastReportedSplitStates.addAll(currentSplitStates);
+        mEmbeddingCallback.accept(currentSplitStates);
+    }
+
+    /**
+     * Returns a list of descriptors for currently active split states.
+     */
+    private List<SplitInfo> getActiveSplitStates() {
+        List<SplitInfo> splitStates = new ArrayList<>();
+        for (SplitContainer container : mSplitContainers) {
+            if (container.getPrimaryContainer().isEmpty()
+                    || container.getSecondaryContainer().isEmpty()) {
+                // Skipping containers that do not have any activities to report.
+                continue;
+            }
+            ActivityStack primaryContainer =
+                    new ActivityStack(
+                            container.getPrimaryContainer().collectActivities());
+            ActivityStack secondaryContainer =
+                    new ActivityStack(
+                            container.getSecondaryContainer().collectActivities());
+            SplitInfo splitState = new SplitInfo(primaryContainer,
+                    secondaryContainer,
+                    // Splits that are not showing side-by-side are reported as having 0 split
+                    // ratio, since by definition in the API the primary container occupies no
+                    // width of the split when covered by the secondary.
+                    mPresenter.shouldShowSideBySide(container)
+                            ? container.getSplitRule().getSplitRatio()
+                            : 0.0f);
+            splitStates.add(splitState);
+        }
+        return splitStates;
+    }
+
+    /**
+     * Checks if all activities that are registered with the containers have already appeared in
+     * the client.
+     */
+    private boolean allActivitiesCreated() {
+        for (TaskFragmentContainer container : mContainers) {
+            if (container.getInfo() == null
+                    || container.getInfo().getActivities().size()
+                    != container.collectActivities().size()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns {@code true} if the container is expanded to occupy full task size.
+     * Returns {@code false} if the container is included in an active split.
+     */
+    boolean shouldContainerBeExpanded(@Nullable TaskFragmentContainer container) {
+        if (container == null) {
+            return false;
+        }
+        for (SplitContainer splitContainer : mSplitContainers) {
+            if (container.equals(splitContainer.getPrimaryContainer())
+                    || container.equals(splitContainer.getSecondaryContainer())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns a split rule for the provided pair of primary activity and secondary activity intent
+     * if available.
+     */
+    @Nullable
+    private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) {
+        for (EmbeddingRule rule : splitRules) {
+            if (!(rule instanceof SplitPairRule)) {
+                continue;
+            }
+            SplitPairRule pairRule = (SplitPairRule) rule;
+            if (pairRule.matchesActivityIntentPair(primaryActivity, secondaryActivityIntent)) {
+                return pairRule;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns a split rule for the provided pair of primary and secondary activities if available.
+     */
+    @Nullable
+    private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) {
+        for (EmbeddingRule rule : splitRules) {
+            if (!(rule instanceof SplitPairRule)) {
+                continue;
+            }
+            SplitPairRule pairRule = (SplitPairRule) rule;
+            final Intent intent = secondaryActivity.getIntent();
+            if (pairRule.matchesActivityPair(primaryActivity, secondaryActivity)
+                    && (intent == null
+                    || pairRule.matchesActivityIntentPair(primaryActivity, intent))) {
+                return pairRule;
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) {
+        for (TaskFragmentContainer container : mContainers) {
+            if (container.getTaskFragmentToken().equals(fragmentToken)) {
+                return container;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns {@code true} if an Activity with the provided component name should always be
+     * expanded to occupy full task bounds. Such activity must not be put in a split.
+     */
+    private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent,
+            List<EmbeddingRule> splitRules) {
+        if (splitRules == null) {
+            return false;
+        }
+        for (EmbeddingRule rule : splitRules) {
+            if (!(rule instanceof ActivityRule)) {
+                continue;
+            }
+            ActivityRule activityRule = (ActivityRule) rule;
+            if (!activityRule.shouldAlwaysExpand()) {
+                continue;
+            }
+            if (activity != null && activityRule.matchesActivity(activity)) {
+                return true;
+            } else if (intent != null && activityRule.matchesIntent(intent)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private final class LifecycleCallbacks implements ActivityLifecycleCallbacks {
+
+        @Override
+        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+        }
+
+        @Override
+        public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
+            // Calling after Activity#onCreate is complete to allow the app launch something
+            // first. In case of a configured placeholder activity we want to make sure
+            // that we don't launch it if an activity itself already requested something to be
+            // launched to side.
+            SplitController.this.onActivityCreated(activity);
+        }
+
+        @Override
+        public void onActivityStarted(Activity activity) {
+        }
+
+        @Override
+        public void onActivityResumed(Activity activity) {
+        }
+
+        @Override
+        public void onActivityPaused(Activity activity) {
+        }
+
+        @Override
+        public void onActivityStopped(Activity activity) {
+        }
+
+        @Override
+        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+        }
+
+        @Override
+        public void onActivityDestroyed(Activity activity) {
+        }
+
+        @Override
+        public void onActivityConfigurationChanged(Activity activity) {
+            SplitController.this.onActivityConfigurationChanged(activity);
+        }
+    }
+
+    /** Executor that posts on the main application thread. */
+    private static class MainThreadExecutor implements Executor {
+        private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+        @Override
+        public void execute(Runnable r) {
+            mHandler.post(r);
+        }
+    }
+
+    /**
+     * A monitor that intercepts all activity start requests originating in the client process and
+     * can amend them to target a specific task fragment to form a split.
+     */
+    private class ActivityStartMonitor extends Instrumentation.ActivityMonitor {
+
+        @Override
+        public Instrumentation.ActivityResult onStartActivity(@NonNull Context who,
+                @NonNull Intent intent, @NonNull Bundle options) {
+            // TODO(b/190433398): Check if the activity is configured to always be expanded.
+
+            // Check if activity should be put in a split with the activity that launched it.
+            if (!(who instanceof Activity)) {
+                return super.onStartActivity(who, intent, options);
+            }
+            final Activity launchingActivity = (Activity) who;
+
+            if (shouldExpand(null, intent, getSplitRules())) {
+                setLaunchingInExpandedContainer(launchingActivity, options);
+            } else if (!setLaunchingToSideContainer(launchingActivity, intent, options)) {
+                setLaunchingInSameContainer(launchingActivity, intent, options);
+            }
+
+            return super.onStartActivity(who, intent, options);
+        }
+
+        private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) {
+            TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer(
+                    launchingActivity);
+
+            // Amend the request to let the WM know that the activity should be placed in the
+            // dedicated container.
+            options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+                    newContainer.getTaskFragmentToken());
+        }
+
+        /**
+         * Returns {@code true} if the activity that is going to be started via the
+         * {@code intent} should be paired with the {@code launchingActivity} and is set to be
+         * launched in an empty side container.
+         */
+        private boolean setLaunchingToSideContainer(Activity launchingActivity, Intent intent,
+                Bundle options) {
+            final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent,
+                    getSplitRules());
+            if (splitPairRule == null) {
+                return false;
+            }
+
+            // Create a new split with an empty side container
+            final TaskFragmentContainer secondaryContainer = mPresenter
+                    .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule);
+
+            // Amend the request to let the WM know that the activity should be placed in the
+            // dedicated container.
+            options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+                    secondaryContainer.getTaskFragmentToken());
+            return true;
+        }
+
+        /**
+         * Checks if the activity that is going to be started via the {@code intent} should be
+         * paired with the existing top activity which is currently paired with the
+         * {@code launchingActivity}. If so, set the activity to be launched in the same
+         * container of the {@code launchingActivity}.
+         */
+        private void setLaunchingInSameContainer(Activity launchingActivity, Intent intent,
+                Bundle options) {
+            final TaskFragmentContainer launchingContainer = getContainerWithActivity(
+                    launchingActivity.getActivityToken());
+            if (launchingContainer == null) {
+                return;
+            }
+
+            final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer);
+            if (splitContainer == null) {
+                return;
+            }
+
+            if (splitContainer.getSecondaryContainer() != launchingContainer) {
+                return;
+            }
+
+            // The launching activity is on the secondary container. Retrieve the primary
+            // activity from the other container.
+            Activity primaryActivity =
+                    splitContainer.getPrimaryContainer().getTopNonFinishingActivity();
+            if (primaryActivity == null) {
+                return;
+            }
+
+            final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent,
+                    getSplitRules());
+            if (splitPairRule == null) {
+                return;
+            }
+
+            // Amend the request to let the WM know that the activity should be placed in the
+            // dedicated container. This is necessary for the case that the activity is started
+            // into a new Task, or new Task will be escaped from the current host Task and be
+            // displayed in fullscreen.
+            options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+                    launchingContainer.getTaskFragmentToken());
+        }
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
new file mode 100644
index 0000000..81be21c
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.LayoutDirection;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowMetrics;
+import android.window.TaskFragmentCreationParams;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controls the visual presentation of the splits according to the containers formed by
+ * {@link SplitController}.
+ */
+class SplitPresenter extends JetpackTaskFragmentOrganizer {
+    private static final int POSITION_START = 0;
+    private static final int POSITION_END = 1;
+    private static final int POSITION_FILL = 2;
+
+    @IntDef(value = {
+            POSITION_START,
+            POSITION_END,
+            POSITION_FILL,
+    })
+    private @interface Position {}
+
+    private final SplitController mController;
+
+    SplitPresenter(@NonNull Executor executor, SplitController controller) {
+        super(executor, controller);
+        mController = controller;
+        registerOrganizer();
+    }
+
+    /**
+     * Updates the presentation of the provided container.
+     */
+    void updateContainer(TaskFragmentContainer container) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mController.updateContainer(wct, container);
+        applyTransaction(wct);
+    }
+
+    /**
+     * Deletes the specified container and all other associated and dependent containers in the same
+     * transaction.
+     */
+    void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+        container.finish(shouldFinishDependent, this, wct, mController);
+
+        final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer();
+        if (newTopContainer != null) {
+            mController.updateContainer(wct, newTopContainer);
+        }
+
+        applyTransaction(wct);
+    }
+
+    /**
+     * Creates a new split with the primary activity and an empty secondary container.
+     * @return The newly created secondary container.
+     */
+    TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity,
+            @NonNull SplitPairRule rule) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+        final Rect parentBounds = getParentContainerBounds(primaryActivity);
+        final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+                isLtr(primaryActivity, rule));
+        final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
+                primaryActivity, primaryRectBounds, null);
+
+        // Create new empty task fragment
+        final TaskFragmentContainer secondaryContainer = mController.newContainer(null);
+        final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds,
+                rule, isLtr(primaryActivity, rule));
+        createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
+                primaryActivity.getActivityToken(), secondaryRectBounds,
+                WINDOWING_MODE_MULTI_WINDOW);
+        secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
+
+        // Set adjacent to each other so that the containers below will be invisible.
+        setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+                secondaryContainer.getTaskFragmentToken(), rule);
+
+        mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
+
+        applyTransaction(wct);
+
+        return secondaryContainer;
+    }
+
+    /**
+     * Creates a new split container with the two provided activities.
+     * @param primaryActivity An activity that should be in the primary container. If it is not
+     *                        currently in an existing container, a new one will be created and the
+     *                        activity will be re-parented to it.
+     * @param secondaryActivity An activity that should be in the secondary container. If it is not
+     *                          currently in an existing container, or if it is currently in the
+     *                          same container as the primary activity, a new container will be
+     *                          created and the activity will be re-parented to it.
+     * @param rule The split rule to be applied to the container.
+     */
+    void createNewSplitContainer(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+        final Rect parentBounds = getParentContainerBounds(primaryActivity);
+        final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+                isLtr(primaryActivity, rule));
+        final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
+                primaryActivity, primaryRectBounds, null);
+
+        final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
+                isLtr(primaryActivity, rule));
+        final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
+                secondaryActivity, secondaryRectBounds, primaryContainer);
+
+        // Set adjacent to each other so that the containers below will be invisible.
+        setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+                secondaryContainer.getTaskFragmentToken(), rule);
+
+        mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
+
+        applyTransaction(wct);
+    }
+
+    /**
+     * Creates a new expanded container.
+     */
+    TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) {
+        final TaskFragmentContainer newContainer = mController.newContainer(null);
+
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        createTaskFragment(wct, newContainer.getTaskFragmentToken(),
+                launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_MULTI_WINDOW);
+
+        applyTransaction(wct);
+        return newContainer;
+    }
+
+    /**
+     * Creates a new container or resizes an existing container for activity to the provided bounds.
+     * @param activity The activity to be re-parented to the container if necessary.
+     * @param containerToAvoid Re-parent from this container if an activity is already in it.
+     */
+    private TaskFragmentContainer prepareContainerForActivity(
+            @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
+            @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
+        TaskFragmentContainer container = mController.getContainerWithActivity(
+                activity.getActivityToken());
+        if (container == null || container == containerToAvoid) {
+            container = mController.newContainer(activity);
+
+            final TaskFragmentCreationParams fragmentOptions =
+                    createFragmentOptions(
+                            container.getTaskFragmentToken(),
+                            activity.getActivityToken(),
+                            bounds,
+                            WINDOWING_MODE_MULTI_WINDOW);
+            wct.createTaskFragment(fragmentOptions);
+
+            wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
+                    activity.getActivityToken());
+
+            container.setLastRequestedBounds(bounds);
+        } else {
+            resizeTaskFragmentIfRegistered(wct, container, bounds);
+        }
+
+        return container;
+    }
+
+    /**
+     * Starts a new activity to the side, creating a new split container. A new container will be
+     * created for the activity that will be started.
+     * @param launchingActivity An activity that should be in the primary container. If it is not
+     *                          currently in an existing container, a new one will be created and
+     *                          the activity will be re-parented to it.
+     * @param activityIntent The intent to start the new activity.
+     * @param activityOptions The options to apply to new activity start.
+     * @param rule The split rule to be applied to the container.
+     */
+    void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
+            @Nullable Bundle activityOptions, @NonNull SplitRule rule) {
+        final Rect parentBounds = getParentContainerBounds(launchingActivity);
+        final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+                isLtr(launchingActivity, rule));
+        final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
+                isLtr(launchingActivity, rule));
+
+        TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
+                launchingActivity.getActivityToken());
+        if (primaryContainer == null) {
+            primaryContainer = mController.newContainer(launchingActivity);
+        }
+
+        TaskFragmentContainer secondaryContainer = mController.newContainer(null);
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
+                rule);
+        startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
+                launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds,
+                activityIntent, activityOptions, rule);
+        applyTransaction(wct);
+
+        primaryContainer.setLastRequestedBounds(primaryRectBounds);
+        secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
+    }
+
+    /**
+     * Updates the positions of containers in an existing split.
+     * @param splitContainer The split container to be updated.
+     * @param updatedContainer The task fragment that was updated and caused this split update.
+     * @param wct WindowContainerTransaction that this update should be performed with.
+     */
+    void updateSplitContainer(@NonNull SplitContainer splitContainer,
+            @NonNull TaskFragmentContainer updatedContainer,
+            @NonNull WindowContainerTransaction wct) {
+        // Getting the parent bounds using the updated container - it will have the recent value.
+        final Rect parentBounds = getParentContainerBounds(updatedContainer);
+        final SplitRule rule = splitContainer.getSplitRule();
+        final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer();
+        final Activity activity = primaryContainer.getTopNonFinishingActivity();
+        if (activity == null) {
+            return;
+        }
+        final boolean isLtr = isLtr(activity, rule);
+        final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
+                isLtr);
+        final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
+                isLtr);
+
+        // If the task fragments are not registered yet, the positions will be updated after they
+        // are created again.
+        resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
+        final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
+        resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
+
+        setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+                secondaryContainer.getTaskFragmentToken(), rule);
+    }
+
+    /**
+     * Resizes the task fragment if it was already registered. Skips the operation if the container
+     * creation has not been reported from the server yet.
+     */
+    // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
+    void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container,
+            @Nullable Rect bounds) {
+        if (container.getInfo() == null) {
+            return;
+        }
+        resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds);
+    }
+
+    @Override
+    void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+            @Nullable Rect bounds) {
+        TaskFragmentContainer container = mController.getContainer(fragmentToken);
+        if (container == null) {
+            throw new IllegalStateException(
+                    "Resizing a task fragment that is not registered with controller.");
+        }
+
+        if (container.areLastRequestedBoundsEqual(bounds)) {
+            // Return early if the provided bounds were already requested
+            return;
+        }
+
+        container.setLastRequestedBounds(bounds);
+        super.resizeTaskFragment(wct, fragmentToken, bounds);
+    }
+
+    boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
+        final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
+        return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule());
+    }
+
+    boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) {
+        // TODO(b/190433398): Supply correct insets.
+        final WindowMetrics parentMetrics = new WindowMetrics(parentBounds,
+                new WindowInsets(new Rect()));
+        return rule.checkParentMetrics(parentMetrics);
+    }
+
+    @NonNull
+    private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
+            @NonNull SplitRule rule, boolean isLtr) {
+        if (!shouldShowSideBySide(parentBounds, rule)) {
+            return new Rect();
+        }
+
+        final float splitRatio = rule.getSplitRatio();
+        final float rtlSplitRatio = 1 - splitRatio;
+        switch (position) {
+            case POSITION_START:
+                return isLtr ? getLeftContainerBounds(parentBounds, splitRatio)
+                        : getRightContainerBounds(parentBounds, rtlSplitRatio);
+            case POSITION_END:
+                return isLtr ? getRightContainerBounds(parentBounds, splitRatio)
+                        : getLeftContainerBounds(parentBounds, rtlSplitRatio);
+            case POSITION_FILL:
+                return parentBounds;
+        }
+        return parentBounds;
+    }
+
+    private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
+        return new Rect(
+                parentBounds.left,
+                parentBounds.top,
+                (int) (parentBounds.left + parentBounds.width() * splitRatio),
+                parentBounds.bottom);
+    }
+
+    private Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
+        return new Rect(
+                (int) (parentBounds.left + parentBounds.width() * splitRatio),
+                parentBounds.top,
+                parentBounds.right,
+                parentBounds.bottom);
+    }
+
+    /**
+     * Checks if a split with the provided rule should be displays in left-to-right layout
+     * direction, either always or with the current configuration.
+     */
+    private boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) {
+        switch (rule.getLayoutDirection()) {
+            case LayoutDirection.LOCALE:
+                return context.getResources().getConfiguration().getLayoutDirection()
+                        == View.LAYOUT_DIRECTION_LTR;
+            case LayoutDirection.RTL:
+                return false;
+            case LayoutDirection.LTR:
+            default:
+                return true;
+        }
+    }
+
+    @NonNull
+    Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
+        final Configuration parentConfig = mFragmentParentConfigs.get(
+                container.getTaskFragmentToken());
+        if (parentConfig != null) {
+            return parentConfig.windowConfiguration.getBounds();
+        }
+
+        // If there is no parent yet - then assuming that activities are running in full task bounds
+        final Activity topActivity = container.getTopNonFinishingActivity();
+        final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null;
+
+        if (bounds == null) {
+            throw new IllegalStateException("Unknown parent bounds");
+        }
+        return bounds;
+    }
+
+    @NonNull
+    Rect getParentContainerBounds(@NonNull Activity activity) {
+        final TaskFragmentContainer container = mController.getContainerWithActivity(
+                activity.getActivityToken());
+        if (container != null) {
+            final Configuration parentConfig = mFragmentParentConfigs.get(
+                    container.getTaskFragmentToken());
+            if (parentConfig != null) {
+                return parentConfig.windowConfiguration.getBounds();
+            }
+        }
+
+        // TODO(b/190433398): Check if the client-side available info about parent bounds is enough.
+        if (!activity.isInMultiWindowMode()) {
+            // In fullscreen mode the max bounds should correspond to the task bounds.
+            return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds();
+        }
+        return activity.getResources().getConfiguration().windowConfiguration.getBounds();
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
new file mode 100644
index 0000000..194b633
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.graphics.Matrix.MSCALE_X;
+
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Wrapper to handle the TaskFragment animation update in one {@link SurfaceControl.Transaction}.
+ *
+ * The base adapter can be used for {@link RemoteAnimationTarget} that is simple open/close.
+ */
+class TaskFragmentAnimationAdapter {
+    final Animation mAnimation;
+    final RemoteAnimationTarget mTarget;
+    final SurfaceControl mLeash;
+
+    final Transformation mTransformation = new Transformation();
+    final float[] mMatrix = new float[9];
+    private boolean mIsFirstFrame = true;
+
+    TaskFragmentAnimationAdapter(@NonNull Animation animation,
+            @NonNull RemoteAnimationTarget target) {
+        this(animation, target, target.leash);
+    }
+
+    /**
+     * @param leash the surface to animate.
+     */
+    TaskFragmentAnimationAdapter(@NonNull Animation animation,
+            @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash) {
+        mAnimation = animation;
+        mTarget = target;
+        mLeash = leash;
+    }
+
+    /** Called on frame update. */
+    final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
+        if (mIsFirstFrame) {
+            t.show(mLeash);
+            mIsFirstFrame = false;
+        }
+
+        // Extract the transformation to the current time.
+        mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
+                mTransformation);
+        t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+        onAnimationUpdateInner(t);
+    }
+
+    /** To be overridden by subclasses to adjust the animation surface change. */
+    void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+        mTransformation.getMatrix().postTranslate(
+                mTarget.localBounds.left, mTarget.localBounds.top);
+        t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+        t.setAlpha(mLeash, mTransformation.getAlpha());
+    }
+
+    /** Called after animation finished. */
+    final void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+        onAnimationUpdate(t, mAnimation.getDuration());
+    }
+
+    final long getDurationHint() {
+        return mAnimation.computeDurationHint();
+    }
+
+    /**
+     * Should be used when the {@link RemoteAnimationTarget} is in split with others, and want to
+     * animate together as one. This adapter will offset the animation leash to make the animate of
+     * two windows look like a single window.
+     */
+    static class SplitAdapter extends TaskFragmentAnimationAdapter {
+        private final boolean mIsLeftHalf;
+        private final int mWholeAnimationWidth;
+
+        /**
+         * @param isLeftHalf whether this is the left half of the animation.
+         * @param wholeAnimationWidth the whole animation windows width.
+         */
+        SplitAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target,
+                boolean isLeftHalf, int wholeAnimationWidth) {
+            super(animation, target);
+            mIsLeftHalf = isLeftHalf;
+            mWholeAnimationWidth = wholeAnimationWidth;
+            if (wholeAnimationWidth == 0) {
+                throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth");
+            }
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            float posX = mTarget.localBounds.left;
+            final float posY = mTarget.localBounds.top;
+            // This window is half of the whole animation window. Offset left/right to make it
+            // look as one with the other half.
+            mTransformation.getMatrix().getValues(mMatrix);
+            final int targetWidth = mTarget.localBounds.width();
+            final float scaleX = mMatrix[MSCALE_X];
+            final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2;
+            final float curOffset = targetWidth * (1 - scaleX) / 2;
+            final float offsetDiff = totalOffset - curOffset;
+            if (mIsLeftHalf) {
+                posX += offsetDiff;
+            } else {
+                posX -= offsetDiff;
+            }
+            mTransformation.getMatrix().postTranslate(posX, posY);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+        }
+    }
+
+    /**
+     * Should be used for the animation of the snapshot of a {@link RemoteAnimationTarget} that has
+     * size change.
+     */
+    static class SnapshotAdapter extends TaskFragmentAnimationAdapter {
+
+        SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) {
+            // Start leash is the snapshot of the starting surface.
+            super(animation, target, target.startLeash);
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            // Snapshot should always be placed at the top left of the animation leash.
+            mTransformation.getMatrix().postTranslate(0, 0);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+        }
+    }
+
+    /**
+     * Should be used for the animation of the {@link RemoteAnimationTarget} that has size change.
+     */
+    static class BoundsChangeAdapter extends TaskFragmentAnimationAdapter {
+        private final float[] mVecs = new float[4];
+        private final Rect mRect = new Rect();
+
+        BoundsChangeAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) {
+            super(animation, target);
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            mTransformation.getMatrix().postTranslate(
+                    mTarget.localBounds.left, mTarget.localBounds.top);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+
+            // The following applies an inverse scale to the clip-rect so that it crops "after" the
+            // scale instead of before.
+            mVecs[1] = mVecs[2] = 0;
+            mVecs[0] = mVecs[3] = 1;
+            mTransformation.getMatrix().mapVectors(mVecs);
+            mVecs[0] = 1.f / mVecs[0];
+            mVecs[3] = 1.f / mVecs[3];
+            final Rect clipRect = mTransformation.getClipRect();
+            mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
+            mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
+            mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
+            mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
+            t.setWindowCrop(mLeash, mRect);
+        }
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
new file mode 100644
index 0000000..535dac1
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
+
+import android.util.Log;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationDefinition;
+import android.window.TaskFragmentOrganizer;
+
+/** Controls the TaskFragment remote animations. */
+class TaskFragmentAnimationController {
+
+    private static final String TAG = "TaskFragAnimationCtrl";
+    static final boolean DEBUG = false;
+
+    private final TaskFragmentOrganizer mOrganizer;
+    private final TaskFragmentAnimationRunner mRemoteRunner = new TaskFragmentAnimationRunner();
+
+    TaskFragmentAnimationController(TaskFragmentOrganizer organizer) {
+        mOrganizer = organizer;
+    }
+
+    void registerRemoteAnimations() {
+        if (DEBUG) {
+            Log.v(TAG, "registerRemoteAnimations");
+        }
+        final RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
+        final RemoteAnimationAdapter animationAdapter =
+                new RemoteAnimationAdapter(mRemoteRunner, 0, 0, true /* changeNeedsSnapshot */);
+        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, animationAdapter);
+        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, animationAdapter);
+        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, animationAdapter);
+        mOrganizer.registerRemoteAnimations(definition);
+    }
+
+    void unregisterRemoteAnimations() {
+        if (DEBUG) {
+            Log.v(TAG, "unregisterRemoteAnimations");
+        }
+        mOrganizer.unregisterRemoteAnimations();
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
new file mode 100644
index 0000000..412559e
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+/** To run the TaskFragment animations. */
+class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
+
+    private static final String TAG = "TaskFragAnimationRunner";
+    private final Handler mHandler = new Handler(Looper.myLooper());
+    private final TaskFragmentAnimationSpec mAnimationSpec;
+
+    TaskFragmentAnimationRunner() {
+        mAnimationSpec = new TaskFragmentAnimationSpec(mHandler);
+    }
+
+    @Nullable
+    private Animator mAnimator;
+
+    @Override
+    public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+            @NonNull RemoteAnimationTarget[] apps,
+            @NonNull RemoteAnimationTarget[] wallpapers,
+            @NonNull RemoteAnimationTarget[] nonApps,
+            @NonNull IRemoteAnimationFinishedCallback finishedCallback) {
+        if (wallpapers.length != 0 || nonApps.length != 0) {
+            throw new IllegalArgumentException("TaskFragment shouldn't handle animation with"
+                    + "wallpaper or non-app windows.");
+        }
+        if (TaskFragmentAnimationController.DEBUG) {
+            Log.v(TAG, "onAnimationStart transit=" + transit);
+        }
+        mHandler.post(() -> startAnimation(transit, apps, finishedCallback));
+    }
+
+    @Override
+    public void onAnimationCancelled() {
+        if (TaskFragmentAnimationController.DEBUG) {
+            Log.v(TAG, "onAnimationCancelled");
+        }
+        mHandler.post(this::cancelAnimation);
+    }
+
+    /** Creates and starts animation. */
+    private void startAnimation(@WindowManager.TransitionOldType int transit,
+            @NonNull RemoteAnimationTarget[] targets,
+            @NonNull IRemoteAnimationFinishedCallback finishedCallback) {
+        if (mAnimator != null) {
+            Log.w(TAG, "start new animation when the previous one is not finished yet.");
+            mAnimator.cancel();
+        }
+        mAnimator = createAnimator(transit, targets, finishedCallback);
+        mAnimator.start();
+    }
+
+    /** Cancels animation. */
+    private void cancelAnimation() {
+        if (mAnimator == null) {
+            return;
+        }
+        mAnimator.cancel();
+        mAnimator = null;
+    }
+
+    /** Creates the animator given the transition type and windows. */
+    private Animator createAnimator(@WindowManager.TransitionOldType int transit,
+            @NonNull RemoteAnimationTarget[] targets,
+            @NonNull IRemoteAnimationFinishedCallback finishedCallback) {
+        final List<TaskFragmentAnimationAdapter> adapters =
+                createAnimationAdapters(transit, targets);
+        long duration = 0;
+        for (TaskFragmentAnimationAdapter adapter : adapters) {
+            duration = Math.max(duration, adapter.getDurationHint());
+        }
+        final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+        animator.setDuration(duration);
+        animator.addUpdateListener((anim) -> {
+            // Update all adapters in the same transaction.
+            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            for (TaskFragmentAnimationAdapter adapter : adapters) {
+                adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
+            }
+            t.apply();
+        });
+        animator.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                for (TaskFragmentAnimationAdapter adapter : adapters) {
+                    adapter.onAnimationEnd(t);
+                }
+                t.apply();
+
+                try {
+                    finishedCallback.onAnimationFinished();
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
+                mAnimator = null;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        });
+        return animator;
+    }
+
+    /** List of {@link TaskFragmentAnimationAdapter} to handle animations on all window targets. */
+    private List<TaskFragmentAnimationAdapter> createAnimationAdapters(
+            @WindowManager.TransitionOldType int transit,
+            @NonNull RemoteAnimationTarget[] targets) {
+        switch (transit) {
+            case TRANSIT_OLD_TASK_FRAGMENT_OPEN:
+                return createOpenAnimationAdapters(targets);
+            case TRANSIT_OLD_TASK_FRAGMENT_CLOSE:
+                return createCloseAnimationAdapters(targets);
+            case TRANSIT_OLD_TASK_FRAGMENT_CHANGE:
+                return createChangeAnimationAdapters(targets);
+            default:
+                throw new IllegalArgumentException("Unhandled transit type=" + transit);
+        }
+    }
+
+    private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters(
+            @NonNull RemoteAnimationTarget[] targets) {
+        return createOpenCloseAnimationAdapters(targets,
+                mAnimationSpec::loadOpenAnimation);
+    }
+
+    private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters(
+            @NonNull RemoteAnimationTarget[] targets) {
+        return createOpenCloseAnimationAdapters(targets,
+                mAnimationSpec::loadCloseAnimation);
+    }
+
+    private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters(
+            @NonNull RemoteAnimationTarget[] targets,
+            @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) {
+        // We need to know if the target window is only a partial of the whole animation screen.
+        // If so, we will need to adjust it to make the whole animation screen looks like one.
+        final List<RemoteAnimationTarget> openingTargets = new ArrayList<>();
+        final List<RemoteAnimationTarget> closingTargets = new ArrayList<>();
+        final Rect openingWholeScreenBounds = new Rect();
+        final Rect closingWholeScreenBounds = new Rect();
+        for (RemoteAnimationTarget target : targets) {
+            if (target.mode != MODE_CLOSING) {
+                openingTargets.add(target);
+                openingWholeScreenBounds.union(target.localBounds);
+            } else {
+                closingTargets.add(target);
+                closingWholeScreenBounds.union(target.localBounds);
+            }
+        }
+
+        final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
+        for (RemoteAnimationTarget target : openingTargets) {
+            adapters.add(createOpenCloseAnimationAdapter(target, animationProvider,
+                    openingWholeScreenBounds));
+        }
+        for (RemoteAnimationTarget target : closingTargets) {
+            adapters.add(createOpenCloseAnimationAdapter(target, animationProvider,
+                    closingWholeScreenBounds));
+        }
+        return adapters;
+    }
+
+    private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter(
+            @NonNull RemoteAnimationTarget target,
+            @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider,
+            @NonNull Rect wholeAnimationBounds) {
+        final Animation animation = animationProvider.apply(target, wholeAnimationBounds);
+        final Rect targetBounds = target.localBounds;
+        if (targetBounds.left == wholeAnimationBounds.left
+                && targetBounds.right != wholeAnimationBounds.right) {
+            // This is the left split of the whole animation window.
+            return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target,
+                    true /* isLeftHalf */, wholeAnimationBounds.width());
+        } else if (targetBounds.left != wholeAnimationBounds.left
+                && targetBounds.right == wholeAnimationBounds.right) {
+            // This is the right split of the whole animation window.
+            return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target,
+                    false /* isLeftHalf */, wholeAnimationBounds.width());
+        }
+        // Open/close window that fills the whole animation.
+        return new TaskFragmentAnimationAdapter(animation, target);
+    }
+
+    private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters(
+            @NonNull RemoteAnimationTarget[] targets) {
+        final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
+        for (RemoteAnimationTarget target : targets) {
+            if (target.startBounds != null) {
+                // This is the target with bounds change.
+                final Animation[] animations =
+                        mAnimationSpec.createChangeBoundsChangeAnimations(target);
+                // Adapter for the starting snapshot leash.
+                adapters.add(new TaskFragmentAnimationAdapter.SnapshotAdapter(
+                        animations[0], target));
+                // Adapter for the ending bounds changed leash.
+                adapters.add(new TaskFragmentAnimationAdapter.BoundsChangeAdapter(
+                        animations[1], target));
+                continue;
+            }
+
+            // These are the other targets that don't have bounds change in the same transition.
+            final Animation animation;
+            if (target.hasAnimatingParent) {
+                // No-op if it will be covered by the changing parent window.
+                animation = TaskFragmentAnimationSpec.createNoopAnimation(target);
+            } else if (target.mode == MODE_CLOSING) {
+                animation = mAnimationSpec.createChangeBoundsCloseAnimation(target);
+            } else {
+                animation = mAnimationSpec.createChangeBoundsOpenAnimation(target);
+            }
+            adapters.add(new TaskFragmentAnimationAdapter(animation, target));
+        }
+        return adapters;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
new file mode 100644
index 0000000..c0908a5
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+
+import android.app.ActivityThread;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.provider.Settings;
+import android.view.RemoteAnimationTarget;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.policy.AttributeCache;
+import com.android.internal.policy.TransitionAnimation;
+
+/** Animation spec for TaskFragment transition. */
+class TaskFragmentAnimationSpec {
+
+    private static final String TAG = "TaskFragAnimationSpec";
+    private static final int CHANGE_ANIMATION_DURATION = 517;
+    private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
+    private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
+
+    private final Context mContext;
+    private final TransitionAnimation mTransitionAnimation;
+    private final Interpolator mFastOutExtraSlowInInterpolator;
+    private final LinearInterpolator mLinearInterpolator;
+    private float mTransitionAnimationScaleSetting;
+
+    TaskFragmentAnimationSpec(@NonNull Handler handler) {
+        mContext = ActivityThread.currentActivityThread().getApplication();
+        mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
+        // Initialize the AttributeCache for the TransitionAnimation.
+        AttributeCache.init(mContext);
+        mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
+                mContext, android.R.interpolator.fast_out_extra_slow_in);
+        mLinearInterpolator = new LinearInterpolator();
+
+        // The transition animation should be adjusted based on the developer option.
+        final ContentResolver resolver = mContext.getContentResolver();
+        mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
+                Settings.Global.TRANSITION_ANIMATION_SCALE,
+                mContext.getResources().getFloat(
+                        R.dimen.config_appTransitionAnimationDurationScaleDefault));
+        resolver.registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
+                new SettingsObserver(handler));
+    }
+
+    /** For target that doesn't need to be animated. */
+    static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) {
+        // Noop but just keep the target showing/hiding.
+        final float alpha = target.mode == MODE_CLOSING ? 0f : 1f;
+        return new AlphaAnimation(alpha, alpha);
+    }
+
+    /** Animation for target that is opening in a change transition. */
+    Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) {
+        final Rect bounds = target.localBounds;
+        // The target will be animated in from left or right depends on its position.
+        final int startLeft = bounds.left == 0 ? -bounds.width() : bounds.width();
+
+        // The position should be 0-based as we will post translate in
+        // TaskFragmentAnimationAdapter#onAnimationUpdate
+        final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0);
+        animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+        animation.setDuration(CHANGE_ANIMATION_DURATION);
+        animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+
+    /** Animation for target that is closing in a change transition. */
+    Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) {
+        final Rect bounds = target.localBounds;
+        // The target will be animated out to left or right depends on its position.
+        final int endLeft = bounds.left == 0 ? -bounds.width() : bounds.width();
+
+        // The position should be 0-based as we will post translate in
+        // TaskFragmentAnimationAdapter#onAnimationUpdate
+        final Animation animation = new TranslateAnimation(0, endLeft, 0, 0);
+        animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+        animation.setDuration(CHANGE_ANIMATION_DURATION);
+        animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+
+    /**
+     * Animation for target that is changing (bounds change) in a change transition.
+     * @return the return array always has two elements. The first one is for the start leash, and
+     *         the second one is for the end leash.
+     */
+    Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) {
+        // Both start bounds and end bounds are in screen coordinates. We will post translate
+        // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate
+        final Rect startBounds = target.startBounds;
+        final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds();
+        final Rect endBounds = target.screenSpaceBounds;
+        float scaleX = ((float) startBounds.width()) / endBounds.width();
+        float scaleY = ((float) startBounds.height()) / endBounds.height();
+        // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
+        // be scaled up with its parent.
+        float startScaleX = 1.f / scaleX;
+        float startScaleY = 1.f / scaleY;
+
+        // The start leash will be fade out.
+        final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
+        final Animation startAlpha = new AlphaAnimation(1f, 0f);
+        startAlpha.setInterpolator(mLinearInterpolator);
+        startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
+        startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
+        startSet.addAnimation(startAlpha);
+        final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
+                startScaleY);
+        startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
+        startScale.setDuration(CHANGE_ANIMATION_DURATION);
+        startSet.addAnimation(startScale);
+        startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
+                endBounds.height());
+        startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+        // The end leash will be moved into the end position while scaling.
+        final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
+        endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
+        final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
+        endScale.setDuration(CHANGE_ANIMATION_DURATION);
+        endSet.addAnimation(endScale);
+        // The position should be 0-based as we will post translate in
+        // TaskFragmentAnimationAdapter#onAnimationUpdate
+        final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
+                0, 0);
+        endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
+        endSet.addAnimation(endTranslate);
+        // The end leash is resizing, we should update the window crop based on the clip rect.
+        final Rect startClip = new Rect(startBounds);
+        final Rect endClip = new Rect(endBounds);
+        startClip.offsetTo(0, 0);
+        endClip.offsetTo(0, 0);
+        final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+        clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
+        endSet.addAnimation(clipAnim);
+        endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
+                parentBounds.height());
+        endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+        return new Animation[]{startSet, endSet};
+    }
+
+    Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target,
+            @NonNull Rect wholeAnimationBounds) {
+        final boolean isEnter = target.mode != MODE_CLOSING;
+        final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter
+                ? R.styleable.WindowAnimation_activityOpenEnterAnimation
+                : R.styleable.WindowAnimation_activityOpenExitAnimation);
+        animation.initialize(target.localBounds.width(), target.localBounds.height(),
+                wholeAnimationBounds.width(), wholeAnimationBounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+
+    Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target,
+            @NonNull Rect wholeAnimationBounds) {
+        final boolean isEnter = target.mode != MODE_CLOSING;
+        final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter
+                ? R.styleable.WindowAnimation_activityCloseEnterAnimation
+                : R.styleable.WindowAnimation_activityCloseExitAnimation);
+        animation.initialize(target.localBounds.width(), target.localBounds.height(),
+                wholeAnimationBounds.width(), wholeAnimationBounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+
+    private class SettingsObserver extends ContentObserver {
+        SettingsObserver(@NonNull Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mTransitionAnimationScaleSetting = Settings.Global.getFloat(
+                    mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
+                    mTransitionAnimationScaleSetting);
+        }
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
new file mode 100644
index 0000000..54e44a7
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.embedding;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
+import android.window.TaskFragmentInfo;
+import android.window.WindowContainerTransaction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Client-side container for a stack of activities. Corresponds to an instance of TaskFragment
+ * on the server side.
+ */
+class TaskFragmentContainer {
+    /**
+     * Client-created token that uniquely identifies the task fragment container instance.
+     */
+    @NonNull
+    private final IBinder mToken;
+
+    /**
+     * Server-provided task fragment information.
+     */
+    private TaskFragmentInfo mInfo;
+
+    /**
+     * Activities that are being reparented or being started to this container, but haven't been
+     * added to {@link #mInfo} yet.
+     */
+    private final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>();
+
+    /** Containers that are dependent on this one and should be completely destroyed on exit. */
+    private final List<TaskFragmentContainer> mContainersToFinishOnExit =
+            new ArrayList<>();
+
+    /** Individual associated activities in different containers that should be finished on exit. */
+    private final List<Activity> mActivitiesToFinishOnExit = new ArrayList<>();
+
+    /** Indicates whether the container was cleaned up after the last activity was removed. */
+    private boolean mIsFinished;
+
+    /**
+     * Bounds that were requested last via {@link android.window.WindowContainerTransaction}.
+     */
+    private final Rect mLastRequestedBounds = new Rect();
+
+    /**
+     * Creates a container with an existing activity that will be re-parented to it in a window
+     * container transaction.
+     */
+    TaskFragmentContainer(@Nullable Activity activity) {
+        mToken = new Binder("TaskFragmentContainer");
+        if (activity != null) {
+            addPendingAppearedActivity(activity);
+        }
+    }
+
+    /**
+     * Returns the client-created token that uniquely identifies this container.
+     */
+    @NonNull
+    IBinder getTaskFragmentToken() {
+        return mToken;
+    }
+
+    /** List of activities that belong to this container and live in this process. */
+    @NonNull
+    List<Activity> collectActivities() {
+        // Add the re-parenting activity, in case the server has not yet reported the task
+        // fragment info update with it placed in this container. We still want to apply rules
+        // in this intermediate state.
+        List<Activity> allActivities = new ArrayList<>();
+        if (!mPendingAppearedActivities.isEmpty()) {
+            allActivities.addAll(mPendingAppearedActivities);
+        }
+        // Add activities reported from the server.
+        if (mInfo == null) {
+            return allActivities;
+        }
+        ActivityThread activityThread = ActivityThread.currentActivityThread();
+        for (IBinder token : mInfo.getActivities()) {
+            Activity activity = activityThread.getActivity(token);
+            if (activity != null && !allActivities.contains(activity)) {
+                allActivities.add(activity);
+            }
+        }
+        return allActivities;
+    }
+
+    void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) {
+        mPendingAppearedActivities.add(pendingAppearedActivity);
+    }
+
+    boolean hasActivity(@NonNull IBinder token) {
+        if (mInfo != null && mInfo.getActivities().contains(token)) {
+            return true;
+        }
+        for (Activity activity : mPendingAppearedActivities) {
+            if (activity.getActivityToken().equals(token)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    int getRunningActivityCount() {
+        int count = mPendingAppearedActivities.size();
+        if (mInfo != null) {
+            count += mInfo.getRunningActivityCount();
+        }
+        return count;
+    }
+
+    @Nullable
+    TaskFragmentInfo getInfo() {
+        return mInfo;
+    }
+
+    void setInfo(@NonNull TaskFragmentInfo info) {
+        mInfo = info;
+        if (mInfo == null || mPendingAppearedActivities.isEmpty()) {
+            return;
+        }
+        // Cleanup activities that were being re-parented
+        List<IBinder> infoActivities = mInfo.getActivities();
+        for (int i = mPendingAppearedActivities.size() - 1; i >= 0; --i) {
+            final Activity activity = mPendingAppearedActivities.get(i);
+            if (infoActivities.contains(activity.getActivityToken())) {
+                mPendingAppearedActivities.remove(i);
+            }
+        }
+    }
+
+    @Nullable
+    Activity getTopNonFinishingActivity() {
+        List<Activity> activities = collectActivities();
+        if (activities.isEmpty()) {
+            return null;
+        }
+        int i = activities.size() - 1;
+        while (i >= 0 && activities.get(i).isFinishing()) {
+            i--;
+        }
+        return i >= 0 ? activities.get(i) : null;
+    }
+
+    boolean isEmpty() {
+        return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty());
+    }
+
+    /**
+     * Adds a container that should be finished when this container is finished.
+     */
+    void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) {
+        mContainersToFinishOnExit.add(containerToFinish);
+    }
+
+    /**
+     * Adds an activity that should be finished when this container is finished.
+     */
+    void addActivityToFinishOnExit(@NonNull Activity activityToFinish) {
+        mActivitiesToFinishOnExit.add(activityToFinish);
+    }
+
+    /**
+     * Removes all activities that belong to this process and finishes other containers/activities
+     * configured to finish together.
+     */
+    void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
+            @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
+        if (!mIsFinished) {
+            mIsFinished = true;
+            finishActivities(shouldFinishDependent, presenter, wct, controller);
+        }
+
+        if (mInfo == null) {
+            // Defer removal the container and wait until TaskFragment appeared.
+            return;
+        }
+
+        // Cleanup the visuals
+        presenter.deleteTaskFragment(wct, getTaskFragmentToken());
+        // Cleanup the records
+        controller.removeContainer(this);
+        // Clean up task fragment information
+        mInfo = null;
+    }
+
+    private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
+            @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
+        // Finish own activities
+        for (Activity activity : collectActivities()) {
+            if (!activity.isFinishing()) {
+                activity.finish();
+            }
+        }
+
+        if (!shouldFinishDependent) {
+            return;
+        }
+
+        // Finish dependent containers
+        for (TaskFragmentContainer container : mContainersToFinishOnExit) {
+            container.finish(true /* shouldFinishDependent */, presenter,
+                    wct, controller);
+        }
+        mContainersToFinishOnExit.clear();
+
+        // Finish associated activities
+        for (Activity activity : mActivitiesToFinishOnExit) {
+            activity.finish();
+        }
+        mActivitiesToFinishOnExit.clear();
+
+        // Finish activities that were being re-parented to this container.
+        for (Activity activity : mPendingAppearedActivities) {
+            activity.finish();
+        }
+        mPendingAppearedActivities.clear();
+    }
+
+    boolean isFinished() {
+        return mIsFinished;
+    }
+
+    /**
+     * Checks if last requested bounds are equal to the provided value.
+     */
+    boolean areLastRequestedBoundsEqual(@Nullable Rect bounds) {
+        return (bounds == null && mLastRequestedBounds.isEmpty())
+                || mLastRequestedBounds.equals(bounds);
+    }
+
+    /**
+     * Updates the last requested bounds.
+     */
+    void setLastRequestedBounds(@Nullable Rect bounds) {
+        if (bounds == null) {
+            mLastRequestedBounds.setEmpty();
+        } else {
+            mLastRequestedBounds.set(bounds);
+        }
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
similarity index 63%
rename from libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java
rename to libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index a0d5b00..383d91d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.extensions;
+package androidx.window.extensions.layout;
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
@@ -36,19 +36,27 @@
 import androidx.window.util.PriorityDataProducer;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
 
 /**
- * Reference implementation of androidx.window.extensions OEM interface for use with
+ * Reference implementation of androidx.window.extensions.layout OEM interface for use with
  * WindowManager Jetpack.
  *
  * NOTE: This version is a work in progress and under active development. It MUST NOT be used in
  * production builds since the interface can still change before reaching stable version.
  * Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead.
  */
-class SampleExtensionImpl extends StubExtension {
+public class WindowLayoutComponentImpl implements WindowLayoutComponent {
     private static final String TAG = "SampleExtension";
+    private static WindowLayoutComponent sInstance;
+
+    private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
+            new HashMap<>();
 
     private final SettingsDevicePostureProducer mSettingsDevicePostureProducer;
     private final DataProducer<Integer> mDevicePostureProducer;
@@ -56,7 +64,7 @@
     private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer;
     private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer;
 
-    SampleExtensionImpl(Context context) {
+    public WindowLayoutComponentImpl(Context context) {
         mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context);
         mDevicePostureProducer = new PriorityDataProducer<>(List.of(
                 mSettingsDevicePostureProducer,
@@ -73,28 +81,68 @@
         mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
     }
 
+    /**
+     * Adds a listener interested in receiving updates to {@link WindowLayoutInfo}
+     * @param activity hosting a {@link android.view.Window}
+     * @param consumer interested in receiving updates to {@link WindowLayoutInfo}
+     */
+    public void addWindowLayoutInfoListener(@NonNull Activity activity,
+            @NonNull Consumer<WindowLayoutInfo> consumer) {
+        mWindowLayoutChangeListeners.put(activity, consumer);
+        updateRegistrations();
+    }
+
+    /**
+     * Removes a listener no longer interested in receiving updates.
+     * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
+     */
+    public void removeWindowLayoutInfoListener(
+            @NonNull Consumer<WindowLayoutInfo> consumer) {
+        mWindowLayoutChangeListeners.values().remove(consumer);
+        updateRegistrations();
+    }
+
+    void updateWindowLayout(@NonNull Activity activity,
+            @NonNull WindowLayoutInfo newLayout) {
+        Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity);
+        if (consumer != null) {
+            consumer.accept(newLayout);
+        }
+    }
+
+    @NonNull
+    Set<Activity> getActivitiesListeningForLayoutChanges() {
+        return mWindowLayoutChangeListeners.keySet();
+    }
+
+    protected boolean hasListeners() {
+        return !mWindowLayoutChangeListeners.isEmpty();
+    }
+
     private int getFeatureState(DisplayFeature feature) {
         Integer featureState = feature.getState();
         Optional<Integer> posture = mDevicePostureProducer.getData();
-        int fallbackPosture = posture.orElse(ExtensionFoldingFeature.STATE_FLAT);
+        int fallbackPosture = posture.orElse(FoldingFeature.STATE_FLAT);
         return featureState == null ? fallbackPosture : featureState;
     }
 
     private void onDisplayFeaturesChanged() {
         for (Activity activity : getActivitiesListeningForLayoutChanges()) {
-            ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(activity);
+            WindowLayoutInfo newLayout = getWindowLayoutInfo(activity);
             updateWindowLayout(activity, newLayout);
         }
     }
 
     @NonNull
-    private ExtensionWindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
-        List<ExtensionDisplayFeature> displayFeatures = getDisplayFeatures(activity);
-        return new ExtensionWindowLayoutInfo(displayFeatures);
+    private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
+        List<androidx.window.extensions.layout.DisplayFeature> displayFeatures =
+                getDisplayFeatures(activity);
+        return new WindowLayoutInfo(displayFeatures);
     }
 
-    private List<ExtensionDisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
-        List<ExtensionDisplayFeature> features = new ArrayList<>();
+    private List<androidx.window.extensions.layout.DisplayFeature> getDisplayFeatures(
+            @NonNull Activity activity) {
+        List<androidx.window.extensions.layout.DisplayFeature> features = new ArrayList<>();
         int displayId = activity.getDisplay().getDisplayId();
         if (displayId != DEFAULT_DISPLAY) {
             Log.w(TAG, "This sample doesn't support display features on secondary displays");
@@ -115,15 +163,14 @@
                 rotateRectToDisplayRotation(displayId, featureRect);
                 transformToWindowSpaceRect(activity, featureRect);
 
-                features.add(new ExtensionFoldingFeature(featureRect, baseFeature.getType(),
+                features.add(new FoldingFeature(featureRect, baseFeature.getType(),
                         getFeatureState(baseFeature)));
             }
         }
         return features;
     }
 
-    @Override
-    protected void onListenersChanged() {
+    private void updateRegistrations() {
         if (hasListeners()) {
             mSettingsDevicePostureProducer.registerObserversIfNeeded();
             mSettingsDisplayFeatureProducer.registerObserversIfNeeded();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
index e6f8388..62959b7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
@@ -28,7 +28,7 @@
      * an OEM by overriding this method.
      */
     public static SidecarInterface getSidecarImpl(Context context) {
-        return new SampleSidecarImpl(context);
+        return new SampleSidecarImpl(context.getApplicationContext());
     }
 
     /**
@@ -36,6 +36,6 @@
      * @return API version string in MAJOR.MINOR.PATCH-description format.
      */
     public static String getApiVersion() {
-        return "0.1.0-settings_sample";
+        return "1.0.0-reference";
     }
 }
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index be6652d..4f36c9c 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 9aaef3b..3ba1a34 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -39,6 +39,14 @@
 }
 
 filegroup {
+    name: "wm_shell_util-sources",
+    srcs: [
+        "src/com/android/wm/shell/util/**/*.java",
+    ],
+    path: "src",
+}
+
+filegroup {
     name: "wm_shell-aidls",
     srcs: [
         "src/**/*.aidl",
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/libs/WindowManager/Shell/res/color/split_divider_background.xml
similarity index 67%
rename from packages/SystemUI/res/drawable/media_output_dialog_background.xml
rename to libs/WindowManager/Shell/res/color/split_divider_background.xml
index 3ceb0f6..329e5b9 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_background.xml
+++ b/libs/WindowManager/Shell/res/color/split_divider_background.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
+  ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,10 +14,6 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android">
-    <shape android:shape="rectangle">
-        <corners android:radius="8dp" />
-        <solid android:color="?android:attr/colorBackground" />
-    </shape>
-</inset>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@android:color/system_neutral1_500" android:lStar="35" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/unfold_transition_background.xml b/libs/WindowManager/Shell/res/color/unfold_transition_background.xml
new file mode 100644
index 0000000..63289a3
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/unfold_transition_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Matches taskbar color -->
+    <item android:color="@android:color/system_neutral2_500" android:lStar="35" />
+</selector>
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
index 8710fb8..96d2d7c 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
@@ -18,7 +18,7 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
     <solid
-        android:color="@android:color/system_neutral1_900"
+        android:color="@android:color/system_neutral1_800"
         />
     <corners android:radius="20dp" />
 
diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
index c09ae53..0cf6d73 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
@@ -17,13 +17,13 @@
 <com.android.wm.shell.common.AlphaOptimizedButton
     xmlns:android="http://schemas.android.com/apk/res/android"
     style="@android:style/Widget.DeviceDefault.Button.Borderless"
-    android:id="@+id/settings_button"
+    android:id="@+id/manage_button"
     android:layout_gravity="start"
     android:layout_width="wrap_content"
-    android:layout_height="40dp"
-    android:layout_marginTop="8dp"
-    android:layout_marginLeft="16dp"
-    android:layout_marginBottom="8dp"
+    android:layout_height="@dimen/bubble_manage_button_height"
+    android:layout_marginStart="@dimen/bubble_manage_button_margin"
+    android:layout_marginTop="@dimen/bubble_manage_button_margin"
+    android:layout_marginBottom="@dimen/bubble_manage_button_margin"
     android:focusable="true"
     android:text="@string/manage_bubbles_text"
     android:textSize="@*android:dimen/text_size_body_2_material"
diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
index f4b3aca..298ad30 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
@@ -25,15 +25,15 @@
         android:id="@+id/bubble_manage_menu_dismiss_container"
         android:background="@drawable/bubble_manage_menu_row"
         android:layout_width="match_parent"
-        android:layout_height="48dp"
+        android:layout_height="@dimen/bubble_menu_item_height"
         android:gravity="center_vertical"
-        android:paddingStart="16dp"
-        android:paddingEnd="16dp"
+        android:paddingStart="@dimen/bubble_menu_padding"
+        android:paddingEnd="@dimen/bubble_menu_padding"
         android:orientation="horizontal">
 
         <ImageView
-            android:layout_width="24dp"
-            android:layout_height="24dp"
+            android:layout_width="@dimen/bubble_menu_icon_size"
+            android:layout_height="@dimen/bubble_menu_icon_size"
             android:src="@drawable/ic_remove_no_shadow"
             android:tint="@color/bubbles_icon_tint"/>
 
@@ -50,15 +50,15 @@
         android:id="@+id/bubble_manage_menu_dont_bubble_container"
         android:background="@drawable/bubble_manage_menu_row"
         android:layout_width="match_parent"
-        android:layout_height="48dp"
+        android:layout_height="@dimen/bubble_menu_item_height"
         android:gravity="center_vertical"
-        android:paddingStart="16dp"
-        android:paddingEnd="16dp"
+        android:paddingStart="@dimen/bubble_menu_padding"
+        android:paddingEnd="@dimen/bubble_menu_padding"
         android:orientation="horizontal">
 
         <ImageView
-            android:layout_width="24dp"
-            android:layout_height="24dp"
+            android:layout_width="@dimen/bubble_menu_icon_size"
+            android:layout_height="@dimen/bubble_menu_icon_size"
             android:src="@drawable/bubble_ic_stop_bubble"
             android:tint="@color/bubbles_icon_tint"/>
 
@@ -75,16 +75,16 @@
         android:id="@+id/bubble_manage_menu_settings_container"
         android:background="@drawable/bubble_manage_menu_row"
         android:layout_width="match_parent"
-        android:layout_height="48dp"
+        android:layout_height="@dimen/bubble_menu_item_height"
         android:gravity="center_vertical"
-        android:paddingStart="16dp"
-        android:paddingEnd="16dp"
+        android:paddingStart="@dimen/bubble_menu_padding"
+        android:paddingEnd="@dimen/bubble_menu_padding"
         android:orientation="horizontal">
 
         <ImageView
             android:id="@+id/bubble_manage_menu_settings_icon"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
+            android:layout_width="@dimen/bubble_menu_icon_size"
+            android:layout_height="@dimen/bubble_menu_icon_size"
             android:src="@drawable/ic_remove_no_shadow"/>
 
         <TextView
diff --git a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml
index fd4c3ba..87deb8b 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml
@@ -21,7 +21,6 @@
     android:layout_width="wrap_content"
     android:paddingTop="48dp"
     android:paddingBottom="48dp"
-    android:paddingStart="@dimen/bubble_stack_user_education_side_inset"
     android:paddingEnd="16dp"
     android:layout_marginEnd="24dp"
     android:orientation="vertical"
diff --git a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml
index c5c42fc..fafe40e 100644
--- a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml
+++ b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml
@@ -23,7 +23,6 @@
     android:clickable="true"
     android:paddingTop="28dp"
     android:paddingBottom="16dp"
-    android:paddingStart="@dimen/bubble_expanded_view_padding"
     android:paddingEnd="48dp"
     android:layout_marginEnd="24dp"
     android:orientation="vertical"
@@ -66,27 +65,21 @@
         android:id="@+id/button_layout"
         android:orientation="horizontal" >
 
-        <com.android.wm.shell.common.AlphaOptimizedButton
-            style="@android:style/Widget.Material.Button.Borderless"
-            android:id="@+id/manage"
-            android:layout_gravity="start"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:focusable="true"
-            android:clickable="false"
-            android:text="@string/manage_bubbles_text"
-            android:textColor="@android:color/system_neutral1_900"
+        <include
+            layout="@layout/bubble_manage_button"
             />
 
         <com.android.wm.shell.common.AlphaOptimizedButton
-            style="@android:style/Widget.Material.Button.Borderless"
+            style="@android:style/Widget.DeviceDefault.Button.Borderless"
             android:id="@+id/got_it"
             android:layout_gravity="start"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_height="@dimen/bubble_manage_button_height"
             android:focusable="true"
             android:text="@string/bubbles_user_education_got_it"
+            android:textSize="@*android:dimen/text_size_body_2_material"
             android:textColor="@android:color/system_neutral1_900"
+            android:background="@drawable/bubble_manage_btn_bg"
             />
     </LinearLayout>
 </LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
index ed5d2e1..d732b01 100644
--- a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
+++ b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml
@@ -22,7 +22,7 @@
     <View
         style="@style/DockedDividerBackground"
         android:id="@+id/docked_divider_background"
-        android:background="@color/docked_divider_background"/>
+        android:background="@color/split_divider_background"/>
 
     <com.android.wm.shell.legacysplitscreen.MinimizedDockShadow
         style="@style/DockedDividerMinimizedShadow"
diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml
index 7f583f3..e3be700 100644
--- a/libs/WindowManager/Shell/res/layout/split_divider.xml
+++ b/libs/WindowManager/Shell/res/layout/split_divider.xml
@@ -19,15 +19,25 @@
     android:layout_height="match_parent"
     android:layout_width="match_parent">
 
-    <View
-        style="@style/DockedDividerBackground"
-        android:id="@+id/docked_divider_background"
-        android:background="@color/docked_divider_background"/>
+    <FrameLayout
+        android:id="@+id/divider_bar"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
 
-    <com.android.wm.shell.common.split.DividerHandleView
-        style="@style/DockedDividerHandle"
-        android:id="@+id/docked_divider_handle"
-        android:contentDescription="@string/accessibility_divider"
-        android:background="@null"/>
+        <View
+            style="@style/DockedDividerBackground"
+            android:id="@+id/docked_divider_background"/>
+
+        <com.android.wm.shell.common.split.DividerHandleView
+            style="@style/DockedDividerHandle"
+            android:id="@+id/docked_divider_handle"
+            android:contentDescription="@string/accessibility_divider"
+            android:background="@null"/>
+
+        <com.android.wm.shell.common.split.DividerRoundedCorner
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+    </FrameLayout>
 
 </com.android.wm.shell.common.split.DividerView>
diff --git a/libs/WindowManager/Shell/res/layout/split_outline.xml b/libs/WindowManager/Shell/res/layout/split_outline.xml
new file mode 100644
index 0000000..13a30f5
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/split_outline.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.android.wm.shell.splitscreen.OutlineView
+        android:id="@+id/split_outline"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent" />
+
+</FrameLayout>
diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml
index aafba58..a95323f 100644
--- a/libs/WindowManager/Shell/res/values-land/dimens.xml
+++ b/libs/WindowManager/Shell/res/values-land/dimens.xml
@@ -16,8 +16,12 @@
 */
 -->
 <resources>
+    <!-- Divider handle size for legacy split screen -->
     <dimen name="docked_divider_handle_width">2dp</dimen>
     <dimen name="docked_divider_handle_height">16dp</dimen>
+    <!-- Divider handle size for split screen -->
+    <dimen name="split_divider_handle_width">3dp</dimen>
+    <dimen name="split_divider_handle_height">72dp</dimen>
 
     <!-- Padding between status bar and bubbles when displayed in expanded state, smaller
      value in landscape since we have limited vertical space-->
diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml
index 863bb69..9eddac4 100644
--- a/libs/WindowManager/Shell/res/values-land/styles.xml
+++ b/libs/WindowManager/Shell/res/values-land/styles.xml
@@ -19,6 +19,7 @@
         <item name="android:layout_width">10dp</item>
         <item name="android:layout_height">match_parent</item>
         <item name="android:layout_gravity">center_horizontal</item>
+        <item name="android:background">@color/split_divider_background</item>
     </style>
 
     <style name="DockedDividerHandle">
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 350beaf..93c0352 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -17,7 +17,6 @@
  */
 -->
 <resources>
-    <color name="docked_divider_background">#ff000000</color>
     <color name="docked_divider_handle">#ffffff</color>
     <drawable name="forced_resizable_background">#59000000</drawable>
     <color name="minimize_dock_shadow_start">#60000000</color>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f28ee82..f857664 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -76,8 +76,15 @@
     <!-- How high we lift the divider when touching -->
     <dimen name="docked_stack_divider_lift_elevation">4dp</dimen>
 
+    <!-- Divider handle size for legacy split screen -->
     <dimen name="docked_divider_handle_width">16dp</dimen>
     <dimen name="docked_divider_handle_height">2dp</dimen>
+    <!-- Divider handle size for split screen -->
+    <dimen name="split_divider_handle_width">72dp</dimen>
+    <dimen name="split_divider_handle_height">3dp</dimen>
+
+    <dimen name="split_divider_bar_width">10dp</dimen>
+    <dimen name="split_divider_corner_size">42dp</dimen>
 
     <!-- One-Handed Mode -->
     <!-- Threshold for dragging distance to enable one-handed mode -->
@@ -100,6 +107,8 @@
     <dimen name="bubble_flyout_space_from_bubble">8dp</dimen>
     <!-- How much space to leave between the flyout text and the avatar displayed in the flyout. -->
     <dimen name="bubble_flyout_avatar_message_space">6dp</dimen>
+    <!-- If the screen percentage is smaller than this, we'll use this value instead.  -->
+    <dimen name="bubbles_flyout_min_width_large_screen">200dp</dimen>
     <!-- Padding between status bar and bubbles when displayed in expanded state -->
     <dimen name="bubble_padding_top">16dp</dimen>
     <!-- Space between bubbles when expanded. -->
@@ -122,7 +131,7 @@
          should also be updated. -->
     <dimen name="bubble_expanded_default_height">180dp</dimen>
     <!-- On large screens the width of the expanded view is restricted to this size. -->
-    <dimen name="bubble_expanded_view_tablet_width">412dp</dimen>
+    <dimen name="bubble_expanded_view_phone_landscape_overflow_width">412dp</dimen>
     <!-- Inset to apply to the icon in the overflow button. -->
     <dimen name="bubble_overflow_icon_inset">30dp</dimen>
     <!-- Default (and minimum) height of bubble overflow -->
@@ -149,9 +158,17 @@
     <!-- Extra padding around the dismiss target for bubbles -->
     <dimen name="bubble_dismiss_slop">16dp</dimen>
     <!-- Height of button allowing users to adjust settings for bubbles. -->
-    <dimen name="bubble_manage_button_height">56dp</dimen>
+    <dimen name="bubble_manage_button_height">36dp</dimen>
+    <!-- Height of manage button including margins. -->
+    <dimen name="bubble_manage_button_total_height">68dp</dimen>
+    <!-- The margin around the outside of the manage button. -->
+    <dimen name="bubble_manage_button_margin">16dp</dimen>
     <!-- Height of an item in the bubble manage menu. -->
     <dimen name="bubble_menu_item_height">60dp</dimen>
+    <!-- Padding applied to the bubble manage menu. -->
+    <dimen name="bubble_menu_padding">16dp</dimen>
+    <!-- Size of the icons in the manage menu. -->
+    <dimen name="bubble_menu_icon_size">24dp</dimen>
     <!-- Max width of the message bubble-->
     <dimen name="bubble_message_max_width">144dp</dimen>
     <!-- Min width of the message bubble -->
@@ -174,14 +191,8 @@
     <dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
     <dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
     <dimen name="bubble_manage_menu_elevation">4dp</dimen>
-
-    <!-- Bubbles user education views -->
-    <dimen name="bubbles_manage_education_width">160dp</dimen>
-    <!-- The inset from the top bound of the manage button to place the user education. -->
-    <dimen name="bubbles_manage_education_top_inset">65dp</dimen>
-    <!-- Size of padding for the user education cling, this should at minimum be larger than
-        individual_bubble_size + some padding. -->
-    <dimen name="bubble_stack_user_education_side_inset">72dp</dimen>
+    <!-- Size of user education views on large screens (phone is just match parent). -->
+    <dimen name="bubbles_user_education_width_large_screen">400dp</dimen>
 
     <!-- The width/height of the size compat restart button. -->
     <dimen name="size_compat_button_size">48dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index fffcd33..cb6d4de 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -32,8 +32,9 @@
 
     <style name="DockedDividerBackground">
         <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_height">10dp</item>
+        <item name="android:layout_height">@dimen/split_divider_bar_width</item>
         <item name="android:layout_gravity">center_vertical</item>
+        <item name="android:background">@color/split_divider_background</item>
     </style>
 
     <style name="DockedDividerMinimizedShadow">
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java
deleted file mode 100644
index 006730d..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell;
-
-import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
-import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
-
-import android.app.ActivityManager;
-import android.graphics.Point;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.view.SurfaceControl;
-
-import androidx.annotation.NonNull;
-
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.transition.Transitions;
-
-import java.io.PrintWriter;
-
-/**
-  * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}.
-  */
-public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
-    private static final String TAG = "FullscreenTaskListener";
-
-    private final SyncTransactionQueue mSyncQueue;
-
-    private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>();
-
-    public FullscreenTaskListener(SyncTransactionQueue syncQueue) {
-        mSyncQueue = syncQueue;
-    }
-
-    @Override
-    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
-        if (mDataByTaskId.get(taskInfo.taskId) != null) {
-            throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId);
-        }
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d",
-                taskInfo.taskId);
-        final Point positionInParent = taskInfo.positionInParent;
-        mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent));
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
-        mSyncQueue.runInSync(t -> {
-            // Reset several properties back to fullscreen (PiP, for example, leaves all these
-            // properties in a bad state).
-            t.setWindowCrop(leash, null);
-            t.setPosition(leash, positionInParent.x, positionInParent.y);
-            t.setAlpha(leash, 1f);
-            t.setMatrix(leash, 1, 0, 0, 1);
-            t.show(leash);
-        });
-    }
-
-    @Override
-    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
-        final TaskData data = mDataByTaskId.get(taskInfo.taskId);
-        final Point positionInParent = taskInfo.positionInParent;
-        if (!positionInParent.equals(data.positionInParent)) {
-            data.positionInParent.set(positionInParent.x, positionInParent.y);
-            mSyncQueue.runInSync(t -> {
-                t.setPosition(data.surface, positionInParent.x, positionInParent.y);
-            });
-        }
-    }
-
-    @Override
-    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
-        if (mDataByTaskId.get(taskInfo.taskId) == null) {
-            Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
-            return;
-        }
-        mDataByTaskId.remove(taskInfo.taskId);
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
-                taskInfo.taskId);
-    }
-
-    @Override
-    public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
-        if (!mDataByTaskId.contains(taskId)) {
-            throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
-        }
-        b.setParent(mDataByTaskId.get(taskId).surface);
-    }
-
-    @Override
-    public void dump(@NonNull PrintWriter pw, String prefix) {
-        final String innerPrefix = prefix + "  ";
-        pw.println(prefix + this);
-        pw.println(innerPrefix + mDataByTaskId.size() + " Tasks");
-    }
-
-    @Override
-    public String toString() {
-        return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN);
-    }
-
-    /**
-     * Per-task data for each managed task.
-     */
-    private static class TaskData {
-        public final SurfaceControl surface;
-        public final Point positionInParent;
-
-        public TaskData(SurfaceControl surface, Point positionInParent) {
-            this.surface = surface;
-            this.positionInParent = positionInParent;
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
new file mode 100644
index 0000000..14ba9df
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell;
+
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaAppearedInfo;
+import android.window.DisplayAreaInfo;
+import android.window.DisplayAreaOrganizer;
+
+import androidx.annotation.NonNull;
+
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Display area organizer for the root display areas */
+public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer {
+
+    private static final String TAG = RootDisplayAreaOrganizer.class.getSimpleName();
+
+    /** {@link DisplayAreaInfo} list, which is mapped by display IDs. */
+    private final SparseArray<DisplayAreaInfo> mDisplayAreasInfo = new SparseArray<>();
+    /** Display area leashes, which is mapped by display IDs. */
+    private final SparseArray<SurfaceControl> mLeashes = new SparseArray<>();
+
+    public RootDisplayAreaOrganizer(Executor executor) {
+        super(executor);
+        List<DisplayAreaAppearedInfo> infos = registerOrganizer(FEATURE_ROOT);
+        for (int i = infos.size() - 1; i >= 0; --i) {
+            onDisplayAreaAppeared(infos.get(i).getDisplayAreaInfo(), infos.get(i).getLeash());
+        }
+    }
+
+    public void attachToDisplayArea(int displayId, SurfaceControl.Builder b) {
+        final SurfaceControl sc = mLeashes.get(displayId);
+        if (sc != null) {
+            b.setParent(sc);
+        }
+    }
+
+    @Override
+    public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
+            @NonNull SurfaceControl leash) {
+        if (displayAreaInfo.featureId != FEATURE_ROOT) {
+            throw new IllegalArgumentException(
+                    "Unknown feature: " + displayAreaInfo.featureId
+                            + "displayAreaInfo:" + displayAreaInfo);
+        }
+
+        final int displayId = displayAreaInfo.displayId;
+        if (mDisplayAreasInfo.get(displayId) != null) {
+            throw new IllegalArgumentException(
+                    "Duplicate DA for displayId: " + displayId
+                            + " displayAreaInfo:" + displayAreaInfo
+                            + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId));
+        }
+
+        mDisplayAreasInfo.put(displayId, displayAreaInfo);
+        mLeashes.put(displayId, leash);
+    }
+
+    @Override
+    public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) {
+        final int displayId = displayAreaInfo.displayId;
+        if (mDisplayAreasInfo.get(displayId) == null) {
+            throw new IllegalArgumentException(
+                    "onDisplayAreaVanished() Unknown DA displayId: " + displayId
+                            + " displayAreaInfo:" + displayAreaInfo
+                            + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId));
+        }
+
+        mDisplayAreasInfo.remove(displayId);
+    }
+
+    @Override
+    public void onDisplayAreaInfoChanged(@NonNull DisplayAreaInfo displayAreaInfo) {
+        final int displayId = displayAreaInfo.displayId;
+        if (mDisplayAreasInfo.get(displayId) == null) {
+            throw new IllegalArgumentException(
+                    "onDisplayAreaInfoChanged() Unknown DA displayId: " + displayId
+                            + " displayAreaInfo:" + displayAreaInfo
+                            + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId));
+        }
+
+        mDisplayAreasInfo.put(displayId, displayAreaInfo);
+    }
+
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        final String childPrefix = innerPrefix + "  ";
+        pw.println(prefix + this);
+    }
+
+    @Override
+    public String toString() {
+        return TAG + "#" + mDisplayAreasInfo.size();
+    }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
index 34c66a4..bf074b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
@@ -97,6 +97,14 @@
         b.setParent(sc);
     }
 
+    public void setPosition(@NonNull SurfaceControl.Transaction tx, int displayId, int x, int y) {
+        final SurfaceControl sc = mLeashes.get(displayId);
+        if (sc == null) {
+            throw new IllegalArgumentException("can't find display" + displayId);
+        }
+        tx.setPosition(sc, x, y);
+    }
+
     @Override
     public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
             @NonNull SurfaceControl leash) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
index 0b941b5..9113c79 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
@@ -103,6 +103,8 @@
                 return runMoveToSideStage(args, pw);
             case "removeFromSideStage":
                 return runRemoveFromSideStage(args, pw);
+            case "setSideStageOutline":
+                return runSetSideStageOutline(args, pw);
             case "setSideStagePosition":
                 return runSetSideStagePosition(args, pw);
             case "setSideStageVisibility":
@@ -161,6 +163,18 @@
         return true;
     }
 
+    private boolean runSetSideStageOutline(String[] args, PrintWriter pw) {
+        if (args.length < 3) {
+            // First arguments are "WMShell" and command name.
+            pw.println("Error: whether to enable or disable side stage outline border should be"
+                    + " provided as arguments");
+            return false;
+        }
+        final boolean enable = new Boolean(args[2]);
+        mSplitScreenOptional.ifPresent(split -> split.setSideStageOutline(enable));
+        return true;
+    }
+
     private boolean runSetSideStagePosition(String[] args, PrintWriter pw) {
         if (args.length < 3) {
             // First arguments are "WMShell" and command name.
@@ -175,7 +189,7 @@
     private boolean runSetSideStageVisibility(String[] args, PrintWriter pw) {
         if (args.length < 3) {
             // First arguments are "WMShell" and command name.
-            pw.println("Error: side stage position should be provided as arguments");
+            pw.println("Error: side stage visibility should be provided as arguments");
             return false;
         }
         final Boolean visible = new Boolean(args[2]);
@@ -197,6 +211,8 @@
         pw.println("    Move a task with given id in split-screen mode.");
         pw.println("  removeFromSideStage <taskId>");
         pw.println("    Remove a task with given id in split-screen mode.");
+        pw.println("  setSideStageOutline <true/false>");
+        pw.println("    Enable/Disable outline on the side-stage.");
         pw.println("  setSideStagePosition <SideStagePosition>");
         pw.println("    Sets the position of the side-stage.");
         pw.println("  setSideStageVisibility <true/false>");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
index d1fbf31..fa58fcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
@@ -20,10 +20,15 @@
 
 import com.android.wm.shell.apppairs.AppPairsController;
 import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.freeform.FreeformTaskListener;
+import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -38,7 +43,9 @@
 public class ShellInitImpl {
     private static final String TAG = ShellInitImpl.class.getSimpleName();
 
+    private final DisplayController mDisplayController;
     private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
     private final DragAndDropController mDragAndDropController;
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final Optional<BubbleController> mBubblesOptional;
@@ -47,13 +54,18 @@
     private final Optional<AppPairsController> mAppPairsOptional;
     private final Optional<PipTouchHandler> mPipTouchHandlerOptional;
     private final FullscreenTaskListener mFullscreenTaskListener;
+    private final Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
+    private final Optional<FreeformTaskListener> mFreeformTaskListenerOptional;
     private final ShellExecutor mMainExecutor;
     private final Transitions mTransitions;
     private final StartingWindowController mStartingWindow;
 
     private final InitImpl mImpl = new InitImpl();
 
-    public ShellInitImpl(DisplayImeController displayImeController,
+    public ShellInitImpl(
+            DisplayController displayController,
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
             DragAndDropController dragAndDropController,
             ShellTaskOrganizer shellTaskOrganizer,
             Optional<BubbleController> bubblesOptional,
@@ -62,10 +74,14 @@
             Optional<AppPairsController> appPairsOptional,
             Optional<PipTouchHandler> pipTouchHandlerOptional,
             FullscreenTaskListener fullscreenTaskListener,
+            Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController,
+            Optional<Optional<FreeformTaskListener>> freeformTaskListenerOptional,
             Transitions transitions,
             StartingWindowController startingWindow,
             ShellExecutor mainExecutor) {
+        mDisplayController = displayController;
         mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
         mDragAndDropController = dragAndDropController;
         mShellTaskOrganizer = shellTaskOrganizer;
         mBubblesOptional = bubblesOptional;
@@ -74,6 +90,8 @@
         mAppPairsOptional = appPairsOptional;
         mFullscreenTaskListener = fullscreenTaskListener;
         mPipTouchHandlerOptional = pipTouchHandlerOptional;
+        mFullscreenUnfoldController = fullscreenUnfoldTransitionController;
+        mFreeformTaskListenerOptional = freeformTaskListenerOptional.flatMap(f -> f);
         mTransitions = transitions;
         mMainExecutor = mainExecutor;
         mStartingWindow = startingWindow;
@@ -84,7 +102,9 @@
     }
 
     private void init() {
-        // Start listening for display changes
+        // Start listening for display and insets changes
+        mDisplayController.initialize();
+        mDisplayInsetsController.initialize();
         mDisplayImeController.startMonitorDisplays();
 
         // Setup the shell organizer
@@ -108,6 +128,13 @@
         // controller instead of the feature interface, can just initialize the touch handler if
         // needed
         mPipTouchHandlerOptional.ifPresent((handler) -> handler.init());
+
+        // Initialize optional freeform
+        mFreeformTaskListenerOptional.ifPresent(f ->
+                mShellTaskOrganizer.addListenerForType(
+                        f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM));
+
+        mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init);
     }
 
     @ExternalThread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 656bdff..020ecb7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -31,6 +31,7 @@
 import android.app.TaskInfo;
 import android.content.Context;
 import android.content.LocusId;
+import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
 import android.os.Binder;
 import android.os.IBinder;
@@ -41,11 +42,13 @@
 import android.view.SurfaceControl;
 import android.window.ITaskOrganizerController;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskOrganizer;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.sizecompatui.SizeCompatUIController;
@@ -71,12 +74,14 @@
     public static final int TASK_LISTENER_TYPE_FULLSCREEN = -2;
     public static final int TASK_LISTENER_TYPE_MULTI_WINDOW = -3;
     public static final int TASK_LISTENER_TYPE_PIP = -4;
+    public static final int TASK_LISTENER_TYPE_FREEFORM = -5;
 
     @IntDef(prefix = {"TASK_LISTENER_TYPE_"}, value = {
             TASK_LISTENER_TYPE_UNDEFINED,
             TASK_LISTENER_TYPE_FULLSCREEN,
             TASK_LISTENER_TYPE_MULTI_WINDOW,
             TASK_LISTENER_TYPE_PIP,
+            TASK_LISTENER_TYPE_FREEFORM,
     })
     public @interface TaskListenerType {}
 
@@ -229,14 +234,14 @@
                             + " already exists");
                 }
                 mTaskListeners.put(listenerType, listener);
+            }
 
-                // Notify the listener of all existing tasks with the given type.
-                for (int i = mTasks.size() - 1; i >= 0; --i) {
-                    final TaskAppearedInfo data = mTasks.valueAt(i);
-                    final TaskListener taskListener = getTaskListener(data.getTaskInfo());
-                    if (taskListener != listener) continue;
-                    listener.onTaskAppeared(data.getTaskInfo(), data.getLeash());
-                }
+            // Notify the listener of all existing tasks with the given type.
+            for (int i = mTasks.size() - 1; i >= 0; --i) {
+                final TaskAppearedInfo data = mTasks.valueAt(i);
+                final TaskListener taskListener = getTaskListener(data.getTaskInfo());
+                if (taskListener != listener) continue;
+                listener.onTaskAppeared(data.getTaskInfo(), data.getLeash());
             }
         }
     }
@@ -262,8 +267,12 @@
                 tasks.add(data);
             }
 
-            // Remove listener
-            mTaskListeners.removeAt(index);
+            // Remove listener, there can be the multiple occurrences, so search the whole list.
+            for (int i = mTaskListeners.size() - 1; i >= 0; --i) {
+                if (mTaskListeners.valueAt(i) == listener) {
+                    mTaskListeners.removeAt(i);
+                }
+            }
 
             // Associate tasks with new listeners if needed.
             for (int i = tasks.size() - 1; i >= 0; --i) {
@@ -314,10 +323,9 @@
     }
 
     @Override
-    public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
         if (mStartingWindow != null) {
-            mStartingWindow.removeStartingWindow(taskId, leash, frame, playRevealAnimation);
+            mStartingWindow.removeStartingWindow(removalInfo);
         }
     }
 
@@ -493,14 +501,40 @@
     }
 
     @Override
+    public void onSizeCompatRestartButtonAppeared(int taskId) {
+        final TaskAppearedInfo info;
+        synchronized (mLock) {
+            info = mTasks.get(taskId);
+        }
+        if (info == null) {
+            return;
+        }
+        logSizeCompatRestartButtonEventReported(info,
+                FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED);
+    }
+
+    @Override
     public void onSizeCompatRestartButtonClicked(int taskId) {
         final TaskAppearedInfo info;
         synchronized (mLock) {
             info = mTasks.get(taskId);
         }
-        if (info != null) {
-            restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token);
+        if (info == null) {
+            return;
         }
+        logSizeCompatRestartButtonEventReported(info,
+                FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED);
+        restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token);
+    }
+
+    private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
+            int event) {
+        ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
+        if (topActivityInfo == null) {
+            return;
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED,
+                topActivityInfo.applicationInfo.uid, event);
     }
 
     /**
@@ -579,6 +613,7 @@
             case WINDOWING_MODE_PINNED:
                 return TASK_LISTENER_TYPE_PIP;
             case WINDOWING_MODE_FREEFORM:
+                return TASK_LISTENER_TYPE_FREEFORM;
             case WINDOWING_MODE_UNDEFINED:
             default:
                 return TASK_LISTENER_TYPE_UNDEFINED;
@@ -593,6 +628,8 @@
                 return "TASK_LISTENER_TYPE_MULTI_WINDOW";
             case TASK_LISTENER_TYPE_PIP:
                 return "TASK_LISTENER_TYPE_PIP";
+            case TASK_LISTENER_TYPE_FREEFORM:
+                return "TASK_LISTENER_TYPE_FREEFORM";
             case TASK_LISTENER_TYPE_UNDEFINED:
                 return "TASK_LISTENER_TYPE_UNDEFINED";
             default:
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
index 1861e48..2f3214d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -40,6 +40,8 @@
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
+import com.android.wm.shell.common.SyncTransactionQueue;
+
 import java.io.PrintWriter;
 import java.util.concurrent.Executor;
 
@@ -74,6 +76,7 @@
 
     private final ShellTaskOrganizer mTaskOrganizer;
     private final Executor mShellExecutor;
+    private final SyncTransactionQueue mSyncQueue;
 
     private ActivityManager.RunningTaskInfo mTaskInfo;
     private WindowContainerToken mTaskToken;
@@ -89,11 +92,12 @@
     private final Rect mTmpRootRect = new Rect();
     private final int[] mTmpLocation = new int[2];
 
-    public TaskView(Context context, ShellTaskOrganizer organizer) {
+    public TaskView(Context context, ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) {
         super(context, null, 0, 0, true /* disableBackgroundLayer */);
 
         mTaskOrganizer = organizer;
         mShellExecutor = organizer.getExecutor();
+        mSyncQueue = syncQueue;
         setUseAlpha();
         getHolder().addCallback(this);
         mGuard.open("release");
@@ -189,8 +193,7 @@
 
         WindowContainerTransaction wct = new WindowContainerTransaction();
         wct.setBounds(mTaskToken, mTmpRect);
-        // TODO(b/151449487): Enable synchronization
-        mTaskOrganizer.applyTransaction(wct);
+        mSyncQueue.queue(wct);
     }
 
     /**
@@ -236,14 +239,16 @@
     private void updateTaskVisibility() {
         WindowContainerTransaction wct = new WindowContainerTransaction();
         wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */);
-        mTaskOrganizer.applyTransaction(wct);
-        // TODO(b/151449487): Only call callback once we enable synchronization
-        if (mListener != null) {
-            final int taskId = mTaskInfo.taskId;
+        mSyncQueue.queue(wct);
+        if (mListener == null) {
+            return;
+        }
+        int taskId = mTaskInfo.taskId;
+        mSyncQueue.runInSync((t) -> {
             mListenerExecutor.execute(() -> {
                 mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated);
             });
-        }
+        });
     }
 
     @Override
@@ -264,10 +269,12 @@
             updateTaskVisibility();
         }
         mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true);
-        // TODO: Synchronize show with the resize
         onLocationChanged();
         if (taskInfo.taskDescription != null) {
-            setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
+            int backgroundColor = taskInfo.taskDescription.getBackgroundColor();
+            mSyncQueue.runInSync((t) -> {
+                setResizeBackgroundColor(t, backgroundColor);
+            });
         }
 
         if (mListener != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java
index 58ca1fb..8286d10 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java
@@ -20,8 +20,8 @@
 import android.content.Context;
 
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -30,12 +30,14 @@
 public class TaskViewFactoryController {
     private final ShellTaskOrganizer mTaskOrganizer;
     private final ShellExecutor mShellExecutor;
+    private final SyncTransactionQueue mSyncQueue;
     private final TaskViewFactory mImpl = new TaskViewFactoryImpl();
 
     public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer,
-            ShellExecutor shellExecutor) {
+            ShellExecutor shellExecutor, SyncTransactionQueue syncQueue) {
         mTaskOrganizer = taskOrganizer;
         mShellExecutor = shellExecutor;
+        mSyncQueue = syncQueue;
     }
 
     public TaskViewFactory asTaskViewFactory() {
@@ -44,7 +46,7 @@
 
     /** Creates an {@link TaskView} */
     public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) {
-        TaskView taskView = new TaskView(context, mTaskOrganizer);
+        TaskView taskView = new TaskView(context, mTaskOrganizer, mSyncQueue);
         executor.execute(() -> {
             onCreate.accept(taskView);
         });
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
index 8aca01d..2aead93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
@@ -62,4 +62,10 @@
      */
     public static final Interpolator PANEL_CLOSE_ACCELERATED =
             new PathInterpolator(0.3f, 0, 0.5f, 1);
+
+    public static final PathInterpolator SLOWDOWN_INTERPOLATOR =
+            new PathInterpolator(0.5f, 1f, 0.5f, 1f);
+
+    public static final PathInterpolator DIM_INTERPOLATOR =
+            new PathInterpolator(.23f, .87f, .52f, -0.11f);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
index e6d088e..6a252e0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
 
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
@@ -26,7 +27,6 @@
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
 
 import android.app.ActivityManager;
-import android.graphics.Rect;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
@@ -39,9 +39,11 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.SurfaceUtils;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.common.split.SplitWindowManager;
 
 import java.io.PrintWriter;
 
@@ -67,13 +69,33 @@
     private final SyncTransactionQueue mSyncQueue;
     private final DisplayController mDisplayController;
     private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
     private SplitLayout mSplitLayout;
 
+    private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
+            new SplitWindowManager.ParentContainerCallbacks() {
+        @Override
+        public void attachToParentSurface(SurfaceControl.Builder b) {
+            b.setParent(mRootTaskLeash);
+        }
+
+        @Override
+        public void onLeashReady(SurfaceControl leash) {
+            mSyncQueue.runInSync(t -> t
+                    .show(leash)
+                    .setLayer(leash, SPLIT_DIVIDER_LAYER)
+                    .setPosition(leash,
+                            mSplitLayout.getDividerBounds().left,
+                            mSplitLayout.getDividerBounds().top));
+        }
+    };
+
     AppPair(AppPairsController controller) {
         mController = controller;
         mSyncQueue = controller.getSyncTransactionQueue();
         mDisplayController = controller.getDisplayController();
         mDisplayImeController = controller.getDisplayImeController();
+        mDisplayInsetsController = controller.getDisplayInsetsController();
     }
 
     int getRootTaskId() {
@@ -109,8 +131,8 @@
         mSplitLayout = new SplitLayout(TAG + "SplitDivider",
                 mDisplayController.getDisplayContext(mRootTaskInfo.displayId),
                 mRootTaskInfo.configuration, this /* layoutChangeListener */,
-                b -> b.setParent(mRootTaskLeash), mDisplayImeController,
-                mController.getTaskOrganizer());
+                mParentContainerCallbacks, mDisplayImeController, mController.getTaskOrganizer());
+        mDisplayInsetsController.addInsetsChangedListener(mRootTaskInfo.displayId, mSplitLayout);
 
         final WindowContainerToken token1 = task1.token;
         final WindowContainerToken token2 = task2.token;
@@ -176,21 +198,17 @@
         if (mTaskLeash1 == null || mTaskLeash2 == null) return;
 
         mSplitLayout.init();
-        final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
-        final Rect dividerBounds = mSplitLayout.getDividerBounds();
 
-        // TODO: Is there more we need to do here?
-        mSyncQueue.runInSync(t -> {
-            t.setLayer(dividerLeash, Integer.MAX_VALUE)
-                    .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x,
-                            mTaskInfo1.positionInParent.y)
-                    .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x,
-                            mTaskInfo2.positionInParent.y)
-                    .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
-                    .show(mRootTaskLeash)
-                    .show(mTaskLeash1)
-                    .show(mTaskLeash2);
-        });
+        mSyncQueue.runInSync(t -> t
+                .show(mRootTaskLeash)
+                .show(mTaskLeash1)
+                .show(mTaskLeash2)
+                .setPosition(mTaskLeash1,
+                        mTaskInfo1.positionInParent.x,
+                        mTaskInfo1.positionInParent.y)
+                .setPosition(mTaskLeash2,
+                        mTaskInfo2.positionInParent.x,
+                        mTaskInfo2.positionInParent.y));
     }
 
     @Override
@@ -214,7 +232,7 @@
 
             if (mSplitLayout != null
                     && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) {
-                onBoundsChanged(mSplitLayout);
+                onLayoutSizeChanged(mSplitLayout);
             }
         } else if (taskInfo.taskId == getTaskId1()) {
             mTaskInfo1 = taskInfo;
@@ -295,17 +313,30 @@
     }
 
     @Override
-    public void onBoundsChanging(SplitLayout layout) {
+    public void onLayoutPositionChanging(SplitLayout layout) {
         mSyncQueue.runInSync(t ->
                 layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2));
     }
 
     @Override
-    public void onBoundsChanged(SplitLayout layout) {
+    public void onLayoutSizeChanging(SplitLayout layout) {
+        mSyncQueue.runInSync(t ->
+                layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2));
+    }
+
+    @Override
+    public void onLayoutSizeChanged(SplitLayout layout) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         layout.applyTaskChanges(wct, mTaskInfo1, mTaskInfo2);
         mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t ->
                 layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2));
     }
+
+    @Override
+    public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, mTaskInfo1, mTaskInfo2);
+        mController.getTaskOrganizer().applyTransaction(wct);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java
index b159333..53234ab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java
@@ -29,6 +29,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 
@@ -50,14 +51,17 @@
     private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>();
     private final DisplayController mDisplayController;
     private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
 
     public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
             DisplayController displayController, ShellExecutor mainExecutor,
-            DisplayImeController displayImeController) {
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController) {
         mTaskOrganizer = organizer;
         mSyncQueue = syncQueue;
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
         mMainExecutor = mainExecutor;
     }
 
@@ -148,6 +152,10 @@
         return mDisplayImeController;
     }
 
+    DisplayInsetsController getDisplayInsetsController() {
+        return mDisplayInsetsController;
+    }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         final String childPrefix = innerPrefix + "  ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 9d65d28..8d43f13 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -121,7 +121,7 @@
     @Nullable
     private Icon mIcon;
     private boolean mIsBubble;
-    private boolean mIsVisuallyInterruptive;
+    private boolean mIsTextChanged;
     private boolean mIsClearable;
     private boolean mShouldSuppressNotificationDot;
     private boolean mShouldSuppressNotificationList;
@@ -342,12 +342,12 @@
     }
 
     /**
-     * Sets whether this bubble is considered visually interruptive. This method is purely for
+     * Sets whether this bubble is considered text changed. This method is purely for
      * testing.
      */
     @VisibleForTesting
-    void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) {
-        mIsVisuallyInterruptive = visuallyInterruptive;
+    void setTextChangedForTest(boolean textChanged) {
+        mIsTextChanged = textChanged;
     }
 
     /**
@@ -422,14 +422,6 @@
         }
     }
 
-    @Override
-    public void setExpandedContentAlpha(float alpha) {
-        if (mExpandedView != null) {
-            mExpandedView.setAlpha(alpha);
-            mExpandedView.setTaskViewAlpha(alpha);
-        }
-    }
-
     /**
      * Set visibility of bubble in the expanded state.
      *
@@ -462,7 +454,7 @@
         mFlyoutMessage = extractFlyoutMessage(entry);
         if (entry.getRanking() != null) {
             mShortcutInfo = entry.getRanking().getConversationShortcutInfo();
-            mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive();
+            mIsTextChanged = entry.getRanking().isTextChanged();
             if (entry.getRanking().getChannel() != null) {
                 mIsImportantConversation =
                         entry.getRanking().getChannel().isImportantConversation();
@@ -503,8 +495,8 @@
         return mIcon;
     }
 
-    boolean isVisuallyInterruptive() {
-        return mIsVisuallyInterruptive;
+    boolean isTextChanged() {
+        return mIsTextChanged;
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 09fcb86..b6d65be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -56,7 +56,6 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -70,6 +69,7 @@
 import android.util.SparseSetArray;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.window.WindowContainerTransaction;
 
@@ -85,6 +85,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
@@ -97,7 +98,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
@@ -137,6 +137,7 @@
     private final TaskStackListenerImpl mTaskStackListener;
     private final ShellTaskOrganizer mTaskOrganizer;
     private final DisplayController mDisplayController;
+    private final SyncTransactionQueue mSyncQueue;
 
     // Used to post to main UI thread
     private final ShellExecutor mMainExecutor;
@@ -144,7 +145,6 @@
 
     private BubbleLogger mLogger;
     private BubbleData mBubbleData;
-    private View mBubbleScrim;
     @Nullable private BubbleStackView mStackView;
     private BubbleIconFactory mBubbleIconFactory;
     private BubblePositioner mBubblePositioner;
@@ -189,6 +189,9 @@
     /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
     private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
 
+    /** Saved insets, used to detect WindowInset changes. */
+    private WindowInsets mWindowInsets;
+
     private boolean mInflateSynchronously;
 
     /** True when user is in status bar unlock shade. */
@@ -209,7 +212,8 @@
             ShellTaskOrganizer organizer,
             DisplayController displayController,
             ShellExecutor mainExecutor,
-            Handler mainHandler) {
+            Handler mainHandler,
+            SyncTransactionQueue syncQueue) {
         BubbleLogger logger = new BubbleLogger(uiEventLogger);
         BubblePositioner positioner = new BubblePositioner(context, windowManager);
         BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
@@ -217,7 +221,7 @@
                 new BubbleDataRepository(context, launcherApps, mainExecutor),
                 statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
                 logger, taskStackListener, organizer, positioner, displayController, mainExecutor,
-                mainHandler);
+                mainHandler, syncQueue);
     }
 
     /**
@@ -239,7 +243,8 @@
             BubblePositioner positioner,
             DisplayController displayController,
             ShellExecutor mainExecutor,
-            Handler mainHandler) {
+            Handler mainHandler,
+            SyncTransactionQueue syncQueue) {
         mContext = context;
         mLauncherApps = launcherApps;
         mBarService = statusBarService == null
@@ -262,6 +267,7 @@
         mSavedBubbleKeysPerUser = new SparseSetArray<>();
         mBubbleIconFactory = new BubbleIconFactory(context);
         mDisplayController = displayController;
+        mSyncQueue = syncQueue;
     }
 
     public void initialize() {
@@ -561,6 +567,10 @@
         return mTaskOrganizer;
     }
 
+    SyncTransactionQueue getSyncTransactionQueue() {
+        return mSyncQueue;
+    }
+
     /** Contains information to help position things on the screen.  */
     BubblePositioner getPositioner() {
         return mBubblePositioner;
@@ -572,7 +582,7 @@
 
     /**
      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
-     * method initializes the stack view and adds it to the StatusBar just above the scrim.
+     * method initializes the stack view and adds it to window manager.
      */
     private void ensureStackViewCreated() {
         if (mStackView == null) {
@@ -620,20 +630,31 @@
         try {
             mAddedToWindowManager = true;
             mBubbleData.getOverflow().initialize(this);
-            mStackView.addView(mBubbleScrim);
             mWindowManager.addView(mStackView, mWmLayoutParams);
-            // Position info is dependent on us being attached to a window
-            mBubblePositioner.update();
+            mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+                if (!windowInsets.equals(mWindowInsets)) {
+                    mWindowInsets = windowInsets;
+                    mBubblePositioner.update();
+                    mStackView.onDisplaySizeChanged();
+                }
+                return windowInsets;
+            });
         } catch (IllegalStateException e) {
             // This means the stack has already been added. This shouldn't happen...
             e.printStackTrace();
         }
     }
 
-    /** For the overflow to be focusable & receive key events the flags must be update. **/
-    void updateWindowFlagsForOverflow(boolean showingOverflow) {
+    /**
+     * In some situations bubble's should be able to receive key events for back:
+     * - when the bubble overflow is showing
+     * - when the user education for the stack is showing.
+     *
+     * @param interceptBack whether back should be intercepted or not.
+     */
+    void updateWindowFlagsForBackpress(boolean interceptBack) {
         if (mStackView != null && mAddedToWindowManager) {
-            mWmLayoutParams.flags = showingOverflow
+            mWmLayoutParams.flags = interceptBack
                     ? 0
                     : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                             | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
@@ -652,7 +673,6 @@
             mAddedToWindowManager = false;
             if (mStackView != null) {
                 mWindowManager.removeView(mStackView);
-                mStackView.removeView(mBubbleScrim);
                 mBubbleData.getOverflow().cleanUpExpandedState();
             } else {
                 Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
@@ -754,13 +774,6 @@
         }
     }
 
-    private void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) {
-        mBubbleScrim = view;
-        callback.accept(mMainExecutor, mMainExecutor.executeBlockingForResult(() -> {
-            return Looper.myLooper();
-        }, Looper.class));
-    }
-
     private void setSysuiProxy(Bubbles.SysuiProxy proxy) {
         mSysuiProxy = proxy;
     }
@@ -897,8 +910,7 @@
      * Fills the overflow bubbles by loading them from disk.
      */
     void loadOverflowBubblesFromDisk() {
-        if (!mBubbleData.getOverflowBubbles().isEmpty() && !mOverflowDataLoadNeeded) {
-            // we don't need to load overflow bubbles from disk if it is already in memory
+        if (!mOverflowDataLoadNeeded) {
             return;
         }
         mOverflowDataLoadNeeded = false;
@@ -927,7 +939,7 @@
     public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) {
         // If this is an interruptive notif, mark that it's interrupted
         mSysuiProxy.setNotificationInterruption(notif.getKey());
-        if (!notif.getRanking().visuallyInterruptive()
+        if (!notif.getRanking().isTextChanged()
                 && (notif.getBubbleMetadata() != null
                     && !notif.getBubbleMetadata().getAutoExpandBubble())
                 && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) {
@@ -1366,8 +1378,9 @@
     private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener {
         @Override
         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+            mBubblePositioner.setImeVisible(imeVisible, imeHeight);
             if (mStackView != null) {
-                mStackView.onImeVisibilityChanged(imeVisible, imeHeight);
+                mStackView.animateForIme(imeVisible);
             }
         }
     }
@@ -1566,13 +1579,6 @@
         }
 
         @Override
-        public void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) {
-            mMainExecutor.execute(() -> {
-                BubbleController.this.setBubbleScrim(view, callback);
-            });
-        }
-
-        @Override
         public void setExpandListener(BubbleExpandListener listener) {
             mMainExecutor.execute(() -> {
                 BubbleController.this.setExpandListener(listener);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index d73ce69..519a856 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -323,7 +323,7 @@
         }
         mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here
         Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
-        suppressFlyout |= !bubble.isVisuallyInterruptive();
+        suppressFlyout |= !bubble.isTextChanged();
 
         if (prevBubble == null) {
             // Create a new bubble
@@ -558,6 +558,8 @@
         }
         Bubble bubbleToRemove = mBubbles.get(indexToRemove);
         bubbleToRemove.stopInflation();
+        overflowBubble(reason, bubbleToRemove);
+
         if (mBubbles.size() == 1) {
             if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) {
                 // No more active bubbles but we have stuff in the overflow -- select that view
@@ -581,8 +583,6 @@
             mStateChange.orderChanged |= repackAll();
         }
 
-        overflowBubble(reason, bubbleToRemove);
-
         // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
         if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
             // Move selection to the new bubble at the same position.
@@ -699,10 +699,9 @@
         if (DEBUG_BUBBLE_DATA) {
             Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
         }
-        if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) {
+        if (Objects.equals(bubble, mSelectedBubble)) {
             return;
         }
-        // Otherwise, if we are showing the overflow menu, return to the previously selected bubble.
         boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey());
         if (bubble != null
                 && !mBubbles.contains(bubble)
@@ -771,6 +770,10 @@
                 Log.e(TAG, "Attempt to expand stack without selected bubble!");
                 return;
             }
+            if (mSelectedBubble.getKey().equals(mOverflow.getKey()) && !mBubbles.isEmpty()) {
+                // Show previously selected bubble instead of overflow menu when expanding.
+                setSelectedBubbleInternal(mBubbles.get(0));
+            }
             if (mSelectedBubble instanceof Bubble) {
                 ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
             }
@@ -779,16 +782,6 @@
             // Apply ordering and grouping rules from expanded -> collapsed, then save
             // the result.
             mStateChange.orderChanged |= repackAll();
-            // Save the state which should be returned to when expanded (with no other changes)
-
-            if (mShowingOverflow) {
-                // Show previously selected bubble instead of overflow menu on next expansion.
-                if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) {
-                    setSelectedBubbleInternal(mSelectedBubble);
-                } else {
-                    setSelectedBubbleInternal(mBubbles.get(0));
-                }
-            }
             if (mBubbles.indexOf(mSelectedBubble) > 0) {
                 // Move the selected bubble to the top while collapsed.
                 int index = mBubbles.indexOf(mSelectedBubble);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 9687ec6..a87aad4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -25,6 +25,7 @@
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
 
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
@@ -60,7 +61,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.launcher3.icons.IconNormalizer;
 import com.android.wm.shell.R;
 import com.android.wm.shell.TaskView;
 import com.android.wm.shell.common.AlphaOptimizedButton;
@@ -77,7 +77,6 @@
 
     // The triangle pointing to the expanded view
     private View mPointerView;
-    private int mPointerMargin;
     @Nullable private int[] mExpandedViewContainerLocation;
 
     private AlphaOptimizedButton mManageButton;
@@ -102,9 +101,6 @@
      */
     private boolean mIsAlphaAnimating = false;
 
-    private int mMinHeight;
-    private int mOverflowHeight;
-    private int mManageButtonHeight;
     private int mPointerWidth;
     private int mPointerHeight;
     private float mPointerRadius;
@@ -232,7 +228,7 @@
         @Override
         public void onBackPressedOnTaskRoot(int taskId) {
             if (mTaskId == taskId && mStackView.isExpanded()) {
-                mController.collapseStack();
+                mStackView.onBackPressed();
             }
         }
     };
@@ -338,7 +334,8 @@
             bringChildToFront(mOverflowView);
             mManageButton.setVisibility(GONE);
         } else {
-            mTaskView = new TaskView(mContext, mController.getTaskOrganizer());
+            mTaskView = new TaskView(mContext, mController.getTaskOrganizer(),
+                    mController.getSyncTransactionQueue());
             mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
             mExpandedViewContainer.addView(mTaskView);
             bringChildToFront(mTaskView);
@@ -347,12 +344,8 @@
 
     void updateDimensions() {
         Resources res = getResources();
-        mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
-        mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
-
         updateFontSize();
 
-        mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
         mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius);
@@ -368,7 +361,6 @@
             updatePointerView();
         }
 
-        mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_height);
         if (mManageButton != null) {
             int visibility = mManageButton.getVisibility();
             removeView(mManageButton);
@@ -406,6 +398,7 @@
         updatePointerView();
     }
 
+    /** Updates the size and visuals of the pointer. **/
     private void updatePointerView() {
         LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams();
         if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) {
@@ -532,9 +525,8 @@
         if (mTaskView != null) {
             mTaskView.setAlpha(alpha);
         }
-        if (mManageButton != null && mManageButton.getVisibility() == View.VISIBLE) {
-            mManageButton.setAlpha(alpha);
-        }
+        mPointerView.setAlpha(alpha);
+        setAlpha(alpha);
     }
 
     /**
@@ -553,6 +545,7 @@
         mIsContentVisible = visibility;
         if (mTaskView != null && !mIsAlphaAnimating) {
             mTaskView.setAlpha(visibility ? 1f : 0f);
+            mPointerView.setAlpha(visibility ? 1f : 0f);
         }
     }
 
@@ -632,12 +625,11 @@
         }
 
         if ((mBubble != null && mTaskView != null) || mIsOverflow) {
-            float desiredHeight = mIsOverflow
-                    ? mPositioner.isLargeScreen() ? getMaxExpandedHeight() : mOverflowHeight
-                    : mBubble.getDesiredHeight(mContext);
-            desiredHeight = Math.max(desiredHeight, mMinHeight);
-            float height = Math.min(desiredHeight, getMaxExpandedHeight());
-            height = Math.max(height, mMinHeight);
+            float desiredHeight = mPositioner.getExpandedViewHeight(mBubble);
+            int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow);
+            float height = desiredHeight == MAX_HEIGHT
+                    ? maxHeight
+                    : Math.min(desiredHeight, maxHeight);
             FrameLayout.LayoutParams lp = mIsOverflow
                     ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams()
                     : (FrameLayout.LayoutParams) mTaskView.getLayoutParams();
@@ -661,23 +653,6 @@
         }
     }
 
-    private int getMaxExpandedHeight() {
-        int expandedContainerY = mExpandedViewContainerLocation != null
-                // Remove top insets back here because availableRect.height would account for that
-                ? mExpandedViewContainerLocation[1] - mPositioner.getInsets().top
-                : 0;
-        int settingsHeight = mIsOverflow ? 0 : mManageButtonHeight;
-        int pointerHeight = mPositioner.showBubblesVertically()
-                ? mPointerWidth
-                : (int) (mPointerHeight - mPointerOverlap + mPointerMargin);
-        return mPositioner.getAvailableRect().height()
-                - expandedContainerY
-                - getPaddingTop()
-                - getPaddingBottom()
-                - settingsHeight
-                - pointerHeight;
-    }
-
     /**
      * Update appearance of the expanded view being displayed.
      *
@@ -715,28 +690,29 @@
      *                       the bubble if showing vertically.
      * @param onLeft whether the stack was on the left side of the screen when expanded.
      */
-    public void setPointerPosition(float bubblePosition, boolean onLeft) {
+    public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) {
         // Pointer gets drawn in the padding
         final boolean showVertically = mPositioner.showBubblesVertically();
         final float paddingLeft = (showVertically && onLeft)
                 ? mPointerHeight - mPointerOverlap
                 : 0;
         final float paddingRight = (showVertically && !onLeft)
-                ? mPointerHeight - mPointerOverlap : 0;
-        final float paddingTop = showVertically ? 0
+                ? mPointerHeight - mPointerOverlap
+                : 0;
+        final float paddingTop = showVertically
+                ? 0
                 : mPointerHeight - mPointerOverlap;
         setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0);
 
-        final float expandedViewY = mPositioner.getExpandedViewY();
-        // TODO: I don't understand why it works but it does - why normalized in portrait
-        //  & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
-        final float normalizedSize = IconNormalizer.getNormalizedCircleSize(
-                mPositioner.getBubbleSize());
-        final float bubbleCenter = showVertically
-                ? bubblePosition + (mPositioner.getBubbleSize() / 2f) - expandedViewY
-                : bubblePosition + (normalizedSize / 2f) - mPointerWidth;
+        // Subtract the expandedViewY here because the pointer is placed within the expandedView.
+        float pointerPosition = mPositioner.getPointerPosition(bubblePosition);
+        final float bubbleCenter = mPositioner.showBubblesVertically()
+                ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition)
+                : pointerPosition;
         // Post because we need the width of the view
         post(() -> {
+            mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
+            updatePointerView();
             float pointerY;
             float pointerX;
             if (showVertically) {
@@ -748,11 +724,13 @@
                 pointerY = mPointerOverlap;
                 pointerX = bubbleCenter - (mPointerWidth / 2f);
             }
-            mPointerView.setTranslationY(pointerY);
-            mPointerView.setTranslationX(pointerX);
-            mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
-            updatePointerView();
-            mPointerView.setVisibility(VISIBLE);
+            if (animate) {
+                mPointerView.animate().translationX(pointerX).translationY(pointerY).start();
+            } else {
+                mPointerView.setTranslationY(pointerY);
+                mPointerView.setTranslationX(pointerX);
+                mPointerView.setVisibility(VISIBLE);
+            }
         });
     }
 
@@ -764,6 +742,10 @@
         mManageButton.getBoundsOnScreen(rect);
     }
 
+    public int getManageButtonMargin() {
+        return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
+    }
+
     /**
      * Cleans up anything related to the task and {@code TaskView}. If this view should be reused
      * after this method is called, then
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
index 35a4f33..9374da4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
@@ -56,9 +56,6 @@
  * transform into the 'new' dot, which is used during flyout dismiss animations/gestures.
  */
 public class BubbleFlyoutView extends FrameLayout {
-    /** Max width of the flyout, in terms of percent of the screen width. */
-    private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f;
-
     /** Translation Y of fade animation. */
     private static final float FLYOUT_FADE_Y = 40f;
 
@@ -68,6 +65,8 @@
     // Whether the flyout view should show a pointer to the bubble.
     private static final boolean SHOW_POINTER = false;
 
+    private BubblePositioner mPositioner;
+
     private final int mFlyoutPadding;
     private final int mFlyoutSpaceFromBubble;
     private final int mPointerSize;
@@ -156,10 +155,11 @@
     /** Callback to run when the flyout is hidden. */
     @Nullable private Runnable mOnHide;
 
-    public BubbleFlyoutView(Context context) {
+    public BubbleFlyoutView(Context context, BubblePositioner positioner) {
         super(context);
-        LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true);
+        mPositioner = positioner;
 
+        LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true);
         mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container);
         mSenderText = findViewById(R.id.bubble_flyout_name);
         mSenderAvatar = findViewById(R.id.bubble_flyout_avatar);
@@ -230,11 +230,11 @@
     /*
      * Fade animation for consecutive flyouts.
      */
-    void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, PointF stackPos,
+    void animateUpdate(Bubble.FlyoutMessage flyoutMessage, PointF stackPos,
             boolean hideDot, Runnable onHide) {
         mOnHide = onHide;
         final Runnable afterFadeOut = () -> {
-            updateFlyoutMessage(flyoutMessage, parentWidth);
+            updateFlyoutMessage(flyoutMessage);
             // Wait for TextViews to layout with updated height.
             post(() -> {
                 fade(true /* in */, stackPos, hideDot, () -> {} /* after */);
@@ -266,7 +266,7 @@
                 .withEndAction(afterFade);
     }
 
-    private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) {
+    private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage) {
         final Drawable senderAvatar = flyoutMessage.senderAvatar;
         if (senderAvatar != null && flyoutMessage.isGroupChat) {
             mSenderAvatar.setVisibility(VISIBLE);
@@ -278,8 +278,7 @@
             mSenderText.setTranslationX(0);
         }
 
-        final int maxTextViewWidth =
-                (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2;
+        final int maxTextViewWidth = (int) mPositioner.getMaxFlyoutSize() - mFlyoutPadding * 2;
 
         // Name visibility
         if (!TextUtils.isEmpty(flyoutMessage.senderName)) {
@@ -328,22 +327,20 @@
     void setupFlyoutStartingAsDot(
             Bubble.FlyoutMessage flyoutMessage,
             PointF stackPos,
-            float parentWidth,
             boolean arrowPointingLeft,
             int dotColor,
             @Nullable Runnable onLayoutComplete,
             @Nullable Runnable onHide,
             float[] dotCenter,
-            boolean hideDot,
-            BubblePositioner positioner)  {
+            boolean hideDot)  {
 
-        mBubbleSize = positioner.getBubbleSize();
+        mBubbleSize = mPositioner.getBubbleSize();
 
         mOriginalDotSize = SIZE_PERCENTAGE * mBubbleSize;
         mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f;
         mNewDotSize = mNewDotRadius * 2f;
 
-        updateFlyoutMessage(flyoutMessage, parentWidth);
+        updateFlyoutMessage(flyoutMessage);
 
         mArrowPointingLeft = arrowPointingLeft;
         mDotColor = dotColor;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
index 705a12a..0c3a6b2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
@@ -154,10 +154,6 @@
         return dotPath
     }
 
-    override fun setExpandedContentAlpha(alpha: Float) {
-        expandedView?.alpha = alpha
-    }
-
     override fun setTaskViewVisibility(visible: Boolean) {
         // Overflow does not have a TaskView.
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
index ede4228..5e9d97f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
@@ -142,7 +142,7 @@
         super.onAttachedToWindow();
         if (mController != null) {
             // For the overflow to get key events (e.g. back press) we need to adjust the flags
-            mController.updateWindowFlagsForOverflow(true);
+            mController.updateWindowFlagsForBackpress(true);
         }
         setOnKeyListener(mKeyListener);
     }
@@ -151,7 +151,7 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         if (mController != null) {
-            mController.updateWindowFlagsForOverflow(false);
+            mController.updateWindowFlagsForBackpress(false);
         }
         setOnKeyListener(null);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index c600f56..127d5a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -34,6 +34,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.icons.IconNormalizer;
 import com.android.wm.shell.R;
 
 import java.lang.annotation.Retention;
@@ -58,29 +59,48 @@
 
     /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/
     public static final int NUM_VISIBLE_WHEN_RESTING = 2;
+    /** Indicates a bubble's height should be the maximum available space. **/
+    public static final int MAX_HEIGHT = -1;
+    /** The max percent of screen width to use for the flyout on large screens. */
+    public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f;
+    /** The max percent of screen width to use for the flyout on phone. */
+    public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f;
+    /** The percent of screen width that should be used for the expanded view on a large screen. **/
+    public static final float EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT = 0.72f;
 
     private Context mContext;
     private WindowManager mWindowManager;
-    private Rect mPositionRect;
+    private Rect mScreenRect;
     private @Surface.Rotation int mRotation = Surface.ROTATION_0;
     private Insets mInsets;
+    private boolean mImeVisible;
+    private int mImeHeight;
+    private boolean mIsLargeScreen;
+
+    private Rect mPositionRect;
     private int mDefaultMaxBubbles;
     private int mMaxBubbles;
-
     private int mBubbleSize;
-    private int mBubbleBadgeSize;
     private int mSpacingBetweenBubbles;
+
+    private int mExpandedViewMinHeight;
     private int mExpandedViewLargeScreenWidth;
+    private int mExpandedViewLargeScreenInset;
+
+    private int mOverflowWidth;
     private int mExpandedViewPadding;
     private int mPointerMargin;
-    private float mPointerWidth;
-    private float mPointerHeight;
+    private int mPointerWidth;
+    private int mPointerHeight;
+    private int mPointerOverlap;
+    private int mManageButtonHeight;
+    private int mOverflowHeight;
+    private int mMinimumFlyoutWidthLargeScreen;
 
     private PointF mPinLocation;
     private PointF mRestingStackPosition;
     private int[] mPaddings = new int[4];
 
-    private boolean mIsLargeScreen;
     private boolean mShowingInTaskbar;
     private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE;
     private int mTaskbarIconSize;
@@ -143,6 +163,7 @@
         mRotation = rotation;
         mInsets = insets;
 
+        mScreenRect = new Rect(bounds);
         mPositionRect = new Rect(bounds);
         mPositionRect.left += mInsets.left;
         mPositionRect.top += mInsets.top;
@@ -151,16 +172,27 @@
 
         Resources res = mContext.getResources();
         mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
-        mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size);
         mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
         mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
-
-        mExpandedViewLargeScreenWidth = res.getDimensionPixelSize(
-                R.dimen.bubble_expanded_view_tablet_width);
         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
+        mExpandedViewLargeScreenWidth = (int) (bounds.width()
+                * EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT);
+        mExpandedViewLargeScreenInset = mIsLargeScreen
+                ? (bounds.width() - mExpandedViewLargeScreenWidth) / 2
+                : mExpandedViewPadding;
+        mOverflowWidth = mIsLargeScreen
+                ? mExpandedViewLargeScreenWidth
+                : res.getDimensionPixelSize(
+                        R.dimen.bubble_expanded_view_phone_landscape_overflow_width);
         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
+        mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
+        mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height);
+        mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
+        mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
+        mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize(
+                R.dimen.bubbles_flyout_min_width_large_screen);
 
         mMaxBubbles = calculateMaxBubbles();
 
@@ -225,6 +257,13 @@
     }
 
     /**
+     * @return a rect of the screen size.
+     */
+    public Rect getScreenRect() {
+        return mScreenRect;
+    }
+
+    /**
      * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its
      * inset is not included here.
      */
@@ -265,48 +304,287 @@
         return mMaxBubbles;
     }
 
-    /**
-     * Calculates the left & right padding for the bubble expanded view.
-     *
-     * On larger screens the width of the expanded view is restricted via this padding.
-     * On landscape the bubble overflow expanded view is also restricted via this padding.
-     */
-    public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) {
-        int leftPadding = mInsets.left + mExpandedViewPadding;
-        int rightPadding = mInsets.right + mExpandedViewPadding;
-        final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow;
-        if (showBubblesVertically()) {
-            if (!onLeft) {
-                rightPadding += mBubbleSize - mPointerHeight;
-                leftPadding += isLargeOrOverflow
-                        ? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth)
-                        : 0;
-            } else {
-                leftPadding += mBubbleSize - mPointerHeight;
-                rightPadding += isLargeOrOverflow
-                        ? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth)
-                        : 0;
-            }
-        }
-        // [left, top, right, bottom]
-        mPaddings[0] = leftPadding;
-        mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
-        mPaddings[2] = rightPadding;
-        mPaddings[3] = 0;
-        return mPaddings;
+    /** The height for the IME if it's visible. **/
+    public int getImeHeight() {
+        return mImeVisible ? mImeHeight : 0;
     }
 
-    /** Calculates the y position of the expanded view when it is expanded. */
-    public float getExpandedViewY() {
+    /** Sets whether the IME is visible. **/
+    public void setImeVisible(boolean visible, int height) {
+        mImeVisible = visible;
+        mImeHeight = height;
+    }
+
+    /**
+     * Calculates the padding for the bubble expanded view.
+     *
+     * Some specifics:
+     * On large screens the width of the expanded view is restricted via this padding.
+     * On phone landscape the bubble overflow expanded view is also restricted via this padding.
+     * On large screens & landscape no top padding is set, the top position is set via translation.
+     * On phone portrait top padding is set as the space between the tip of the pointer and the
+     * bubble.
+     * When the overflow is shown it doesn't have the manage button to pad out the bottom so
+     * padding is added.
+     */
+    public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) {
+        final int pointerTotalHeight = mPointerHeight - mPointerOverlap;
+        if (mIsLargeScreen) {
+            // [left, top, right, bottom]
+            mPaddings[0] = onLeft
+                    ? mExpandedViewLargeScreenInset - pointerTotalHeight
+                    : mExpandedViewLargeScreenInset;
+            mPaddings[1] = 0;
+            mPaddings[2] = onLeft
+                    ? mExpandedViewLargeScreenInset
+                    : mExpandedViewLargeScreenInset - pointerTotalHeight;
+            // Overflow doesn't show manage button / get padding from it so add padding here for it
+            mPaddings[3] = isOverflow ? mExpandedViewPadding : 0;
+            return mPaddings;
+        } else {
+            int leftPadding = mInsets.left + mExpandedViewPadding;
+            int rightPadding = mInsets.right + mExpandedViewPadding;
+            final float expandedViewWidth = isOverflow
+                    ? mOverflowWidth
+                    : mExpandedViewLargeScreenWidth;
+            if (showBubblesVertically()) {
+                if (!onLeft) {
+                    rightPadding += mBubbleSize - pointerTotalHeight;
+                    leftPadding += isOverflow
+                            ? (mPositionRect.width() - rightPadding - expandedViewWidth)
+                            : 0;
+                } else {
+                    leftPadding += mBubbleSize - pointerTotalHeight;
+                    rightPadding += isOverflow
+                            ? (mPositionRect.width() - leftPadding - expandedViewWidth)
+                            : 0;
+                }
+            }
+            // [left, top, right, bottom]
+            mPaddings[0] = leftPadding;
+            mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
+            mPaddings[2] = rightPadding;
+            mPaddings[3] = 0;
+            return mPaddings;
+        }
+    }
+
+    /** Gets the y position of the expanded view if it was top-aligned. */
+    public float getExpandedViewYTopAligned() {
         final int top = getAvailableRect().top;
         if (showBubblesVertically()) {
-            return top - mPointerWidth;
+            return top - mPointerWidth + mExpandedViewPadding;
         } else {
             return top + mBubbleSize + mPointerMargin;
         }
     }
 
     /**
+     * Calculate the maximum height the expanded view can be depending on where it's placed on
+     * the screen and the size of the elements around it (e.g. padding, pointer, manage button).
+     */
+    public int getMaxExpandedViewHeight(boolean isOverflow) {
+        // Subtract top insets because availableRect.height would account for that
+        int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top;
+        int paddingTop = showBubblesVertically()
+                ? 0
+                : mPointerHeight;
+        // Subtract pointer size because it's laid out in LinearLayout with the expanded view.
+        int pointerSize = showBubblesVertically()
+                ? mPointerWidth
+                : (mPointerHeight + mPointerMargin);
+        int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
+        return getAvailableRect().height()
+                - expandedContainerY
+                - paddingTop
+                - pointerSize
+                - bottomPadding;
+    }
+
+    /**
+     * Determines the height for the bubble, ensuring a minimum height. If the height should be as
+     * big as available, returns {@link #MAX_HEIGHT}.
+     */
+    public float getExpandedViewHeight(BubbleViewProvider bubble) {
+        boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
+        if (isOverflow && showBubblesVertically() && !mIsLargeScreen) {
+            // overflow in landscape on phone is max
+            return MAX_HEIGHT;
+        }
+        float desiredHeight = isOverflow
+                ? mOverflowHeight
+                : ((Bubble) bubble).getDesiredHeight(mContext);
+        desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight);
+        if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) {
+            return MAX_HEIGHT;
+        }
+        return desiredHeight;
+    }
+
+    /**
+     * Gets the y position for the expanded view. This is the position on screen of the top
+     * horizontal line of the expanded view.
+     *
+     * @param bubble the bubble being positioned.
+     * @param bubblePosition the x position of the bubble if showing on top, the y position of the
+     *                       bubble if showing vertically.
+     * @return the y position for the expanded view.
+     */
+    public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) {
+        boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
+        float expandedViewHeight = getExpandedViewHeight(bubble);
+        float topAlignment = getExpandedViewYTopAligned();
+        if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) {
+            // Top-align when bubbles are shown at the top or are max size.
+            return topAlignment;
+        }
+        // If we're here, we're showing vertically & developer has made height less than maximum.
+        int manageButtonHeight = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
+        float pointerPosition = getPointerPosition(bubblePosition);
+        float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight;
+        float topIfCentered = pointerPosition - (expandedViewHeight / 2);
+        if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) {
+            // Center it
+            return pointerPosition - mPointerWidth - (expandedViewHeight / 2f);
+        } else if (topIfCentered <= mPositionRect.top) {
+            // Top align
+            return topAlignment;
+        } else {
+            // Bottom align
+            return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth;
+        }
+    }
+
+    /**
+     * The position the pointer points to, the center of the bubble.
+     *
+     * @param bubblePosition the x position of the bubble if showing on top, the y position of the
+     *                       bubble if showing vertically.
+     * @return the position the tip of the pointer points to. The x position if showing on top, the
+     * y position if showing vertically.
+     */
+    public float getPointerPosition(float bubblePosition) {
+        // TODO: I don't understand why it works but it does - why normalized in portrait
+        //  & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
+        final float normalizedSize = IconNormalizer.getNormalizedCircleSize(
+                getBubbleSize());
+        return showBubblesVertically()
+                ? bubblePosition + (getBubbleSize() / 2f)
+                : bubblePosition + (normalizedSize / 2f) - mPointerWidth;
+    }
+
+    private int getExpandedStackSize(int numberOfBubbles) {
+        return (numberOfBubbles * mBubbleSize)
+                + ((numberOfBubbles - 1) * mSpacingBetweenBubbles);
+    }
+
+    /**
+     * Returns the position of the bubble on-screen when the stack is expanded.
+     *
+     * @param index the index of the bubble in the stack.
+     * @param state state information about the stack to help with calculations.
+     * @return the position of the bubble on-screen when the stack is expanded.
+     */
+    public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) {
+        final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles);
+        final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles);
+        final float centerPosition = showBubblesVertically()
+                ? mPositionRect.centerY()
+                : mPositionRect.centerX();
+        // alignment - centered on the edge
+        final float rowStart = centerPosition - (expandedStackSize / 2f);
+        float x;
+        float y;
+        if (showBubblesVertically()) {
+            y = rowStart + positionInRow;
+            int left = mIsLargeScreen
+                    ? mExpandedViewLargeScreenInset - mExpandedViewPadding - mBubbleSize
+                    : mPositionRect.left;
+            int right = mIsLargeScreen
+                    ? mPositionRect.right - mExpandedViewLargeScreenInset + mExpandedViewPadding
+                    : mPositionRect.right - mBubbleSize;
+            x = state.onLeft
+                    ? left
+                    : right;
+        } else {
+            y = mPositionRect.top + mExpandedViewPadding;
+            x = rowStart + positionInRow;
+        }
+
+        if (showBubblesVertically() && mImeVisible) {
+            return new PointF(x, getExpandedBubbleYForIme(index, state.numberOfBubbles));
+        }
+        return new PointF(x, y);
+    }
+
+    /**
+     * Returns the position of the bubble on-screen when the stack is expanded and the IME
+     * is showing.
+     *
+     * @param index the index of the bubble in the stack.
+     * @param numberOfBubbles the total number of bubbles in the stack.
+     * @return y position of the bubble on-screen when the stack is expanded.
+     */
+    private float getExpandedBubbleYForIme(int index, int numberOfBubbles) {
+        final float top = getAvailableRect().top + mExpandedViewPadding;
+        if (!showBubblesVertically()) {
+            // Showing horizontally: align to top
+            return top;
+        }
+
+        // Showing vertically: might need to translate the bubbles above the IME.
+        // Subtract spacing here to provide a margin between top of IME and bottom of bubble row.
+        final float bottomInset = getImeHeight() + mInsets.bottom - (mSpacingBetweenBubbles * 2);
+        final float expandedStackSize = getExpandedStackSize(numberOfBubbles);
+        final float centerPosition = showBubblesVertically()
+                ? mPositionRect.centerY()
+                : mPositionRect.centerX();
+        final float rowBottom = centerPosition + (expandedStackSize / 2f);
+        final float rowTop = centerPosition - (expandedStackSize / 2f);
+        float rowTopForIme = rowTop;
+        if (rowBottom > bottomInset) {
+            // We overlap with IME, must shift the bubbles
+            float translationY = rowBottom - bottomInset;
+            rowTopForIme = Math.max(rowTop - translationY, top);
+            if (rowTop - translationY < top) {
+                // Even if we shift the bubbles, they will still overlap with the IME.
+                // Hide the overflow for a lil more space:
+                final float expandedStackSizeNoO = getExpandedStackSize(numberOfBubbles - 1);
+                final float centerPositionNoO = showBubblesVertically()
+                        ? mPositionRect.centerY()
+                        : mPositionRect.centerX();
+                final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f);
+                final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f);
+                translationY = rowBottomNoO - bottomInset;
+                rowTopForIme = rowTopNoO - translationY;
+            }
+        }
+        return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles));
+    }
+
+    /**
+     * @return the width of the bubble flyout (message originating from the bubble).
+     */
+    public float getMaxFlyoutSize() {
+        if (isLargeScreen()) {
+            return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN,
+                    mMinimumFlyoutWidthLargeScreen);
+        }
+        return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT;
+    }
+
+    /**
+     * @return whether the stack is considered on the left side of the screen.
+     */
+    public boolean isStackOnLeft(PointF currentStackPosition) {
+        if (currentStackPosition == null) {
+            currentStackPosition = getRestingPosition();
+        }
+        final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2;
+        return stackCenter < mScreenRect.width() / 2;
+    }
+
+    /**
      * Sets the stack's most recent position along the edge of the screen. This is saved when the
      * last bubble is removed, so that the stack can be restored in its previous position.
      */
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 ac97c8f..300319a 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
@@ -19,6 +19,8 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
+import static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
+import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -26,6 +28,8 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.SuppressLint;
 import android.content.ContentResolver;
@@ -33,11 +37,11 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
-import android.graphics.Color;
 import android.graphics.Outline;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.util.Log;
@@ -106,14 +110,8 @@
      */
     private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
 
-    /** Duration of the flyout alpha animations. */
-    private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
-
     private static final int FADE_IN_DURATION = 320;
 
-    /** Percent to darken the bubbles when they're in the dismiss target. */
-    private static final float DARKEN_PERCENT = 0.3f;
-
     /** How long to wait, in milliseconds, before hiding the flyout. */
     @VisibleForTesting
     static final int FLYOUT_HIDE_AFTER = 5000;
@@ -122,6 +120,8 @@
 
     private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
 
+    private static final float SCRIM_ALPHA = 0.6f;
+
     /**
      * How long to wait to animate the stack temporarily invisible after a drag/flyout hide
      * animation ends, if we are in fact temporarily invisible.
@@ -188,6 +188,7 @@
     };
     private final BubbleController mBubbleController;
     private final BubbleData mBubbleData;
+    private StackViewState mStackViewState = new StackViewState();
 
     private final ValueAnimator mDismissBubbleAnimator;
 
@@ -195,7 +196,8 @@
     private StackAnimationController mStackAnimationController;
     private ExpandedAnimationController mExpandedAnimationController;
 
-    private View mTaskbarScrim;
+    private View mScrim;
+    private View mManageMenuScrim;
     private FrameLayout mExpandedViewContainer;
 
     /** Matrix used to scale the expanded view container with a given pivot point. */
@@ -245,7 +247,6 @@
     private int mBubbleTouchPadding;
     private int mExpandedViewPadding;
     private int mCornerRadius;
-    private int mImeOffset;
     @Nullable private BubbleViewProvider mExpandedBubble;
     private boolean mIsExpanded;
 
@@ -555,7 +556,7 @@
 
             if (mBubbleData.isExpanded()) {
                 if (mManageEduView != null) {
-                    mManageEduView.hide(false /* show */);
+                    mManageEduView.hide();
                 }
 
                 // If we're expanded, tell the animation controller to prepare to drag this bubble,
@@ -756,7 +757,6 @@
         mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
         mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
         mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
-        mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
 
         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
         int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
@@ -777,8 +777,8 @@
                 floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut,
                 this::animateShadows /* onStackAnimationFinished */, mPositioner);
 
-        mExpandedAnimationController = new ExpandedAnimationController(
-                mPositioner, mExpandedViewPadding, onBubbleAnimatedOut);
+        mExpandedAnimationController = new ExpandedAnimationController(mPositioner,
+                onBubbleAnimatedOut, this);
         mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
 
         // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or
@@ -793,8 +793,6 @@
         mBubbleContainer.setClipChildren(false);
         addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
 
-        updateUserEdu();
-
         mExpandedViewContainer = new FrameLayout(context);
         mExpandedViewContainer.setElevation(elevation);
         mExpandedViewContainer.setClipChildren(false);
@@ -858,11 +856,20 @@
             mBubbleData.setExpanded(true);
         });
 
-        mTaskbarScrim = new View(getContext());
-        mTaskbarScrim.setBackgroundColor(Color.BLACK);
-        addView(mTaskbarScrim);
-        mTaskbarScrim.setAlpha(0f);
-        mTaskbarScrim.setVisibility(GONE);
+        mScrim = new View(getContext());
+        mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+        mScrim.setBackgroundDrawable(new ColorDrawable(
+                getResources().getColor(android.R.color.system_neutral1_1000)));
+        addView(mScrim);
+        mScrim.setAlpha(0f);
+
+        mManageMenuScrim = new View(getContext());
+        mManageMenuScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+        mManageMenuScrim.setBackgroundDrawable(new ColorDrawable(
+                getResources().getColor(android.R.color.system_neutral1_1000)));
+        addView(mManageMenuScrim, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+        mManageMenuScrim.setAlpha(0f);
+        mManageMenuScrim.setVisibility(INVISIBLE);
 
         mOrientationChangedListener =
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -882,12 +889,15 @@
                         // Re-draw bubble row and pointer for new orientation.
                         beforeExpandedViewAnimation();
                         updateOverflowVisibility();
-                        updatePointerPosition();
+                        updatePointerPosition(false /* forIme */);
                         mExpandedAnimationController.expandFromStack(() -> {
                             afterExpandedViewAnimation();
+                            showManageMenu(mShowingManage);
                         } /* after */);
+                        final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
+                                getBubbleIndex(mExpandedBubble));
                         mExpandedViewContainer.setTranslationX(0f);
-                        mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY());
+                        mExpandedViewContainer.setTranslationY(translationY);
                         mExpandedViewContainer.setAlpha(1f);
                     }
                     removeOnLayoutChangeListener(mOrientationChangedListener);
@@ -917,8 +927,10 @@
         setOnClickListener(view -> {
             if (mShowingManage) {
                 showManageMenu(false /* show */);
+            } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+                mManageEduView.hide();
             } else if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) {
-                mStackEduView.hide(false);
+                mStackEduView.hide(false /* isExpanding */);
             } else if (mBubbleData.isExpanded()) {
                 mBubbleData.setExpanded(false);
             }
@@ -955,8 +967,9 @@
             }
         });
         mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
-            if (mExpandedBubble != null) {
-                mExpandedBubble.setExpandedContentAlpha((float) valueAnimator.getAnimatedValue());
+            if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+                mExpandedBubble.getExpandedView().setTaskViewAlpha(
+                        (float) valueAnimator.getAnimatedValue());
             }
         });
 
@@ -1117,10 +1130,10 @@
             return;
         }
         if (mManageEduView == null) {
-            mManageEduView = new ManageEducationView(mContext);
+            mManageEduView = new ManageEducationView(mContext, mPositioner);
             addView(mManageEduView);
         }
-        mManageEduView.show(mExpandedBubble.getExpandedView(), mTempRect);
+        mManageEduView.show(mExpandedBubble.getExpandedView());
     }
 
     /**
@@ -1148,21 +1161,27 @@
             return false;
         }
         if (mStackEduView == null) {
-            mStackEduView = new StackEducationView(mContext);
+            mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
             addView(mStackEduView);
         }
         mBubbleContainer.bringToFront();
         return mStackEduView.show(mPositioner.getDefaultStartPosition());
     }
 
+    // Recreates & shows the education views. Call when a theme/config change happens.
     private void updateUserEdu() {
-        maybeShowStackEdu();
-        if (mManageEduView != null) {
-            mManageEduView.invalidate();
+        if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) {
+            removeView(mStackEduView);
+            mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+            addView(mStackEduView);
+            mBubbleContainer.bringToFront(); // Stack appears on top of the stack education
+            mStackEduView.show(mPositioner.getDefaultStartPosition());
         }
-        maybeShowManageEdu();
-        if (mStackEduView != null) {
-            mStackEduView.invalidate();
+        if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+            removeView(mManageEduView);
+            mManageEduView = new ManageEducationView(mContext, mPositioner);
+            addView(mManageEduView);
+            mManageEduView.show(mExpandedBubble.getExpandedView());
         }
     }
 
@@ -1171,7 +1190,7 @@
         if (mFlyout != null) {
             removeView(mFlyout);
         }
-        mFlyout = new BubbleFlyoutView(getContext());
+        mFlyout = new BubbleFlyoutView(getContext(), mPositioner);
         mFlyout.setVisibility(GONE);
         mFlyout.setOnClickListener(mFlyoutClickListener);
         mFlyout.setOnTouchListener(mFlyoutTouchListener);
@@ -1218,6 +1237,10 @@
         updateOverflow();
         updateUserEdu();
         updateExpandedViewTheme();
+        mScrim.setBackgroundDrawable(new ColorDrawable(
+                getResources().getColor(android.R.color.system_neutral1_1000)));
+        mManageMenuScrim.setBackgroundDrawable(new ColorDrawable(
+                getResources().getColor(android.R.color.system_neutral1_1000)));
     }
 
     /**
@@ -1229,9 +1252,6 @@
         mRelativeStackPositionBeforeRotation = new RelativeStackPosition(
                 mPositioner.getRestingPosition(),
                 mStackAnimationController.getAllowableStackPositionRegion());
-        mManageMenu.setVisibility(View.INVISIBLE);
-        mShowingManage = false;
-
         addOnLayoutChangeListener(mOrientationChangedListener);
         hideFlyoutImmediate();
     }
@@ -1255,6 +1275,7 @@
         setUpManageMenu();
         setUpFlyout();
         setUpDismissView();
+        updateUserEdu();
         mBubbleSize = mPositioner.getBubbleSize();
         for (Bubble b : mBubbleData.getBubbles()) {
             if (b.getIconView() == null) {
@@ -1292,6 +1313,7 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
+        mPositioner.update();
         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
         getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
     }
@@ -1534,7 +1556,8 @@
                 } else {
                     bubble.cleanupViews();
                 }
-                updatePointerPosition();
+                updatePointerPosition(false /* forIme */);
+                updateExpandedView();
                 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
                 return;
             }
@@ -1574,7 +1597,7 @@
                     .map(b -> b.getIconView()).collect(Collectors.toList());
             mStackAnimationController.animateReorder(bubbleViews, reorder);
         }
-        updatePointerPosition();
+        updatePointerPosition(false /* forIme */);
     }
 
     /**
@@ -1645,7 +1668,6 @@
     private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) {
         final BubbleViewProvider previouslySelected = mExpandedBubble;
         mExpandedBubble = bubbleToSelect;
-        updatePointerPosition();
 
         if (mIsExpanded) {
             hideCurrentInputMethod();
@@ -1710,6 +1732,21 @@
         notifyExpansionChanged(mExpandedBubble, mIsExpanded);
     }
 
+    /**
+     * Called when back press occurs while bubbles are expanded.
+     */
+    public void onBackPressed() {
+        if (mIsExpanded) {
+            if (mShowingManage) {
+                showManageMenu(false);
+            } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+                mManageEduView.hide();
+            } else {
+                setExpanded(false);
+            }
+        }
+    }
+
     void setBubbleVisibility(Bubble b, boolean visible) {
         if (b.getIconView() != null) {
             b.getIconView().setVisibility(visible ? VISIBLE : GONE);
@@ -1722,6 +1759,7 @@
      * not.
      */
     void hideCurrentInputMethod() {
+        mPositioner.setImeVisible(false, 0);
         mBubbleController.hideCurrentInputMethod();
     }
 
@@ -1796,6 +1834,20 @@
         mExpandedViewAlphaAnimator.start();
     }
 
+    private void showScrim(boolean show) {
+        if (show) {
+            mScrim.animate()
+                    .setInterpolator(ALPHA_IN)
+                    .alpha(SCRIM_ALPHA)
+                    .start();
+        } else {
+            mScrim.animate()
+                    .alpha(0f)
+                    .setInterpolator(ALPHA_OUT)
+                    .start();
+        }
+    }
+
     private void animateExpansion() {
         cancelDelayedExpandCollapseSwitchAnimations();
         final boolean showVertically = mPositioner.showBubblesVertically();
@@ -1805,47 +1857,38 @@
         }
         beforeExpandedViewAnimation();
 
+        showScrim(true);
         updateZOrder();
         updateBadges(false /* setBadgeForCollapsedStack */);
         mBubbleContainer.setActiveController(mExpandedAnimationController);
         updateOverflowVisibility();
-        updatePointerPosition();
+        updatePointerPosition(false /* forIme */);
         mExpandedAnimationController.expandFromStack(() -> {
             if (mIsExpanded && mExpandedBubble.getExpandedView() != null) {
                 maybeShowManageEdu();
             }
         } /* after */);
-
-        if (mPositioner.showingInTaskbar()
-                // Don't need the scrim when the bar is at the bottom
-                && mPositioner.getTaskbarPosition() != BubblePositioner.TASKBAR_POSITION_BOTTOM) {
-            mTaskbarScrim.getLayoutParams().width = mPositioner.getTaskbarSize();
-            mTaskbarScrim.setTranslationX(mStackOnLeftOrWillBe
-                    ? 0f
-                    : mPositioner.getAvailableRect().right - mPositioner.getTaskbarSize());
-            mTaskbarScrim.setVisibility(VISIBLE);
-            mTaskbarScrim.animate().alpha(1f).start();
-        }
-
-        mExpandedViewContainer.setTranslationX(0f);
-        mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY());
-        mExpandedViewContainer.setAlpha(1f);
-
         int index;
         if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
             index = mBubbleData.getBubbles().size();
         } else {
             index = getBubbleIndex(mExpandedBubble);
         }
-        // Position of the bubble we're expanding, once it's settled in its row.
-        final float bubbleWillBeAt =
-                mExpandedAnimationController.getBubbleXOrYForOrientation(index);
+        PointF p = mPositioner.getExpandedBubbleXY(index, getState());
+        final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
+                mPositioner.showBubblesVertically() ? p.y : p.x);
+        mExpandedViewContainer.setTranslationX(0f);
+        mExpandedViewContainer.setTranslationY(translationY);
+        mExpandedViewContainer.setAlpha(1f);
 
         // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles
         // that are animating farther, so that the expanded view doesn't move as much.
         final float relevantStackPosition = showVertically
                 ? mStackAnimationController.getStackPosition().y
                 : mStackAnimationController.getStackPosition().x;
+        final float bubbleWillBeAt = showVertically
+                ? p.y
+                : p.x;
         final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition);
 
         // Wait for the path animation target to reach its end, and add a small amount of extra time
@@ -1862,27 +1905,27 @@
         // Set the pivot point for the scale, so the expanded view animates out from the bubble.
         if (showVertically) {
             float pivotX;
-            float pivotY = bubbleWillBeAt + mBubbleSize / 2f;
             if (mStackOnLeftOrWillBe) {
-                pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
+                pivotX = p.x + mBubbleSize + mExpandedViewPadding;
             } else {
-                pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding;
+                pivotX = p.x - mExpandedViewPadding;
             }
             mExpandedViewContainerMatrix.setScale(
                     1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
                     1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
-                    pivotX, pivotY);
+                    pivotX,
+                    p.y + mBubbleSize / 2f);
         } else {
             mExpandedViewContainerMatrix.setScale(
                     1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
                     1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
-                    bubbleWillBeAt + mBubbleSize / 2f,
-                    mPositioner.getExpandedViewY());
+                    p.x + mBubbleSize / 2f,
+                    p.y + mBubbleSize + mExpandedViewPadding);
         }
         mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
 
         if (mExpandedBubble.getExpandedView() != null) {
-            mExpandedBubble.setExpandedContentAlpha(0f);
+            mExpandedBubble.getExpandedView().setTaskViewAlpha(0f);
 
             // We'll be starting the alpha animation after a slight delay, so set this flag early
             // here.
@@ -1914,6 +1957,7 @@
                                 mExpandedViewContainerMatrix);
                     })
                     .withEndActions(() -> {
+                        mExpandedViewContainer.setAnimationMatrix(null);
                         afterExpandedViewAnimation();
                         if (mExpandedBubble != null
                                 && mExpandedBubble.getExpandedView() != null) {
@@ -1929,12 +1973,17 @@
     private void animateCollapse() {
         cancelDelayedExpandCollapseSwitchAnimations();
 
+        if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+            mManageEduView.hide();
+        }
         // Hide the menu if it's visible.
         showManageMenu(false);
 
         mIsExpanded = false;
         mIsExpansionAnimating = true;
 
+        showScrim(false);
+
         mBubbleContainer.cancelAllAnimations();
 
         // If we were in the middle of swapping, the animating-out surface would have been scaling
@@ -1952,10 +2001,6 @@
                 /* collapseTo */,
                 () -> mBubbleContainer.setActiveController(mStackAnimationController));
 
-        if (mTaskbarScrim.getVisibility() == VISIBLE) {
-            mTaskbarScrim.animate().alpha(0f).start();
-        }
-
         int index;
         if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
             index = mBubbleData.getBubbles().size();
@@ -1963,12 +2008,10 @@
             index = mBubbleData.getBubbles().indexOf(mExpandedBubble);
         }
         // Value the bubble is animating from (back into the stack).
-        final float expandingFromBubbleAt =
-                mExpandedAnimationController.getBubbleXOrYForOrientation(index);
-        final boolean showVertically = mPositioner.showBubblesVertically();
+        final PointF p = mPositioner.getExpandedBubbleXY(index, getState());
         if (mPositioner.showBubblesVertically()) {
             float pivotX;
-            float pivotY = expandingFromBubbleAt + mBubbleSize / 2f;
+            float pivotY = p.y + mBubbleSize / 2f;
             if (mStackOnLeftOrWillBe) {
                 pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
             } else {
@@ -1980,8 +2023,8 @@
         } else {
             mExpandedViewContainerMatrix.setScale(
                     1f, 1f,
-                    expandingFromBubbleAt + mBubbleSize / 2f,
-                    mPositioner.getExpandedViewY());
+                    p.x + mBubbleSize / 2f,
+                    p.y + mBubbleSize + mExpandedViewPadding);
         }
 
         mExpandedViewAlphaAnimator.reverse();
@@ -2008,7 +2051,7 @@
                     final BubbleViewProvider previouslySelected = mExpandedBubble;
                     beforeExpandedViewAnimation();
                     if (mManageEduView != null) {
-                        mManageEduView.hide(false /* fromExpansion */);
+                        mManageEduView.hide();
                     }
 
                     if (DEBUG_BUBBLE_STACK_VIEW) {
@@ -2023,10 +2066,6 @@
                     if (previouslySelected != null) {
                         previouslySelected.setTaskViewVisibility(false);
                     }
-
-                    if (mPositioner.showingInTaskbar()) {
-                        mTaskbarScrim.setVisibility(GONE);
-                    }
                 })
                 .start();
     }
@@ -2063,32 +2102,31 @@
 
         boolean isOverflow = mExpandedBubble != null
                 && mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
-        float expandingFromBubbleDestination =
-                mExpandedAnimationController.getBubbleXOrYForOrientation(isOverflow
-                        ? getBubbleCount()
-                        : mBubbleData.getBubbles().indexOf(mExpandedBubble));
-
+        PointF p = mPositioner.getExpandedBubbleXY(isOverflow
+                        ? mBubbleContainer.getChildCount() - 1
+                        : mBubbleData.getBubbles().indexOf(mExpandedBubble),
+                getState());
         mExpandedViewContainer.setAlpha(1f);
         mExpandedViewContainer.setVisibility(View.VISIBLE);
 
         if (mPositioner.showBubblesVertically()) {
             float pivotX;
-            float pivotY = expandingFromBubbleDestination + mBubbleSize / 2f;
+            float pivotY = p.y + mBubbleSize / 2f;
             if (mStackOnLeftOrWillBe) {
-                pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
+                pivotX = p.x + mBubbleSize + mExpandedViewPadding;
             } else {
-                pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding;
-
+                pivotX = p.x - mExpandedViewPadding;
             }
             mExpandedViewContainerMatrix.setScale(
-                    0f, 0f,
+                    1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
+                    1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
                     pivotX, pivotY);
         } else {
             mExpandedViewContainerMatrix.setScale(
                     1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
                     1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
-                    expandingFromBubbleDestination + mBubbleSize / 2f,
-                    mPositioner.getExpandedViewY());
+                    p.x + mBubbleSize / 2f,
+                    p.y + mBubbleSize + mExpandedViewPadding);
         }
 
         mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
@@ -2113,6 +2151,7 @@
                     .withEndActions(() -> {
                         mExpandedViewTemporarilyHidden = false;
                         mIsBubbleSwitchAnimating = false;
+                        mExpandedViewContainer.setAnimationMatrix(null);
                     })
                     .start();
         }, 25);
@@ -2144,9 +2183,20 @@
         }
     }
 
-    /** Moves the bubbles out of the way if they're going to be over the keyboard. */
-    public void onImeVisibilityChanged(boolean visible, int height) {
-        mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
+    /**
+     * Updates the stack based for IME changes. When collapsed it'll move the stack if it
+     * overlaps where they IME would be. When expanded it'll shift the expanded bubbles
+     * if they might overlap with the IME (this only happens for large screens).
+     */
+    public void animateForIme(boolean visible) {
+        if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
+            // This will update the animation so the bubbles move to position for the IME
+            mExpandedAnimationController.expandFromStack(() -> {
+                updatePointerPosition(false /* forIme */);
+                afterExpandedViewAnimation();
+            } /* after */);
+            return;
+        }
 
         if (!mIsExpanded && getBubbleCount() > 0) {
             final float stackDestinationY =
@@ -2165,9 +2215,20 @@
                                 FLYOUT_IME_ANIMATION_SPRING_CONFIG)
                         .start();
             }
-        } else if (mIsExpanded && mExpandedBubble != null
-                && mExpandedBubble.getExpandedView() != null) {
+        } else if (mPositioner.showBubblesVertically() && mIsExpanded
+                && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
             mExpandedBubble.getExpandedView().setImeVisible(visible);
+            List<Animator> animList = new ArrayList();
+            for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
+                View child = mBubbleContainer.getChildAt(i);
+                float transY = mPositioner.getExpandedBubbleXY(i, getState()).y;
+                ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY);
+                animList.add(anim);
+            }
+            updatePointerPosition(true /* forIme */);
+            AnimatorSet set = new AnimatorSet();
+            set.playTogether(animList);
+            set.start();
         }
     }
 
@@ -2403,20 +2464,19 @@
 
 
             if (mFlyout.getVisibility() == View.VISIBLE) {
-                mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(),
+                mFlyout.animateUpdate(bubble.getFlyoutMessage(),
                         mStackAnimationController.getStackPosition(), !bubble.showDot(),
                         mAfterFlyoutHidden /* onHide */);
             } else {
                 mFlyout.setVisibility(INVISIBLE);
                 mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(),
-                        mStackAnimationController.getStackPosition(), getWidth(),
+                        mStackAnimationController.getStackPosition(),
                         mStackAnimationController.isStackOnLeftSide(),
                         bubble.getIconView().getDotColor() /* dotColor */,
                         expandFlyoutAfterDelay /* onLayoutComplete */,
                         mAfterFlyoutHidden /* onHide */,
                         bubble.getIconView().getDotCenter(),
-                        !bubble.showDot(),
-                        mPositioner);
+                        !bubble.showDot());
             }
             mFlyout.bringToFront();
         });
@@ -2472,7 +2532,7 @@
             // Account for the IME in the touchable region so that the touchable region of the
             // Bubble window doesn't obscure the IME. The touchable region affects which areas
             // of the screen can be excluded by lower windows (IME is just above the embedded task)
-            outRect.bottom -= (int) mStackAnimationController.getImeHeight();
+            outRect.bottom -= mPositioner.getImeHeight();
         }
 
         if (mFlyout.getVisibility() == View.VISIBLE) {
@@ -2491,15 +2551,36 @@
         invalidate();
     }
 
-    private void showManageMenu(boolean show) {
+    /** Hide or show the manage menu for the currently expanded bubble. */
+    @VisibleForTesting
+    public void showManageMenu(boolean show) {
         mShowingManage = show;
 
         // This should not happen, since the manage menu is only visible when there's an expanded
         // bubble. If we end up in this state, just hide the menu immediately.
         if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
             mManageMenu.setVisibility(View.INVISIBLE);
+            mManageMenuScrim.setVisibility(INVISIBLE);
+            mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */);
             return;
         }
+        if (show) {
+            mManageMenuScrim.setVisibility(VISIBLE);
+            mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f);
+        }
+        Runnable endAction = () -> {
+            if (!show) {
+                mManageMenuScrim.setVisibility(INVISIBLE);
+                mManageMenuScrim.setTranslationZ(0f);
+            }
+        };
+
+        mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show);
+        mManageMenuScrim.animate()
+                .setInterpolator(show ? ALPHA_IN : ALPHA_OUT)
+                .alpha(show ? SCRIM_ALPHA : 0f)
+                .withEndAction(endAction)
+                .start();
 
         // If available, update the manage menu's settings option with the expanded bubble's app
         // name and icon.
@@ -2510,7 +2591,6 @@
                     R.string.bubbles_app_settings, bubble.getAppName()));
         }
 
-        mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
         if (mExpandedBubble.getExpandedView().getTaskView() != null) {
             mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage
                     ? new Rect(0, 0, getWidth(), getHeight())
@@ -2522,7 +2602,11 @@
 
         // When the menu is open, it should be at these coordinates. The menu pops out to the right
         // in LTR and to the left in RTL.
-        final float targetX = isLtr ? mTempRect.left : mTempRect.right - mManageMenu.getWidth();
+        mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
+        final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin();
+        final float targetX = isLtr
+                ? mTempRect.left - margin
+                : mTempRect.right + margin - mManageMenu.getWidth();
         final float targetY = mTempRect.bottom - mManageMenu.getHeight();
 
         final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f;
@@ -2702,18 +2786,21 @@
         }
         boolean isOverflowExpanded = mExpandedBubble != null
                 && BubbleOverflow.KEY.equals(mExpandedBubble.getKey());
-        int[] paddings = mPositioner.getExpandedViewPadding(
+        int[] paddings = mPositioner.getExpandedViewContainerPadding(
                 mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded);
         mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
         if (mIsExpansionAnimating) {
             mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
         }
         if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
-            mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY());
+            PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
+                    getState());
+            mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble,
+                    mPositioner.showBubblesVertically() ? p.y : p.x));
             mExpandedViewContainer.setTranslationX(0f);
             mExpandedBubble.getExpandedView().updateView(
                     mExpandedViewContainer.getLocationOnScreen());
-            updatePointerPosition();
+            updatePointerPosition(false /* forIme */);
         }
 
         mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
@@ -2784,7 +2871,13 @@
         }
     }
 
-    private void updatePointerPosition() {
+    /**
+     * Updates the position of the pointer based on the expanded bubble.
+     *
+     * @param forIme whether the position is being updated due to the ime appearing, in this case
+     *               the pointer is animated to the location.
+     */
+    private void updatePointerPosition(boolean forIme) {
         if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
             return;
         }
@@ -2792,8 +2885,12 @@
         if (index == -1) {
             return;
         }
-        float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index);
-        mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, mStackOnLeftOrWillBe);
+        PointF position = mPositioner.getExpandedBubbleXY(index, getState());
+        float bubblePosition = mPositioner.showBubblesVertically()
+                ? position.y
+                : position.x;
+        mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition,
+                mStackOnLeftOrWillBe, forIme /* animate */);
     }
 
     /**
@@ -2876,6 +2973,26 @@
         return bubbles;
     }
 
+    /** @return the current stack state. */
+    public StackViewState getState() {
+        mStackViewState.numberOfBubbles = mBubbleContainer.getChildCount();
+        mStackViewState.selectedIndex = getBubbleIndex(mExpandedBubble);
+        mStackViewState.onLeft = mStackOnLeftOrWillBe;
+        return mStackViewState;
+    }
+
+    /**
+     * Holds some commonly queried information about the stack.
+     */
+    public static class StackViewState {
+        // Number of bubbles (including the overflow itself) in the stack.
+        public int numberOfBubbles;
+        // The selected index if the stack is expanded.
+        public int selectedIndex;
+        // Whether the stack is resting on the left or right side of the screen when collapsed.
+        public boolean onLeft;
+    }
+
     /**
      * Representation of stack position that uses relative properties rather than absolute
      * coordinates. This is used to maintain similar stack positions across configuration changes.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
index 38b3ba9..7e55282 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
@@ -29,12 +29,6 @@
     @Nullable BubbleExpandedView getExpandedView();
 
     /**
-     * Sets the alpha of the expanded view content. This will be applied to both the expanded view
-     * container itself (the manage button, etc.) as well as the TaskView within it.
-     */
-    void setExpandedContentAlpha(float alpha);
-
-    /**
      * Sets whether the contents of the bubble's TaskView should be visible.
      */
     void setTaskViewVisibility(boolean visible);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index c73b5ee..c82249b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -24,12 +24,10 @@
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
 import android.os.Bundle;
-import android.os.Looper;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.util.ArraySet;
 import android.util.Pair;
 import android.util.SparseArray;
-import android.view.View;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
@@ -43,7 +41,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.Executor;
-import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
@@ -160,14 +157,6 @@
     /** Set the proxy to commnuicate with SysUi side components. */
     void setSysuiProxy(SysuiProxy proxy);
 
-    /**
-     * Set the scrim view for bubbles.
-     *
-     * @param callback The callback made with the executor and the executor's looper that the view
-     *                 will be running on.
-     **/
-    void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback);
-
     /** Set a listener to be notified of bubble expand events. */
     void setExpandListener(BubbleExpandListener listener);
 
@@ -295,6 +284,8 @@
 
         void onStackExpandChanged(boolean shouldExpand);
 
+        void onManageMenuExpandChanged(boolean menuExpanded);
+
         void onUnbubbleConversation(String key);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
index 0a1cd22..74672a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
@@ -28,37 +28,40 @@
 import com.android.wm.shell.R
 import com.android.wm.shell.animation.PhysicsAnimator
 import com.android.wm.shell.common.DismissCircleView
+import android.view.WindowInsets
+import android.view.WindowManager
 
 /*
  * View that handles interactions between DismissCircleView and BubbleStackView.
  */
 class DismissView(context: Context) : FrameLayout(context) {
 
-    var circle = DismissCircleView(context).apply {
-        val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
-        val newParams = LayoutParams(targetSize, targetSize)
-        newParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
-        setLayoutParams(newParams)
-        setTranslationY(
-            resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height).toFloat())
-    }
-
+    var circle = DismissCircleView(context)
     var isShowing = false
+
     private val animator = PhysicsAnimator.getInstance(circle)
     private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
     private val DISMISS_SCRIM_FADE_MS = 200
+    private var wm: WindowManager =
+            context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
     init {
         setLayoutParams(LayoutParams(
             ViewGroup.LayoutParams.MATCH_PARENT,
             resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
             Gravity.BOTTOM))
-        setPadding(0, 0, 0, resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin))
+        updatePadding()
         setClipToPadding(false)
         setClipChildren(false)
         setVisibility(View.INVISIBLE)
         setBackgroundResource(
             R.drawable.floating_dismiss_gradient_transition)
-        addView(circle)
+
+        val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
+        addView(circle, LayoutParams(targetSize, targetSize,
+                Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL))
+        // start with circle offscreen so it's animated up
+        circle.setTranslationY(resources.getDimensionPixelSize(
+                R.dimen.floating_dismiss_gradient_height).toFloat())
     }
 
     /**
@@ -91,9 +94,21 @@
     }
 
     fun updateResources() {
-        val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
+        updatePadding()
+        layoutParams.height = resources.getDimensionPixelSize(
+                R.dimen.floating_dismiss_gradient_height)
+
+        val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
         circle.layoutParams.width = targetSize
         circle.layoutParams.height = targetSize
         circle.requestLayout()
     }
+
+    private fun updatePadding() {
+        val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets()
+        val navInset = insets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.navigationBars())
+        setPadding(0, 0, 0, navInset.bottom +
+                resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin))
+    }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
index 4cc6702..eb4737a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
@@ -18,12 +18,13 @@
 import android.content.Context
 import android.graphics.Color
 import android.graphics.Rect
+import android.graphics.drawable.ColorDrawable
 import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewGroup
 import android.widget.Button
 import android.widget.LinearLayout
-import android.widget.TextView
-import com.android.internal.util.ContrastColorUtil
+import com.android.internal.R.color.system_neutral1_900
 import com.android.wm.shell.R
 import com.android.wm.shell.animation.Interpolators
 
@@ -31,21 +32,22 @@
  * User education view to highlight the manage button that allows a user to configure the settings
  * for the bubble. Shown only the first time a user expands a bubble.
  */
-class ManageEducationView constructor(context: Context) : LinearLayout(context) {
+class ManageEducationView constructor(context: Context, positioner: BubblePositioner)
+    : LinearLayout(context) {
 
-    private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleManageEducationView"
+    private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView"
         else BubbleDebugConfig.TAG_BUBBLES
 
     private val ANIMATE_DURATION: Long = 200
-    private val ANIMATE_DURATION_SHORT: Long = 40
 
-    private val manageView by lazy { findViewById<View>(R.id.manage_education_view) }
-    private val manageButton by lazy { findViewById<Button>(R.id.manage) }
+    private val positioner: BubblePositioner = positioner
+    private val manageView by lazy { findViewById<ViewGroup>(R.id.manage_education_view) }
+    private val manageButton by lazy { findViewById<Button>(R.id.manage_button) }
     private val gotItButton by lazy { findViewById<Button>(R.id.got_it) }
-    private val titleTextView by lazy { findViewById<TextView>(R.id.user_education_title) }
-    private val descTextView by lazy { findViewById<TextView>(R.id.user_education_description) }
 
     private var isHiding = false
+    private var realManageButtonRect = Rect()
+    private var bubbleExpandedView: BubbleExpandedView? = null
 
     init {
         LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this)
@@ -66,18 +68,17 @@
     override fun onFinishInflate() {
         super.onFinishInflate()
         layoutDirection = resources.configuration.layoutDirection
-        setTextColor()
     }
 
-    private fun setTextColor() {
-        val typedArray = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent,
-            android.R.attr.textColorPrimaryInverse))
-        val bgColor = typedArray.getColor(0 /* index */, Color.BLACK)
-        var textColor = typedArray.getColor(1 /* index */, Color.WHITE)
+    private fun setButtonColor() {
+        val typedArray = mContext.obtainStyledAttributes(intArrayOf(
+                com.android.internal.R.attr.colorAccentPrimary))
+        val buttonColor = typedArray.getColor(0 /* index */, Color.TRANSPARENT)
         typedArray.recycle()
-        textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true)
-        titleTextView.setTextColor(textColor)
-        descTextView.setTextColor(textColor)
+
+        manageButton.setTextColor(mContext.getColor(system_neutral1_900))
+        manageButton.setBackgroundDrawable(ColorDrawable(buttonColor))
+        gotItButton.setBackgroundDrawable(ColorDrawable(buttonColor))
     }
 
     private fun setDrawableDirection() {
@@ -91,30 +92,39 @@
      * If necessary, toggles the user education view for the manage button. This is shown when the
      * bubble stack is expanded for the first time.
      *
-     * @param show whether the user education view should show or not.
+     * @param expandedView the expandedView the user education is shown on top of.
      */
-    fun show(expandedView: BubbleExpandedView, rect: Rect) {
+    fun show(expandedView: BubbleExpandedView) {
+        setButtonColor()
         if (visibility == VISIBLE) return
 
+        bubbleExpandedView = expandedView
+        expandedView.taskView?.setObscuredTouchRect(Rect(positioner.screenRect))
+
+        layoutParams.width = if (positioner.isLargeScreen)
+            context.resources.getDimensionPixelSize(
+                    R.dimen.bubbles_user_education_width_large_screen)
+        else ViewGroup.LayoutParams.MATCH_PARENT
+
         alpha = 0f
         visibility = View.VISIBLE
+        expandedView.getManageButtonBoundsOnScreen(realManageButtonRect)
+        manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin,
+                manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom)
         post {
-            expandedView.getManageButtonBoundsOnScreen(rect)
-
             manageButton
                 .setOnClickListener {
-                    expandedView.findViewById<View>(R.id.settings_button).performClick()
-                    hide(true /* isStackExpanding */)
+                    hide()
+                    expandedView.findViewById<View>(R.id.manage_button).performClick()
                 }
-            gotItButton.setOnClickListener { hide(true /* isStackExpanding */) }
-            setOnClickListener { hide(true /* isStackExpanding */) }
+            gotItButton.setOnClickListener { hide() }
+            setOnClickListener { hide() }
 
-            with(manageView) {
-                translationX = 0f
-                val inset = resources.getDimensionPixelSize(
-                    R.dimen.bubbles_manage_education_top_inset)
-                translationY = (rect.top - manageView.height + inset).toFloat()
-            }
+            val offsetViewBounds = Rect()
+            manageButton.getDrawingRect(offsetViewBounds)
+            manageView.offsetDescendantRectToMyCoords(manageButton, offsetViewBounds)
+            translationX = 0f
+            translationY = (realManageButtonRect.top - offsetViewBounds.top).toFloat()
             bringToFront()
             animate()
                 .setDuration(ANIMATE_DURATION)
@@ -124,13 +134,14 @@
         setShouldShow(false)
     }
 
-    fun hide(isStackExpanding: Boolean) {
+    fun hide() {
+        bubbleExpandedView?.taskView?.setObscuredTouchRect(null)
         if (visibility != VISIBLE || isHiding) return
 
         animate()
             .withStartAction { isHiding = true }
             .alpha(0f)
-            .setDuration(if (isStackExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
+            .setDuration(ANIMATE_DURATION)
             .withEndAction {
                 isHiding = false
                 visibility = GONE
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 0a2cfc4..f6a90b7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -18,8 +18,11 @@
 import android.content.Context
 import android.graphics.Color
 import android.graphics.PointF
+import android.view.KeyEvent
 import android.view.LayoutInflater
 import android.view.View
+import android.view.View.OnKeyListener
+import android.view.ViewGroup
 import android.widget.LinearLayout
 import android.widget.TextView
 import com.android.internal.util.ContrastColorUtil
@@ -30,7 +33,12 @@
  * User education view to highlight the collapsed stack of bubbles.
  * Shown only the first time a user taps the stack.
  */
-class StackEducationView constructor(context: Context) : LinearLayout(context) {
+class StackEducationView constructor(
+    context: Context,
+    positioner: BubblePositioner,
+    controller: BubbleController
+)
+    : LinearLayout(context) {
 
     private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
         else BubbleDebugConfig.TAG_BUBBLES
@@ -38,6 +46,9 @@
     private val ANIMATE_DURATION: Long = 200
     private val ANIMATE_DURATION_SHORT: Long = 40
 
+    private val positioner: BubblePositioner = positioner
+    private val controller: BubbleController = controller
+
     private val view by lazy { findViewById<View>(R.id.stack_education_layout) }
     private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) }
     private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) }
@@ -67,6 +78,28 @@
         setTextColor()
     }
 
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        setFocusableInTouchMode(true)
+        setOnKeyListener(object : OnKeyListener {
+            override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean {
+                // if the event is a key down event on the enter button
+                if (event.action == KeyEvent.ACTION_UP &&
+                        keyCode == KeyEvent.KEYCODE_BACK && !isHiding) {
+                    hide(false)
+                    return true
+                }
+                return false
+            }
+        })
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        setOnKeyListener(null)
+        controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+    }
+
     private fun setTextColor() {
         val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent,
             android.R.attr.textColorPrimaryInverse))
@@ -94,13 +127,25 @@
     fun show(stackPosition: PointF): Boolean {
         if (visibility == VISIBLE) return false
 
+        controller.updateWindowFlagsForBackpress(true /* interceptBack */)
+        layoutParams.width = if (positioner.isLargeScreen)
+            context.resources.getDimensionPixelSize(
+                    R.dimen.bubbles_user_education_width_large_screen)
+        else ViewGroup.LayoutParams.MATCH_PARENT
+
         setAlpha(0f)
         setVisibility(View.VISIBLE)
         post {
+            requestFocus()
             with(view) {
-                val bubbleSize = context.resources.getDimensionPixelSize(
-                    R.dimen.bubble_size)
-                translationY = stackPosition.y + bubbleSize / 2 - getHeight() / 2
+                if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+                    setPadding(positioner.bubbleSize + paddingRight, paddingTop, paddingRight,
+                            paddingBottom)
+                } else {
+                    setPadding(paddingLeft, paddingTop, positioner.bubbleSize + paddingLeft,
+                            paddingBottom)
+                }
+                translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2
             }
             animate()
                 .setDuration(ANIMATE_DURATION)
@@ -114,15 +159,16 @@
     /**
      * If necessary, hides the stack education view.
      *
-     * @param fromExpansion if true this indicates the hide is happening due to the bubble being
+     * @param isExpanding if true this indicates the hide is happening due to the bubble being
      *                      expanded, false if due to a touch outside of the bubble stack.
      */
-    fun hide(fromExpansion: Boolean) {
+    fun hide(isExpanding: Boolean) {
         if (visibility != VISIBLE || isHiding) return
 
+        controller.updateWindowFlagsForBackpress(false /* interceptBack */)
         animate()
             .alpha(0f)
-            .setDuration(if (fromExpansion) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
+            .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
             .withEndAction { visibility = GONE }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index df2b440..f0f78748 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -21,7 +21,6 @@
 import android.content.res.Resources;
 import android.graphics.Path;
 import android.graphics.PointF;
-import android.graphics.Rect;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -33,6 +32,7 @@
 import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 import com.google.android.collect.Sets;
@@ -64,9 +64,6 @@
     /** Stiffness for the expand/collapse path-following animation. */
     private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
 
-    /** What percentage of the screen to use when centering the bubbles in landscape. */
-    private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
-
     /**
      * Velocity required to dismiss an individual bubble without dragging it into the dismiss
      * target.
@@ -79,16 +76,8 @@
 
     /** Horizontal offset between bubbles, which we need to know to re-stack them. */
     private float mStackOffsetPx;
-    /** Space between status bar and bubbles in the expanded state. */
-    private float mBubblePaddingTop;
     /** Size of each bubble. */
     private float mBubbleSizePx;
-    /** Max number of bubbles shown in row above expanded view. */
-    private int mBubblesMaxRendered;
-    /** Max amount of space to have between bubbles when expanded. */
-    private int mBubblesMaxSpace;
-    /** Amount of space between the bubbles when expanded. */
-    private float mSpaceBetweenBubbles;
     /** Whether the expand / collapse animation is running. */
     private boolean mAnimatingExpand = false;
 
@@ -127,8 +116,6 @@
     /** The bubble currently being dragged out of the row (to potentially be dismissed). */
     private MagnetizedObject<View> mMagnetizedBubbleDraggingOut;
 
-    private int mExpandedViewPadding;
-
     /**
      * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the
      * end of this animation means we have no bubbles left, and notify the BubbleController.
@@ -137,13 +124,15 @@
 
     private BubblePositioner mPositioner;
 
-    public ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding,
-            Runnable onBubbleAnimatedOutAction) {
+    private BubbleStackView mBubbleStackView;
+
+    public ExpandedAnimationController(BubblePositioner positioner,
+            Runnable onBubbleAnimatedOutAction, BubbleStackView stackView) {
         mPositioner = positioner;
         updateResources();
-        mExpandedViewPadding = expandedViewPadding;
         mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction;
         mCollapsePoint = mPositioner.getDefaultStartPosition();
+        mBubbleStackView = stackView;
     }
 
     /**
@@ -208,11 +197,8 @@
             return;
         }
         Resources res = mLayout.getContext().getResources();
-        mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
         mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
         mBubbleSizePx = mPositioner.getBubbleSize();
-        mBubblesMaxRendered = mPositioner.getMaxBubbles();
-        mSpaceBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
     }
 
     /**
@@ -256,31 +242,19 @@
             final Path path = new Path();
             path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
 
-            final float expandedY = mPositioner.showBubblesVertically()
-                    ? getBubbleXOrYForOrientation(index)
-                    : getExpandedY();
+            final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState());
             if (expanding) {
-                // If we're expanding, first draw a line from the bubble's current position to the
-                // top of the screen.
-                path.lineTo(bubble.getTranslationX(), expandedY);
+                // If we're expanding, first draw a line from the bubble's current position to where
+                // it'll end up
+                path.lineTo(bubble.getTranslationX(), p.y);
                 // Then, draw a line across the screen to the bubble's resting position.
-                if (mPositioner.showBubblesVertically()) {
-                    Rect availableRect = mPositioner.getAvailableRect();
-                    boolean onLeft = mCollapsePoint != null
-                            && mCollapsePoint.x < (availableRect.width() / 2f);
-                    float translationX = onLeft
-                            ? availableRect.left
-                            : availableRect.right - mBubbleSizePx;
-                    path.lineTo(translationX, getBubbleXOrYForOrientation(index));
-                } else {
-                    path.lineTo(getBubbleXOrYForOrientation(index), expandedY);
-                }
+                path.lineTo(p.x, p.y);
             } else {
                 final float stackedX = mCollapsePoint.x;
 
                 // If we're collapsing, draw a line from the bubble's current position to the side
                 // of the screen where the bubble will be stacked.
-                path.lineTo(stackedX, expandedY);
+                path.lineTo(stackedX, p.y);
 
                 // Then, draw a line down to the stack position.
                 path.lineTo(stackedX, mCollapsePoint.y
@@ -390,8 +364,9 @@
             bubbleView.setTranslationY(y);
         }
 
+        final float expandedY = mPositioner.getExpandedViewYTopAligned();
         final boolean draggedOutEnough =
-                y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
+                y > expandedY + mBubbleSizePx || y < expandedY - mBubbleSizePx;
         if (draggedOutEnough != mBubbleDraggedOutEnough) {
             updateBubblePositions();
             mBubbleDraggedOutEnough = draggedOutEnough;
@@ -435,9 +410,9 @@
             return;
         }
         final int index = mLayout.indexOfChild(bubbleView);
-
+        final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState());
         animationForChildAtIndex(index)
-                .position(getBubbleXOrYForOrientation(index), getExpandedY())
+                .position(p.x, p.y)
                 .withPositionStartVelocities(velX, velY)
                 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
 
@@ -453,20 +428,6 @@
         updateBubblePositions();
     }
 
-    /**
-     * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
-     */
-    public void updateYPosition(Runnable after) {
-        if (mLayout == null) return;
-        animationsForChildrenFromIndex(
-                0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
-    }
-
-    /** The Y value of the row of expanded bubbles. */
-    public float getExpandedY() {
-        return mPositioner.getAvailableRect().top + mBubblePaddingTop;
-    }
-
     /** Description of current animation controller state. */
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("ExpandedAnimationController state:");
@@ -522,37 +483,35 @@
             startOrUpdatePathAnimation(true /* expanding */);
         } else if (mAnimatingCollapse) {
             startOrUpdatePathAnimation(false /* expanding */);
-        } else if (mPositioner.showBubblesVertically()) {
-            child.setTranslationY(getBubbleXOrYForOrientation(index));
-            if (!mPreparingToCollapse) {
-                // Only animate if we're not collapsing as that animation will handle placing the
-                // new bubble in the stacked position.
-                Rect availableRect = mPositioner.getAvailableRect();
-                boolean onLeft = mCollapsePoint != null
-                        && mCollapsePoint.x < (availableRect.width() / 2f);
-                float fromX = onLeft
-                        ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
-                        : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
-                float toX = onLeft
-                        ? availableRect.left + mExpandedViewPadding
-                        : availableRect.right - mBubbleSizePx - mExpandedViewPadding;
-                animationForChild(child)
-                        .translationX(fromX, toX)
-                        .start();
-                updateBubblePositions();
-            }
         } else {
-            child.setTranslationX(getBubbleXOrYForOrientation(index));
-            if (!mPreparingToCollapse) {
-                // Only animate if we're not collapsing as that animation will handle placing the
-                // new bubble in the stacked position.
-                float toY = getExpandedY();
-                float fromY = getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
-                animationForChild(child)
-                        .translationY(fromY, toY)
-                        .start();
-                updateBubblePositions();
+            boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint);
+            final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState());
+            if (mPositioner.showBubblesVertically()) {
+                child.setTranslationY(p.y);
+            } else {
+                child.setTranslationX(p.x);
             }
+
+            if (mPreparingToCollapse) {
+                // Don't animate if we're collapsing, as that animation will handle placing the
+                // new bubble in the stacked position.
+                return;
+            }
+
+            if (mPositioner.showBubblesVertically()) {
+                float fromX = onLeft
+                        ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
+                        : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
+                animationForChild(child)
+                        .translationX(fromX, p.y)
+                        .start();
+            } else {
+                float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
+                animationForChild(child)
+                        .translationY(fromY, p.y)
+                        .start();
+            }
+            updateBubblePositions();
         }
     }
 
@@ -599,7 +558,6 @@
         if (mAnimatingExpand || mAnimatingCollapse) {
             return;
         }
-
         for (int i = 0; i < mLayout.getChildCount(); i++) {
             final View bubble = mLayout.getChildAt(i);
 
@@ -609,49 +567,11 @@
                 return;
             }
 
-            if (mPositioner.showBubblesVertically()) {
-                Rect availableRect = mPositioner.getAvailableRect();
-                boolean onLeft = mCollapsePoint != null
-                        && mCollapsePoint.x < (availableRect.width() / 2f);
-                animationForChild(bubble)
-                        .translationX(onLeft
-                                ? availableRect.left
-                                : availableRect.right - mBubbleSizePx)
-                        .translationY(getBubbleXOrYForOrientation(i))
-                        .start();
-            } else {
-                animationForChild(bubble)
-                        .translationX(getBubbleXOrYForOrientation(i))
-                        .translationY(getExpandedY())
-                        .start();
-            }
+            final PointF p = mPositioner.getExpandedBubbleXY(i, mBubbleStackView.getState());
+            animationForChild(bubble)
+                    .translationX(p.x)
+                    .translationY(p.y)
+                    .start();
         }
     }
-
-    // TODO - could move to method on bubblePositioner if mSpaceBetweenBubbles gets moved
-    /**
-     * When bubbles are expanded in portrait, they display at the top of the screen in a horizontal
-     * row. When in landscape or on a large screen, they show at the left or right side in a
-     * vertical row. This method accounts for screen orientation and will return an x or y value
-     * for the position of the bubble in the row.
-     *
-     * @param index Bubble index in row.
-     * @return the y position of the bubble if showing vertically and the x position if showing
-     * horizontally.
-     */
-    public float getBubbleXOrYForOrientation(int index) {
-        if (mLayout == null) {
-            return 0;
-        }
-        final float positionInBar = index * (mBubbleSizePx + mSpaceBetweenBubbles);
-        Rect availableRect = mPositioner.getAvailableRect();
-        final boolean isLandscape = mPositioner.showBubblesVertically();
-        final float expandedStackSize = (mLayout.getChildCount() * mBubbleSizePx)
-                + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
-        final float centerPosition = isLandscape
-                ? availableRect.centerY()
-                : availableRect.centerX();
-        final float rowStart = centerPosition - (expandedStackSize / 2f);
-        return rowStart + positionInBar;
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index 636e145..60b6433 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -127,9 +127,6 @@
     /** Whether or not the stack's start position has been set. */
     private boolean mStackMovedToStartPosition = false;
 
-    /** The height of the most recently visible IME. */
-    private float mImeHeight = 0f;
-
     /**
      * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
      * IME is not visible or the user moved the stack since the IME became visible.
@@ -173,7 +170,7 @@
      */
     private boolean mSpringToTouchOnNextMotionEvent = false;
 
-    /** Horizontal offset of bubbles in the stack. */
+    /** Offset of bubbles in the stack (i.e. how much they overlap). */
     private float mStackOffset;
     /** Offset between stack y and animation y for bubble swap. */
     private float mSwapAnimationOffset;
@@ -305,10 +302,7 @@
         if (mLayout == null || !isStackPositionSet()) {
             return true; // Default to left, which is where it starts by default.
         }
-
-        float stackCenter = mStackPosition.x + mBubbleSize / 2;
-        float screenCenter = mLayout.getWidth() / 2;
-        return stackCenter < screenCenter;
+        return mPositioner.isStackOnLeft(mStackPosition);
     }
 
     /**
@@ -524,16 +518,6 @@
         removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
     }
 
-    /** Save the current IME height so that we know where the stack bounds should be. */
-    public void setImeHeight(int imeHeight) {
-        mImeHeight = imeHeight;
-    }
-
-    /** Returns the current IME height that the stack is offset by. */
-    public float getImeHeight() {
-        return mImeHeight;
-    }
-
     /**
      * Animates the stack either away from the newly visible IME, or back to its original position
      * due to the IME going away.
@@ -592,11 +576,14 @@
      */
     public RectF getAllowableStackPositionRegion() {
         final RectF allowableRegion = new RectF(mPositioner.getAvailableRect());
+        final int imeHeight = mPositioner.getImeHeight();
+        final float bottomPadding = getBubbleCount() > 1
+                ? mBubblePaddingTop + mStackOffset
+                : mBubblePaddingTop;
         allowableRegion.left -= mBubbleOffscreen;
         allowableRegion.top += mBubblePaddingTop;
         allowableRegion.right += mBubbleOffscreen - mBubbleSize;
-        allowableRegion.bottom -= mBubblePaddingTop + mBubbleSize
-                + (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f);
+        allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize;
         return allowableRegion;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
index 3a7b534..ffda1f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.common;
 
 import android.os.RemoteException;
+import android.util.Slog;
 import android.view.IDisplayWindowRotationCallback;
 import android.view.IDisplayWindowRotationController;
 import android.view.IWindowManager;
@@ -27,6 +28,7 @@
 import com.android.wm.shell.common.annotations.ShellMainThread;
 
 import java.util.ArrayList;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * This module deals with display rotations coming from WM. When WM starts a rotation: after it has
@@ -35,14 +37,14 @@
  * rotation.
  */
 public class DisplayChangeController {
+    private static final String TAG = DisplayChangeController.class.getSimpleName();
 
     private final ShellExecutor mMainExecutor;
     private final IWindowManager mWmService;
     private final IDisplayWindowRotationController mControllerImpl;
 
-    private final ArrayList<OnDisplayChangingListener> mRotationListener =
-            new ArrayList<>();
-    private final ArrayList<OnDisplayChangingListener> mTmpListeners = new ArrayList<>();
+    private final CopyOnWriteArrayList<OnDisplayChangingListener> mRotationListener =
+            new CopyOnWriteArrayList<>();
 
     public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
@@ -59,34 +61,26 @@
      * Adds a display rotation controller.
      */
     public void addRotationListener(OnDisplayChangingListener listener) {
-        synchronized (mRotationListener) {
-            mRotationListener.add(listener);
-        }
+        mRotationListener.add(listener);
     }
 
     /**
      * Removes a display rotation controller.
      */
     public void removeRotationListener(OnDisplayChangingListener listener) {
-        synchronized (mRotationListener) {
-            mRotationListener.remove(listener);
-        }
+        mRotationListener.remove(listener);
     }
 
     private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation,
             IDisplayWindowRotationCallback callback) {
         WindowContainerTransaction t = new WindowContainerTransaction();
-        synchronized (mRotationListener) {
-            mTmpListeners.clear();
-            // Make a local copy in case the handlers add/remove themselves.
-            mTmpListeners.addAll(mRotationListener);
-        }
-        for (OnDisplayChangingListener c : mTmpListeners) {
+        for (OnDisplayChangingListener c : mRotationListener) {
             c.onRotateDisplay(displayId, fromRotation, toRotation, t);
         }
         try {
             callback.continueRotateDisplay(toRotation, t);
         } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to continue rotation", e);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index ba9ba5e..a1fb658 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -26,6 +26,7 @@
 import android.view.Display;
 import android.view.IDisplayWindowListener;
 import android.view.IWindowManager;
+import android.view.InsetsState;
 
 import androidx.annotation.BinderThread;
 
@@ -52,14 +53,6 @@
     private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>();
     private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
 
-    /**
-     * Gets a display by id from DisplayManager.
-     */
-    public Display getDisplay(int displayId) {
-        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
-        return displayManager.getDisplay(displayId);
-    }
-
     public DisplayController(Context context, IWindowManager wmService,
             ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
@@ -67,14 +60,31 @@
         mWmService = wmService;
         mChangeController = new DisplayChangeController(mWmService, mainExecutor);
         mDisplayContainerListener = new DisplayWindowListenerImpl();
+    }
+
+    /**
+     * Initializes the window listener.
+     */
+    public void initialize() {
         try {
-            mWmService.registerDisplayWindowListener(mDisplayContainerListener);
+            int[] displayIds = mWmService.registerDisplayWindowListener(mDisplayContainerListener);
+            for (int i = 0; i < displayIds.length; i++) {
+                onDisplayAdded(displayIds[i]);
+            }
         } catch (RemoteException e) {
-            throw new RuntimeException("Unable to register hierarchy listener");
+            throw new RuntimeException("Unable to register display controller");
         }
     }
 
     /**
+     * Gets a display by id from DisplayManager.
+     */
+    public Display getDisplay(int displayId) {
+        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        return displayManager.getDisplay(displayId);
+    }
+
+    /**
      * Gets the DisplayLayout associated with a display.
      */
     public @Nullable DisplayLayout getDisplayLayout(int displayId) {
@@ -91,6 +101,16 @@
     }
 
     /**
+     * Updates the insets for a given display.
+     */
+    public void updateDisplayInsets(int displayId, InsetsState state) {
+        final DisplayRecord r = mDisplays.get(displayId);
+        if (r != null) {
+            r.setInsets(state);
+        }
+    }
+
+    /**
      * Add a display window-container listener. It will get notified whenever a display's
      * configuration changes or when displays are added/removed from the WM hierarchy.
      */
@@ -134,17 +154,18 @@
             if (mDisplays.get(displayId) != null) {
                 return;
             }
-            Display display = getDisplay(displayId);
+            final Display display = getDisplay(displayId);
             if (display == null) {
                 // It's likely that the display is private to some app and thus not
                 // accessible by system-ui.
                 return;
             }
-            DisplayRecord record = new DisplayRecord();
-            record.mDisplayId = displayId;
-            record.mContext = (displayId == Display.DEFAULT_DISPLAY) ? mContext
+
+            final Context context = (displayId == Display.DEFAULT_DISPLAY)
+                    ? mContext
                     : mContext.createDisplayContext(display);
-            record.mDisplayLayout = new DisplayLayout(record.mContext, display);
+            final DisplayRecord record = new DisplayRecord(displayId);
+            record.setDisplayLayout(context, new DisplayLayout(context, display));
             mDisplays.put(displayId, record);
             for (int i = 0; i < mDisplayChangedListeners.size(); ++i) {
                 mDisplayChangedListeners.get(i).onDisplayAdded(displayId);
@@ -154,24 +175,23 @@
 
     private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
         synchronized (mDisplays) {
-            DisplayRecord dr = mDisplays.get(displayId);
+            final DisplayRecord dr = mDisplays.get(displayId);
             if (dr == null) {
                 Slog.w(TAG, "Skipping Display Configuration change on non-added"
                         + " display.");
                 return;
             }
-            Display display = getDisplay(displayId);
+            final Display display = getDisplay(displayId);
             if (display == null) {
                 Slog.w(TAG, "Skipping Display Configuration change on invalid"
                         + " display. It may have been removed.");
                 return;
             }
-            Context perDisplayContext = mContext;
-            if (displayId != Display.DEFAULT_DISPLAY) {
-                perDisplayContext = mContext.createDisplayContext(display);
-            }
-            dr.mContext = perDisplayContext.createConfigurationContext(newConfig);
-            dr.mDisplayLayout = new DisplayLayout(dr.mContext, display);
+            final Context perDisplayContext = (displayId == Display.DEFAULT_DISPLAY)
+                    ? mContext
+                    : mContext.createDisplayContext(display);
+            final Context context = perDisplayContext.createConfigurationContext(newConfig);
+            dr.setDisplayLayout(context, new DisplayLayout(context, display));
             for (int i = 0; i < mDisplayChangedListeners.size(); ++i) {
                 mDisplayChangedListeners.get(i).onDisplayConfigurationChanged(
                         displayId, newConfig);
@@ -219,9 +239,25 @@
     }
 
     private static class DisplayRecord {
-        int mDisplayId;
-        Context mContext;
-        DisplayLayout mDisplayLayout;
+        private int mDisplayId;
+        private Context mContext;
+        private DisplayLayout mDisplayLayout;
+        private InsetsState mInsetsState = new InsetsState();
+
+        private DisplayRecord(int displayId) {
+            mDisplayId = displayId;
+        }
+
+        private void setDisplayLayout(Context context, DisplayLayout displayLayout) {
+            mContext = context;
+            mDisplayLayout = displayLayout;
+            mDisplayLayout.setInsets(mContext.getResources(), mInsetsState);
+        }
+
+        private void setInsets(InsetsState state) {
+            mInsetsState = state;
+            mDisplayLayout.setInsets(mContext.getResources(), state);
+        }
     }
 
     @BinderThread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index a7996f05..a7052bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -33,6 +33,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowInsets;
@@ -68,14 +69,17 @@
     protected final Executor mMainExecutor;
     private final TransactionPool mTransactionPool;
     private final DisplayController mDisplayController;
+    private final DisplayInsetsController mDisplayInsetsController;
     private final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>();
     private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>();
 
 
     public DisplayImeController(IWindowManager wmService, DisplayController displayController,
+            DisplayInsetsController displayInsetsController,
             Executor mainExecutor, TransactionPool transactionPool) {
         mWmService = wmService;
         mDisplayController = displayController;
+        mDisplayInsetsController = displayInsetsController;
         mMainExecutor = mainExecutor;
         mTransactionPool = transactionPool;
     }
@@ -109,11 +113,11 @@
 
     @Override
     public void onDisplayRemoved(int displayId) {
-        try {
-            mWmService.setDisplayWindowInsetsController(displayId, null);
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Unable to remove insets controller on display " + displayId);
+        PerDisplay pd = mImePerDisplay.get(displayId);
+        if (pd == null) {
+            return;
         }
+        pd.unregister();
         mImePerDisplay.remove(displayId);
     }
 
@@ -195,11 +199,10 @@
     }
 
     /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */
-    public class PerDisplay {
+    public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener {
         final int mDisplayId;
         final InsetsState mInsetsState = new InsetsState();
-        protected final DisplayWindowInsetsControllerImpl mInsetsControllerImpl =
-                new DisplayWindowInsetsControllerImpl();
+        final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
         InsetsSourceControl mImeSourceControl = null;
         int mAnimationDirection = DIRECTION_NONE;
         ValueAnimator mAnimation = null;
@@ -214,14 +217,15 @@
         }
 
         public void register() {
-            try {
-                mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId);
-            }
+            mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this);
         }
 
-        protected void insetsChanged(InsetsState insetsState) {
+        public void unregister() {
+            mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this);
+        }
+
+        @Override
+        public void insetsChanged(InsetsState insetsState) {
             if (mInsetsState.equals(insetsState)) {
                 return;
             }
@@ -239,8 +243,9 @@
             }
         }
 
+        @Override
         @VisibleForTesting
-        protected void insetsControlChanged(InsetsState insetsState,
+        public void insetsControlChanged(InsetsState insetsState,
                 InsetsSourceControl[] activeControls) {
             insetsChanged(insetsState);
             InsetsSourceControl imeSourceControl = null;
@@ -279,9 +284,9 @@
                     if (!mImeShowing) {
                         removeImeSurface();
                     }
-                }
-                if (mImeSourceControl != null) {
-                    mImeSourceControl.release(SurfaceControl::release);
+                    if (mImeSourceControl != null) {
+                        mImeSourceControl.release(SurfaceControl::release);
+                    }
                 }
                 mImeSourceControl = imeSourceControl;
             }
@@ -301,7 +306,8 @@
             }
         }
 
-        protected void showInsets(int types, boolean fromIme) {
+        @Override
+        public void showInsets(int types, boolean fromIme) {
             if ((types & WindowInsets.Type.ime()) == 0) {
                 return;
             }
@@ -309,8 +315,8 @@
             startAnimation(true /* show */, false /* forceRestart */);
         }
 
-
-        protected void hideInsets(int types, boolean fromIme) {
+        @Override
+        public void hideInsets(int types, boolean fromIme) {
             if ((types & WindowInsets.Type.ime()) == 0) {
                 return;
             }
@@ -318,6 +324,7 @@
             startAnimation(false /* show */, false /* forceRestart */);
         }
 
+        @Override
         public void topFocusedWindowChanged(String packageName) {
             // Do nothing
         }
@@ -327,8 +334,10 @@
          */
         private void setVisibleDirectly(boolean visible) {
             mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible);
+            mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible);
             try {
-                mWmService.modifyDisplayWindowInsets(mDisplayId, mInsetsState);
+                mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId,
+                        mRequestedVisibilities);
             } catch (RemoteException e) {
             }
         }
@@ -489,47 +498,6 @@
                 dispatchVisibilityChanged(mDisplayId, isShowing);
             }
         }
-
-        @VisibleForTesting
-        @BinderThread
-        public class DisplayWindowInsetsControllerImpl
-                extends IDisplayWindowInsetsController.Stub {
-            @Override
-            public void topFocusedWindowChanged(String packageName) throws RemoteException {
-                mMainExecutor.execute(() -> {
-                    PerDisplay.this.topFocusedWindowChanged(packageName);
-                });
-            }
-
-            @Override
-            public void insetsChanged(InsetsState insetsState) throws RemoteException {
-                mMainExecutor.execute(() -> {
-                    PerDisplay.this.insetsChanged(insetsState);
-                });
-            }
-
-            @Override
-            public void insetsControlChanged(InsetsState insetsState,
-                    InsetsSourceControl[] activeControls) throws RemoteException {
-                mMainExecutor.execute(() -> {
-                    PerDisplay.this.insetsControlChanged(insetsState, activeControls);
-                });
-            }
-
-            @Override
-            public void showInsets(int types, boolean fromIme) throws RemoteException {
-                mMainExecutor.execute(() -> {
-                    PerDisplay.this.showInsets(types, fromIme);
-                });
-            }
-
-            @Override
-            public void hideInsets(int types, boolean fromIme) throws RemoteException {
-                mMainExecutor.execute(() -> {
-                    PerDisplay.this.hideInsets(types, fromIme);
-                });
-            }
-        }
     }
 
     void removeImeSurface() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
new file mode 100644
index 0000000..565f148
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import android.os.RemoteException;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.IDisplayWindowInsetsController;
+import android.view.IWindowManager;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+
+import androidx.annotation.BinderThread;
+
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Manages insets from the core.
+ */
+public class DisplayInsetsController implements DisplayController.OnDisplaysChangedListener {
+    private static final String TAG = "DisplayInsetsController";
+
+    private final IWindowManager mWmService;
+    private final ShellExecutor mMainExecutor;
+    private final DisplayController mDisplayController;
+    private final SparseArray<PerDisplay> mInsetsPerDisplay = new SparseArray<>();
+    private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners =
+            new SparseArray<>();
+
+    public DisplayInsetsController(IWindowManager wmService, DisplayController displayController,
+            ShellExecutor mainExecutor) {
+        mWmService = wmService;
+        mDisplayController = displayController;
+        mMainExecutor = mainExecutor;
+    }
+
+    /**
+     * Starts listening for insets for each display.
+     **/
+    public void initialize() {
+        mDisplayController.addDisplayWindowListener(this);
+    }
+
+    /**
+     * Adds a callback to listen for insets changes for a particular display.  Note that the
+     * listener will not be updated with the existing state of the insets on that display.
+     */
+    public void addInsetsChangedListener(int displayId, OnInsetsChangedListener listener) {
+        CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId);
+        if (listeners == null) {
+            listeners = new CopyOnWriteArrayList<>();
+            mListeners.put(displayId, listeners);
+        }
+        if (!listeners.contains(listener)) {
+            listeners.add(listener);
+        }
+    }
+
+    /**
+     * Removes a callback listening for insets changes from a particular display.
+     */
+    public void removeInsetsChangedListener(int displayId, OnInsetsChangedListener listener) {
+        CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId);
+        if (listeners == null) {
+            return;
+        }
+        listeners.remove(listener);
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) {
+        PerDisplay pd = new PerDisplay(displayId);
+        pd.register();
+        mInsetsPerDisplay.put(displayId, pd);
+    }
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+        PerDisplay pd = mInsetsPerDisplay.get(displayId);
+        if (pd == null) {
+            return;
+        }
+        pd.unregister();
+        mInsetsPerDisplay.remove(displayId);
+    }
+
+    /**
+     * An implementation of {@link IDisplayWindowInsetsController} for a given display id.
+     **/
+    public class PerDisplay {
+        private final int mDisplayId;
+        private final DisplayWindowInsetsControllerImpl mInsetsControllerImpl =
+                new DisplayWindowInsetsControllerImpl();
+
+        public PerDisplay(int displayId) {
+            mDisplayId = displayId;
+        }
+
+        public void register() {
+            try {
+                mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId);
+            }
+        }
+
+        public void unregister() {
+            try {
+                mWmService.setDisplayWindowInsetsController(mDisplayId, null);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Unable to remove insets controller on display " + mDisplayId);
+            }
+        }
+
+        private void insetsChanged(InsetsState insetsState) {
+            CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+            if (listeners == null) {
+                return;
+            }
+            mDisplayController.updateDisplayInsets(mDisplayId, insetsState);
+            for (OnInsetsChangedListener listener : listeners) {
+                listener.insetsChanged(insetsState);
+            }
+        }
+
+        private void insetsControlChanged(InsetsState insetsState,
+                InsetsSourceControl[] activeControls) {
+            CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+            if (listeners == null) {
+                return;
+            }
+            for (OnInsetsChangedListener listener : listeners) {
+                listener.insetsControlChanged(insetsState, activeControls);
+            }
+        }
+
+        private void showInsets(int types, boolean fromIme) {
+            CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+            if (listeners == null) {
+                return;
+            }
+            for (OnInsetsChangedListener listener : listeners) {
+                listener.showInsets(types, fromIme);
+            }
+        }
+
+        private void hideInsets(int types, boolean fromIme) {
+            CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+            if (listeners == null) {
+                return;
+            }
+            for (OnInsetsChangedListener listener : listeners) {
+                listener.hideInsets(types, fromIme);
+            }
+        }
+
+        private void topFocusedWindowChanged(String packageName) {
+            CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+            if (listeners == null) {
+                return;
+            }
+            for (OnInsetsChangedListener listener : listeners) {
+                listener.topFocusedWindowChanged(packageName);
+            }
+        }
+
+        @BinderThread
+        private class DisplayWindowInsetsControllerImpl
+                extends IDisplayWindowInsetsController.Stub {
+            @Override
+            public void topFocusedWindowChanged(String packageName) throws RemoteException {
+                mMainExecutor.execute(() -> {
+                    PerDisplay.this.topFocusedWindowChanged(packageName);
+                });
+            }
+
+            @Override
+            public void insetsChanged(InsetsState insetsState) throws RemoteException {
+                mMainExecutor.execute(() -> {
+                    PerDisplay.this.insetsChanged(insetsState);
+                });
+            }
+
+            @Override
+            public void insetsControlChanged(InsetsState insetsState,
+                    InsetsSourceControl[] activeControls) throws RemoteException {
+                mMainExecutor.execute(() -> {
+                    PerDisplay.this.insetsControlChanged(insetsState, activeControls);
+                });
+            }
+
+            @Override
+            public void showInsets(int types, boolean fromIme) throws RemoteException {
+                mMainExecutor.execute(() -> {
+                    PerDisplay.this.showInsets(types, fromIme);
+                });
+            }
+
+            @Override
+            public void hideInsets(int types, boolean fromIme) throws RemoteException {
+                mMainExecutor.execute(() -> {
+                    PerDisplay.this.hideInsets(types, fromIme);
+                });
+            }
+        }
+    }
+
+    /**
+     * Gets notified whenever the insets change.
+     *
+     * @see IDisplayWindowInsetsController
+     */
+    @ShellMainThread
+    public interface OnInsetsChangedListener {
+        /**
+         * Called when top focused window changes to determine whether or not to take over insets
+         * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false.
+         * @param packageName: Passes the top package name
+         */
+        default void topFocusedWindowChanged(String packageName) {}
+
+        /**
+         * Called when the window insets configuration has changed.
+         */
+        default void insetsChanged(InsetsState insetsState) {}
+
+        /**
+         * Called when this window retrieved control over a specified set of insets sources.
+         */
+        default void insetsControlChanged(InsetsState insetsState,
+                InsetsSourceControl[] activeControls) {}
+
+        /**
+         * Called when a set of insets source window should be shown by policy.
+         *
+         * @param types internal insets types (WindowInsets.Type.InsetsType) to show
+         * @param fromIme true if this request originated from IME (InputMethodService).
+         */
+        default void showInsets(int types, boolean fromIme) {}
+
+        /**
+         * Called when a set of insets source window should be hidden by policy.
+         *
+         * @param types internal insets types (WindowInsets.Type.InsetsType) to hide
+         * @param fromIme true if this request originated from IME (InputMethodService).
+         */
+        default void hideInsets(int types, boolean fromIme) {}
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index b7235a3..962aca1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -25,6 +25,7 @@
 import static android.util.RotationUtils.rotateBounds;
 import static android.util.RotationUtils.rotateInsets;
 import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
+import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
@@ -44,7 +45,10 @@
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 import android.view.Gravity;
+import android.view.InsetsSource;
+import android.view.InsetsState;
 import android.view.Surface;
+import android.view.WindowInsets;
 
 import com.android.internal.R;
 
@@ -82,6 +86,10 @@
     private boolean mHasNavigationBar = false;
     private boolean mHasStatusBar = false;
     private int mNavBarFrameHeight = 0;
+    private boolean mAllowSeamlessRotationDespiteNavBarMoving = false;
+    private boolean mNavigationBarCanMove = false;
+    private boolean mReverseDefaultRotation = false;
+    private InsetsState mInsetsState = new InsetsState();
 
     @Override
     public boolean equals(Object o) {
@@ -98,14 +106,20 @@
                 && Objects.equals(mStableInsets, other.mStableInsets)
                 && mHasNavigationBar == other.mHasNavigationBar
                 && mHasStatusBar == other.mHasStatusBar
-                && mNavBarFrameHeight == other.mNavBarFrameHeight;
+                && mAllowSeamlessRotationDespiteNavBarMoving
+                        == other.mAllowSeamlessRotationDespiteNavBarMoving
+                && mNavigationBarCanMove == other.mNavigationBarCanMove
+                && mReverseDefaultRotation == other.mReverseDefaultRotation
+                && mNavBarFrameHeight == other.mNavBarFrameHeight
+                && Objects.equals(mInsetsState, other.mInsetsState);
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi,
                 mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
-                mNavBarFrameHeight);
+                mNavBarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving,
+                mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState);
     }
 
     /**
@@ -150,9 +164,13 @@
         mDensityDpi = dl.mDensityDpi;
         mHasNavigationBar = dl.mHasNavigationBar;
         mHasStatusBar = dl.mHasStatusBar;
+        mAllowSeamlessRotationDespiteNavBarMoving = dl.mAllowSeamlessRotationDespiteNavBarMoving;
+        mNavigationBarCanMove = dl.mNavigationBarCanMove;
+        mReverseDefaultRotation = dl.mReverseDefaultRotation;
         mNavBarFrameHeight = dl.mNavBarFrameHeight;
         mNonDecorInsets.set(dl.mNonDecorInsets);
         mStableInsets.set(dl.mStableInsets);
+        mInsetsState.set(dl.mInsetsState, true /* copySources */);
     }
 
     private void init(DisplayInfo info, Resources res, boolean hasNavigationBar,
@@ -165,12 +183,24 @@
         mDensityDpi = info.logicalDensityDpi;
         mHasNavigationBar = hasNavigationBar;
         mHasStatusBar = hasStatusBar;
+        mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean(
+            R.bool.config_allowSeamlessRotationDespiteNavBarMoving);
+        mNavigationBarCanMove = res.getBoolean(R.bool.config_navBarCanMove);
+        mReverseDefaultRotation = res.getBoolean(R.bool.config_reverseDefaultRotation);
+        recalcInsets(res);
+    }
+
+    /**
+     * Updates the current insets.
+     */
+    public void setInsets(Resources res, InsetsState state) {
+        mInsetsState = state;
         recalcInsets(res);
     }
 
     private void recalcInsets(Resources res) {
-        computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets,
-                mHasNavigationBar);
+        computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mInsetsState, mUiMode,
+                mNonDecorInsets, mHasNavigationBar);
         mStableInsets.set(mNonDecorInsets);
         if (mHasStatusBar) {
             convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar);
@@ -244,11 +274,33 @@
         return mWidth > mHeight;
     }
 
-    /** Get the navbar frame height (used by ime). */
+    /** Get the navbar frame (or window) height (used by ime). */
     public int navBarFrameHeight() {
         return mNavBarFrameHeight;
     }
 
+    /** @return whether we can seamlessly rotate even if nav-bar can change sides. */
+    public boolean allowSeamlessRotationDespiteNavBarMoving() {
+        return mAllowSeamlessRotationDespiteNavBarMoving;
+    }
+
+    /** @return whether the navigation bar will change sides during rotation. */
+    public boolean navigationBarCanMove() {
+        return mNavigationBarCanMove;
+    }
+
+    /** @return the rotation that would make the physical display "upside down". */
+    public int getUpsideDownRotation() {
+        boolean displayHardwareIsLandscape = mWidth > mHeight;
+        if ((mRotation % 2) != 0) {
+            displayHardwareIsLandscape = !displayHardwareIsLandscape;
+        }
+        if (displayHardwareIsLandscape) {
+            return mReverseDefaultRotation ? Surface.ROTATION_270 : Surface.ROTATION_90;
+        }
+        return Surface.ROTATION_180;
+    }
+
     /** Gets the orientation of this layout */
     public int getOrientation() {
         return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
@@ -291,21 +343,29 @@
      * @param outInsets the insets to return
      */
     static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth,
-            int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
-            boolean hasNavigationBar) {
+            int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode,
+            Rect outInsets, boolean hasNavigationBar) {
         outInsets.setEmpty();
 
         // Only navigation bar
         if (hasNavigationBar) {
+            final InsetsSource extraNavBar = insetsState.getSource(ITYPE_EXTRA_NAVIGATION_BAR);
+            final boolean hasExtraNav = extraNavBar != null && extraNavBar.isVisible();
             int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
             int navBarSize =
                     getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode);
             if (position == NAV_BAR_BOTTOM) {
-                outInsets.bottom = navBarSize;
+                outInsets.bottom = hasExtraNav
+                        ? Math.max(navBarSize, extraNavBar.getFrame().height())
+                        : navBarSize;
             } else if (position == NAV_BAR_RIGHT) {
-                outInsets.right = navBarSize;
+                outInsets.right = hasExtraNav
+                        ? Math.max(navBarSize, extraNavBar.getFrame().width())
+                        : navBarSize;
             } else if (position == NAV_BAR_LEFT) {
-                outInsets.left = navBarSize;
+                outInsets.left = hasExtraNav
+                        ? Math.max(navBarSize, extraNavBar.getFrame().width())
+                        : navBarSize;
             }
         }
 
@@ -327,13 +387,13 @@
      * @param outInsets the insets to return
      */
     static void computeStableInsets(Resources res, int displayRotation, int displayWidth,
-            int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
-            boolean hasNavigationBar, boolean hasStatusBar) {
+            int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode,
+            Rect outInsets, boolean hasNavigationBar, boolean hasStatusBar) {
         outInsets.setEmpty();
 
         // Navigation bar and status bar.
         computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout,
-                uiMode, outInsets, hasNavigationBar);
+                insetsState, uiMode, outInsets, hasNavigationBar);
         convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight,
                 hasStatusBar);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
index 33beab5..4c0281d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
@@ -18,13 +18,15 @@
 
 import android.annotation.BinderThread;
 import android.annotation.NonNull;
+import android.os.RemoteException;
 import android.util.Slog;
 import android.view.SurfaceControl;
+import android.view.WindowManager;
 import android.window.WindowContainerTransaction;
 import android.window.WindowContainerTransactionCallback;
 import android.window.WindowOrganizer;
 
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.transition.LegacyTransitions;
 
 import java.util.ArrayList;
 
@@ -66,6 +68,10 @@
      * Queues a sync transaction to be sent serially to WM.
      */
     public void queue(WindowContainerTransaction wct) {
+        if (wct.isEmpty()) {
+            if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty");
+            return;
+        }
         SyncCallback cb = new SyncCallback(wct);
         synchronized (mQueue) {
             if (DEBUG) Slog.d(TAG, "Queueing up " + wct);
@@ -77,11 +83,34 @@
     }
 
     /**
+     * Queues a legacy transition to be sent serially to WM
+     */
+    public void queue(LegacyTransitions.ILegacyTransition transition,
+            @WindowManager.TransitionType int type, WindowContainerTransaction wct) {
+        if (wct.isEmpty()) {
+            if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty");
+            return;
+        }
+        SyncCallback cb = new SyncCallback(transition, type, wct);
+        synchronized (mQueue) {
+            if (DEBUG) Slog.d(TAG, "Queueing up legacy transition " + wct);
+            mQueue.add(cb);
+            if (mQueue.size() == 1) {
+                cb.send();
+            }
+        }
+    }
+
+    /**
      * Queues a sync transaction only if there are already sync transaction(s) queued or in flight.
      * Otherwise just returns without queueing.
      * @return {@code true} if queued, {@code false} if not.
      */
     public boolean queueIfWaiting(WindowContainerTransaction wct) {
+        if (wct.isEmpty()) {
+            if (DEBUG) Slog.d(TAG, "Skip queueIfWaiting due to transaction change is empty");
+            return false;
+        }
         synchronized (mQueue) {
             if (mQueue.isEmpty()) {
                 if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct);
@@ -118,12 +147,12 @@
     // Synchronized on mQueue
     private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) {
         if (DEBUG) Slog.d(TAG, "  Running " + mRunnables.size() + " sync runnables");
-        for (int i = 0, n = mRunnables.size(); i < n; ++i) {
+        final int n = mRunnables.size();
+        for (int i = 0; i < n; ++i) {
             mRunnables.get(i).runWithTransaction(t);
         }
-        mRunnables.clear();
-        t.apply();
-        t.close();
+        // More runnables may have been added, so only remove the ones that ran.
+        mRunnables.subList(0, n).clear();
     }
 
     /** Task to run with transaction. */
@@ -135,20 +164,38 @@
     private class SyncCallback extends WindowContainerTransactionCallback {
         int mId = -1;
         final WindowContainerTransaction mWCT;
+        final LegacyTransitions.LegacyTransition mLegacyTransition;
 
         SyncCallback(WindowContainerTransaction wct) {
             mWCT = wct;
+            mLegacyTransition = null;
+        }
+
+        SyncCallback(LegacyTransitions.ILegacyTransition legacyTransition,
+                @WindowManager.TransitionType int type, WindowContainerTransaction wct) {
+            mWCT = wct;
+            mLegacyTransition = new LegacyTransitions.LegacyTransition(type, legacyTransition);
         }
 
         // Must be sychronized on mQueue
         void send() {
+            if (mInFlight == this) {
+                // This was probably queued up and sent during a sync runnable of the last callback.
+                // Don't queue it again.
+                return;
+            }
             if (mInFlight != null) {
                 throw new IllegalStateException("Sync Transactions must be serialized. In Flight: "
                         + mInFlight.mId + " - " + mInFlight.mWCT);
             }
             mInFlight = this;
             if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT);
-            mId = new WindowOrganizer().applySyncTransaction(mWCT, this);
+            if (mLegacyTransition != null) {
+                mId = new WindowOrganizer().startLegacyTransition(mLegacyTransition.getType(),
+                        mLegacyTransition.getAdapter(), this, mWCT);
+            } else {
+                mId = new WindowOrganizer().applySyncTransaction(mWCT, this);
+            }
             if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId);
             mMainExecutor.executeDelayed(mOnReplyTimeout, REPLY_TIMEOUT);
         }
@@ -169,6 +216,16 @@
                     if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId);
                     mQueue.remove(this);
                     onTransactionReceived(t);
+                    if (mLegacyTransition != null) {
+                        try {
+                            mLegacyTransition.getSyncCallback().onTransactionReady(mId, t);
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "Error sending callback to legacy transition: " + mId, e);
+                        }
+                    } else {
+                        t.apply();
+                        t.close();
+                    }
                     if (!mQueue.isEmpty()) {
                         mQueue.get(0).send();
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
index 218bf47..c76937d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
@@ -70,7 +70,8 @@
     private final Paint mPaint = new Paint();
     private final int mWidth;
     private final int mHeight;
-    private final int mCircleDiameter;
+    private final int mTouchingWidth;
+    private final int mTouchingHeight;
     private int mCurrentWidth;
     private int mCurrentHeight;
     private AnimatorSet mAnimator;
@@ -80,11 +81,12 @@
         super(context, attrs);
         mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null));
         mPaint.setAntiAlias(true);
-        mWidth = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_width);
-        mHeight = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_height);
+        mWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_width);
+        mHeight = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_height);
         mCurrentWidth = mWidth;
         mCurrentHeight = mHeight;
-        mCircleDiameter = (mWidth + mHeight) / 3;
+        mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth;
+        mTouchingHeight = mHeight > mWidth ? mHeight / 2 : mHeight;
     }
 
     /** Sets touching state for this handle view. */
@@ -98,16 +100,16 @@
         }
         if (!animate) {
             if (touching) {
-                mCurrentWidth = mCircleDiameter;
-                mCurrentHeight = mCircleDiameter;
+                mCurrentWidth = mTouchingWidth;
+                mCurrentHeight = mTouchingHeight;
             } else {
                 mCurrentWidth = mWidth;
                 mCurrentHeight = mHeight;
             }
             invalidate();
         } else {
-            animateToTarget(touching ? mCircleDiameter : mWidth,
-                    touching ? mCircleDiameter : mHeight, touching);
+            animateToTarget(touching ? mTouchingWidth : mWidth,
+                    touching ? mTouchingHeight : mHeight, touching);
         }
         mTouching = touching;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
new file mode 100644
index 0000000..364bb65
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.split;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
+import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
+import static android.view.RoundedCorner.POSITION_TOP_LEFT;
+import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.RoundedCorner;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+
+/**
+ * Draws inverted rounded corners beside divider bar to keep splitting tasks cropped with proper
+ * rounded corners.
+ */
+public class DividerRoundedCorner extends View {
+    private final int mDividerWidth;
+    private final Paint mDividerBarBackground;
+    private final Point mStartPos = new Point();
+    private InvertedRoundedCornerDrawInfo mTopLeftCorner;
+    private InvertedRoundedCornerDrawInfo mTopRightCorner;
+    private InvertedRoundedCornerDrawInfo mBottomLeftCorner;
+    private InvertedRoundedCornerDrawInfo mBottomRightCorner;
+
+    public DividerRoundedCorner(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        mDividerWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_bar_width);
+        mDividerBarBackground = new Paint();
+        mDividerBarBackground.setColor(
+                getResources().getColor(R.color.split_divider_background, null));
+        mDividerBarBackground.setFlags(Paint.ANTI_ALIAS_FLAG);
+        mDividerBarBackground.setStyle(Paint.Style.FILL);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mTopLeftCorner = new InvertedRoundedCornerDrawInfo(POSITION_TOP_LEFT);
+        mTopRightCorner = new InvertedRoundedCornerDrawInfo(POSITION_TOP_RIGHT);
+        mBottomLeftCorner = new InvertedRoundedCornerDrawInfo(POSITION_BOTTOM_LEFT);
+        mBottomRightCorner = new InvertedRoundedCornerDrawInfo(POSITION_BOTTOM_RIGHT);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.save();
+
+        mTopLeftCorner.calculateStartPos(mStartPos);
+        canvas.translate(mStartPos.x, mStartPos.y);
+        canvas.drawPath(mTopLeftCorner.mPath, mDividerBarBackground);
+
+        canvas.translate(-mStartPos.x, -mStartPos.y);
+        mTopRightCorner.calculateStartPos(mStartPos);
+        canvas.translate(mStartPos.x, mStartPos.y);
+        canvas.drawPath(mTopRightCorner.mPath, mDividerBarBackground);
+
+        canvas.translate(-mStartPos.x, -mStartPos.y);
+        mBottomLeftCorner.calculateStartPos(mStartPos);
+        canvas.translate(mStartPos.x, mStartPos.y);
+        canvas.drawPath(mBottomLeftCorner.mPath, mDividerBarBackground);
+
+        canvas.translate(-mStartPos.x, -mStartPos.y);
+        mBottomRightCorner.calculateStartPos(mStartPos);
+        canvas.translate(mStartPos.x, mStartPos.y);
+        canvas.drawPath(mBottomRightCorner.mPath, mDividerBarBackground);
+
+        canvas.restore();
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    private boolean isLandscape() {
+        return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
+    }
+
+    /**
+     * Holds draw information of the inverted rounded corner at a specific position.
+     *
+     * @see {@link com.android.launcher3.taskbar.TaskbarDragLayer}
+     */
+    private class InvertedRoundedCornerDrawInfo {
+        @RoundedCorner.Position
+        private final int mCornerPosition;
+
+        private final int mRadius;
+
+        private final Path mPath = new Path();
+
+        InvertedRoundedCornerDrawInfo(@RoundedCorner.Position int cornerPosition) {
+            mCornerPosition = cornerPosition;
+
+            final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(cornerPosition);
+            mRadius = roundedCorner == null ? 0 : roundedCorner.getRadius();
+
+            // Starts with a filled square, and then subtracting out a circle from the appropriate
+            // corner.
+            final Path square = new Path();
+            square.addRect(0, 0, mRadius, mRadius, Path.Direction.CW);
+            final Path circle = new Path();
+            circle.addCircle(
+                    isLeftCorner() ? mRadius : 0 /* x */,
+                    isTopCorner() ? mRadius : 0 /* y */,
+                    mRadius, Path.Direction.CW);
+            mPath.op(square, circle, Path.Op.DIFFERENCE);
+        }
+
+        private void calculateStartPos(Point outPos) {
+            if (isLandscape()) {
+                // Place left corner at the right side of the divider bar.
+                outPos.x = isLeftCorner()
+                        ? getWidth() / 2 + mDividerWidth / 2
+                        : getWidth() / 2 - mDividerWidth / 2 - mRadius;
+                outPos.y = isTopCorner() ? 0 : getHeight() - mRadius;
+            } else {
+                outPos.x = isLeftCorner() ? 0 : getWidth() - mRadius;
+                // Place top corner at the bottom of the divider bar.
+                outPos.y = isTopCorner()
+                        ? getHeight() / 2 + mDividerWidth / 2
+                        : getHeight() / 2 - mDividerWidth / 2 - mRadius;
+            }
+        }
+
+        private boolean isLeftCorner() {
+            return mCornerPosition == POSITION_TOP_LEFT || mCornerPosition == POSITION_BOTTOM_LEFT;
+        }
+
+        private boolean isTopCorner() {
+            return mCornerPosition == POSITION_TOP_LEFT || mCornerPosition == POSITION_TOP_RIGHT;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index cba019a..6ea806b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -19,14 +19,21 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
 
+import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Property;
 import android.view.GestureDetector;
+import android.view.InsetsController;
+import android.view.InsetsSource;
+import android.view.InsetsState;
 import android.view.MotionEvent;
 import android.view.SurfaceControlViewHost;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
@@ -44,6 +51,9 @@
     public static final long TOUCH_ANIMATION_DURATION = 150;
     public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;
 
+    /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */
+    private float mExpandedTaskBarHeight;
+
     private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
 
     private SplitLayout mSplitLayout;
@@ -58,6 +68,31 @@
     private GestureDetector mDoubleTapDetector;
     private boolean mInteractive;
 
+    /**
+     * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
+     * insets.
+     */
+    private final Rect mDividerBounds = new Rect();
+    private final Rect mTempRect = new Rect();
+    private FrameLayout mDividerBar;
+
+
+    static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY =
+            new Property<DividerView, Integer>(Integer.class, "height") {
+                @Override
+                public Integer get(DividerView object) {
+                    return object.mDividerBar.getLayoutParams().height;
+                }
+
+                @Override
+                public void set(DividerView object, Integer value) {
+                    ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
+                            object.mDividerBar.getLayoutParams();
+                    lp.height = value;
+                    object.mDividerBar.setLayoutParams(lp);
+                }
+            };
+
     public DividerView(@NonNull Context context) {
         super(context);
     }
@@ -79,16 +114,46 @@
     /** Sets up essential dependencies of the divider bar. */
     public void setup(
             SplitLayout layout,
-            SurfaceControlViewHost viewHost) {
+            SurfaceControlViewHost viewHost,
+            InsetsState insetsState) {
         mSplitLayout = layout;
         mViewHost = viewHost;
+        mDividerBounds.set(layout.getDividerBounds());
+        onInsetsChanged(insetsState, false /* animate */);
+    }
+
+    void onInsetsChanged(InsetsState insetsState, boolean animate) {
+        mTempRect.set(mSplitLayout.getDividerBounds());
+        final InsetsSource taskBarInsetsSource =
+                insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        // Only insets the divider bar with task bar when it's expanded so that the rounded corners
+        // will be drawn against task bar.
+        if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+            mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect));
+        }
+
+        if (!mTempRect.equals(mDividerBounds)) {
+            if (animate) {
+                ObjectAnimator animator = ObjectAnimator.ofInt(this,
+                        DIVIDER_HEIGHT_PROPERTY, mDividerBounds.height(), mTempRect.height());
+                animator.setInterpolator(InsetsController.RESIZE_INTERPOLATOR);
+                animator.setDuration(InsetsController.ANIMATION_DURATION_RESIZE);
+                animator.start();
+            } else {
+                DIVIDER_HEIGHT_PROPERTY.set(this, mTempRect.height());
+            }
+            mDividerBounds.set(mTempRect);
+        }
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        mDividerBar = findViewById(R.id.divider_bar);
         mHandle = findViewById(R.id.docked_divider_handle);
         mBackground = findViewById(R.id.docked_divider_background);
+        mExpandedTaskBarHeight = getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
         mTouchElevation = getResources().getDimensionPixelSize(
                 R.dimen.docked_stack_divider_lift_elevation);
         mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener());
@@ -106,10 +171,12 @@
             return true;
         }
 
+        // Convert to use screen-based coordinates to prevent lost track of motion events while
+        // moving divider bar and calculating dragging velocity.
+        event.setLocation(event.getRawX(), event.getRawY());
         final int action = event.getAction() & MotionEvent.ACTION_MASK;
         final boolean isLandscape = isLandscape();
-        // Using raw xy to prevent lost track of motion events while moving divider bar.
-        final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY();
+        final int touchPos = (int) (isLandscape ? event.getX() : event.getY());
         switch (action) {
             case MotionEvent.ACTION_DOWN:
                 mVelocityTracker = VelocityTracker.obtain();
@@ -153,16 +220,6 @@
     private void setTouching() {
         setSlippery(false);
         mHandle.setTouching(true, true);
-        if (isLandscape()) {
-            mBackground.animate().scaleX(1.4f);
-        } else {
-            mBackground.animate().scaleY(1.4f);
-        }
-        mBackground.animate()
-                .setInterpolator(Interpolators.TOUCH_RESPONSE)
-                .setDuration(TOUCH_ANIMATION_DURATION)
-                .translationZ(mTouchElevation)
-                .start();
         // Lift handle as well so it doesn't get behind the background, even though it doesn't
         // cast shadow.
         mHandle.animate()
@@ -175,13 +232,6 @@
     private void releaseTouching() {
         setSlippery(true);
         mHandle.setTouching(false, true);
-        mBackground.animate()
-                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
-                .translationZ(0)
-                .scaleX(1f)
-                .scaleY(1f)
-                .start();
         mHandle.animate()
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 5b158d2..5b3ce2d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -16,11 +16,19 @@
 
 package com.android.wm.shell.common.split;
 
+import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+import static android.view.WindowManager.DOCKED_BOTTOM;
+import static android.view.WindowManager.DOCKED_INVALID;
 import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_RIGHT;
 import static android.view.WindowManager.DOCKED_TOP;
+import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
 
 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
 import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;
+import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR;
+import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -30,7 +38,10 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.Point;
 import android.graphics.Rect;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
 import android.view.SurfaceControl;
 import android.view.WindowInsets;
 import android.view.WindowManager;
@@ -39,16 +50,19 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.internal.policy.DockedDividerUtils;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 
 /**
  * Records and handles layout of splits. Helps to calculate proper bounds when configuration or
  * divide position changes.
  */
-public final class SplitLayout {
+public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener {
     /**
      * Split position isn't specified normally meaning to use what ever it is currently set to.
      */
@@ -78,32 +92,44 @@
     private final int mDividerInsets;
     private final int mDividerSize;
 
+    private final Rect mTempRect = new Rect();
     private final Rect mRootBounds = new Rect();
     private final Rect mDividerBounds = new Rect();
     private final Rect mBounds1 = new Rect();
     private final Rect mBounds2 = new Rect();
+    private final Rect mWinBounds1 = new Rect();
+    private final Rect mWinBounds2 = new Rect();
     private final SplitLayoutHandler mSplitLayoutHandler;
     private final SplitWindowManager mSplitWindowManager;
     private final DisplayImeController mDisplayImeController;
     private final ImePositionProcessor mImePositionProcessor;
+    private final DismissingParallaxPolicy mDismissingParallaxPolicy;
     private final ShellTaskOrganizer mTaskOrganizer;
+    private final InsetsState mInsetsState = new InsetsState();
 
     private Context mContext;
     private DividerSnapAlgorithm mDividerSnapAlgorithm;
+    private WindowContainerToken mWinToken1;
+    private WindowContainerToken mWinToken2;
     private int mDividePosition;
     private boolean mInitialized = false;
+    private int mOrientation;
+    private int mRotation;
 
     public SplitLayout(String windowName, Context context, Configuration configuration,
             SplitLayoutHandler splitLayoutHandler,
             SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
             DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer) {
         mContext = context.createConfigurationContext(configuration);
+        mOrientation = configuration.orientation;
+        mRotation = configuration.windowConfiguration.getRotation();
         mSplitLayoutHandler = splitLayoutHandler;
         mDisplayImeController = displayImeController;
-        mSplitWindowManager = new SplitWindowManager(
-                windowName, mContext, configuration, parentContainerCallbacks);
+        mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration,
+                parentContainerCallbacks);
         mTaskOrganizer = taskOrganizer;
         mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId());
+        mDismissingParallaxPolicy = new DismissingParallaxPolicy();
 
         final Resources resources = context.getResources();
         mDividerWindowWidth = resources.getDimensionPixelSize(
@@ -142,35 +168,71 @@
         return mDividePosition;
     }
 
+    /**
+     * Returns the divider position as a fraction from 0 to 1.
+     */
+    public float getDividerPositionAsFraction() {
+        return Math.min(1f, Math.max(0f, isLandscape()
+                ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right
+                : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom));
+    }
+
     /** Applies new configuration, returns {@code false} if there's no effect to the layout. */
     public boolean updateConfiguration(Configuration configuration) {
+        boolean affectsLayout = false;
+
+        // Update the split bounds when necessary. Besides root bounds changed, split bounds need to
+        // be updated when the rotation changed to cover the case that users rotated the screen 180
+        // degrees.
+        // Make sure to render the divider bar with proper resources that matching the screen
+        // orientation.
+        final int rotation = configuration.windowConfiguration.getRotation();
         final Rect rootBounds = configuration.windowConfiguration.getBounds();
-        if (mRootBounds.equals(rootBounds)) {
+        final int orientation = configuration.orientation;
+
+        if (mOrientation == orientation
+                && rotation == mRotation
+                && mRootBounds.equals(rootBounds)) {
             return false;
         }
 
         mContext = mContext.createConfigurationContext(configuration);
         mSplitWindowManager.setConfiguration(configuration);
+        mOrientation = orientation;
+        mTempRect.set(mRootBounds);
         mRootBounds.set(rootBounds);
+        mRotation = rotation;
         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
-        resetDividerPosition();
+        initDividerPosition(mTempRect);
 
-        // Don't inflate divider bar if it is not initialized.
-        if (!mInitialized) {
-            return false;
+        if (mInitialized) {
+            release();
+            init();
         }
 
-        release();
-        init();
         return true;
     }
 
+    private void initDividerPosition(Rect oldBounds) {
+        final float snapRatio = (float) mDividePosition
+                / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height());
+        // Estimate position by previous ratio.
+        final float length =
+                (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height());
+        final int estimatePosition = (int) (length * snapRatio);
+        // Init divider position by estimated position using current bounds snap algorithm.
+        mDividePosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
+                estimatePosition).position;
+        updateBounds(mDividePosition);
+    }
+
     /** Updates recording bounds of divider window and both of the splits. */
     private void updateBounds(int position) {
         mDividerBounds.set(mRootBounds);
         mBounds1.set(mRootBounds);
         mBounds2.set(mRootBounds);
-        if (isLandscape(mRootBounds)) {
+        final boolean isLandscape = isLandscape(mRootBounds);
+        if (isLandscape) {
             position += mRootBounds.left;
             mDividerBounds.left = position - mDividerInsets;
             mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth;
@@ -183,13 +245,16 @@
             mBounds1.bottom = position;
             mBounds2.top = mBounds1.bottom + mDividerSize;
         }
+        DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */);
+        DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */);
+        mDismissingParallaxPolicy.applyDividerPosition(position, isLandscape);
     }
 
     /** Inflates {@link DividerView} on the root surface. */
     public void init() {
         if (mInitialized) return;
         mInitialized = true;
-        mSplitWindowManager.init(this);
+        mSplitWindowManager.init(this, mInsetsState);
         mDisplayImeController.addPositionProcessor(mImePositionProcessor);
     }
 
@@ -202,6 +267,23 @@
         mImePositionProcessor.reset();
     }
 
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mInsetsState.set(insetsState);
+        if (!mInitialized) {
+            return;
+        }
+        mSplitWindowManager.onInsetsChanged(insetsState);
+    }
+
+    @Override
+    public void insetsControlChanged(InsetsState insetsState,
+            InsetsSourceControl[] activeControls) {
+        if (!mInsetsState.equals(insetsState)) {
+            insetsChanged(insetsState);
+        }
+    }
+
     /**
      * Updates bounds with the passing position. Usually used to update recording bounds while
      * performing animation or dragging divider bar to resize the splits.
@@ -209,20 +291,25 @@
     void updateDivideBounds(int position) {
         updateBounds(position);
         mSplitWindowManager.setResizingSplits(true);
-        mSplitLayoutHandler.onBoundsChanging(this);
+        mSplitLayoutHandler.onLayoutSizeChanging(this);
     }
 
     void setDividePosition(int position) {
         mDividePosition = position;
         updateBounds(mDividePosition);
-        mSplitLayoutHandler.onBoundsChanged(this);
+        mSplitLayoutHandler.onLayoutSizeChanged(this);
         mSplitWindowManager.setResizingSplits(false);
     }
 
     /** Resets divider position. */
     public void resetDividerPosition() {
         mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
+        mSplitWindowManager.setResizingSplits(false);
         updateBounds(mDividePosition);
+        mWinToken1 = null;
+        mWinToken2 = null;
+        mWinBounds1.setEmpty();
+        mWinBounds2.setEmpty();
     }
 
     /**
@@ -232,15 +319,15 @@
     public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
         switch (snapTarget.flag) {
             case FLAG_DISMISS_START:
-                mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */);
-                mSplitWindowManager.setResizingSplits(false);
+                flingDividePosition(currentPosition, snapTarget.position,
+                        () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */));
                 break;
             case FLAG_DISMISS_END:
-                mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */);
-                mSplitWindowManager.setResizingSplits(false);
+                flingDividePosition(currentPosition, snapTarget.position,
+                        () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */));
                 break;
             default:
-                flingDividePosition(currentPosition, snapTarget.position);
+                flingDividePosition(currentPosition, snapTarget.position, null);
                 break;
         }
     }
@@ -270,8 +357,13 @@
                 isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
     }
 
-    private void flingDividePosition(int from, int to) {
-        if (from == to) return;
+    @VisibleForTesting
+    void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) {
+        if (from == to) {
+            // No animation run, it should stop resizing here.
+            mSplitWindowManager.setResizingSplits(false);
+            return;
+        }
         ValueAnimator animator = ValueAnimator
                 .ofInt(from, to)
                 .setDuration(250);
@@ -282,6 +374,9 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 setDividePosition(to);
+                if (flingFinishedCallback != null) {
+                    flingFinishedCallback.run();
+                }
             }
 
             @Override
@@ -296,42 +391,99 @@
         return context.getSystemService(WindowManager.class)
                 .getMaximumWindowMetrics()
                 .getWindowInsets()
-                .getInsets(WindowInsets.Type.navigationBars()
-                        | WindowInsets.Type.statusBars()
-                        | WindowInsets.Type.displayCutout()).toRect();
+                .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout())
+                .toRect();
     }
 
     private static boolean isLandscape(Rect bounds) {
         return bounds.width() > bounds.height();
     }
 
+    /**
+     * Return if this layout is landscape.
+     */
+    public boolean isLandscape() {
+        return isLandscape(mRootBounds);
+    }
+
     /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */
     public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1,
             SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
-        final Rect dividerBounds = mImePositionProcessor.adjustForIme(mDividerBounds);
-        final Rect bounds1 = mImePositionProcessor.adjustForIme(mBounds1);
-        final Rect bounds2 = mImePositionProcessor.adjustForIme(mBounds2);
         final SurfaceControl dividerLeash = getDividerLeash();
         if (dividerLeash != null) {
-            t.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
-                    // Resets layer of divider bar to make sure it is always on top.
-                    .setLayer(dividerLeash, Integer.MAX_VALUE);
+            t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top);
+            // Resets layer of divider bar to make sure it is always on top.
+            t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER);
+        }
+        t.setPosition(leash1, mBounds1.left, mBounds1.top)
+                .setWindowCrop(leash1, mBounds1.width(), mBounds1.height());
+        t.setPosition(leash2, mBounds2.left, mBounds2.top)
+                .setWindowCrop(leash2, mBounds2.width(), mBounds2.height());
+
+        if (mImePositionProcessor.adjustSurfaceLayoutForIme(
+                t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) {
+            return;
         }
 
-        t.setPosition(leash1, bounds1.left, bounds1.top)
-                .setWindowCrop(leash1, bounds1.width(), bounds1.height());
-
-        t.setPosition(leash2, bounds2.left, bounds2.top)
-                .setWindowCrop(leash2, bounds2.width(), bounds2.height());
-
-        mImePositionProcessor.applySurfaceDimValues(t, dimLayer1, dimLayer2);
+        mDismissingParallaxPolicy.adjustDismissingSurface(t, leash1, leash2, dimLayer1, dimLayer2);
     }
 
     /** Apply recorded task layout to the {@link WindowContainerTransaction}. */
     public void applyTaskChanges(WindowContainerTransaction wct,
             ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
-        wct.setBounds(task1.token, mImePositionProcessor.adjustForIme(mBounds1))
-                .setBounds(task2.token, mImePositionProcessor.adjustForIme(mBounds2));
+        if (mImePositionProcessor.applyTaskLayoutForIme(wct, task1.token, task2.token)) {
+            return;
+        }
+
+        if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
+            wct.setBounds(task1.token, mBounds1);
+            mWinBounds1.set(mBounds1);
+            mWinToken1 = task1.token;
+        }
+        if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
+            wct.setBounds(task2.token, mBounds2);
+            mWinBounds2.set(mBounds2);
+            mWinToken2 = task2.token;
+        }
+    }
+
+    /**
+     * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And
+     * restore shifted configuration bounds if it's no longer shifted.
+     */
+    public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY,
+            ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) {
+        if (offsetX == 0 && offsetY == 0) {
+            wct.setBounds(taskInfo1.token, mBounds1);
+            wct.setAppBounds(taskInfo1.token, null);
+            wct.setScreenSizeDp(taskInfo1.token,
+                    SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
+
+            wct.setBounds(taskInfo2.token, mBounds2);
+            wct.setAppBounds(taskInfo2.token, null);
+            wct.setScreenSizeDp(taskInfo2.token,
+                    SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
+        } else {
+            mTempRect.set(taskInfo1.configuration.windowConfiguration.getBounds());
+            mTempRect.offset(offsetX, offsetY);
+            wct.setBounds(taskInfo1.token, mTempRect);
+            mTempRect.set(taskInfo1.configuration.windowConfiguration.getAppBounds());
+            mTempRect.offset(offsetX, offsetY);
+            wct.setAppBounds(taskInfo1.token, mTempRect);
+            wct.setScreenSizeDp(taskInfo1.token,
+                    taskInfo1.configuration.screenWidthDp,
+                    taskInfo1.configuration.screenHeightDp);
+
+            mTempRect.set(taskInfo2.configuration.windowConfiguration.getBounds());
+            mTempRect.offset(offsetX, offsetY);
+            wct.setBounds(taskInfo2.token, mTempRect);
+            mTempRect.set(taskInfo2.configuration.windowConfiguration.getAppBounds());
+            mTempRect.offset(offsetX, offsetY);
+            wct.setAppBounds(taskInfo2.token, mTempRect);
+            wct.setScreenSizeDp(taskInfo2.token,
+                    taskInfo2.configuration.screenWidthDp,
+                    taskInfo2.configuration.screenHeightDp);
+        }
     }
 
     /** Handles layout change event. */
@@ -340,11 +492,43 @@
         /** Calls when dismissing split. */
         void onSnappedToDismiss(boolean snappedToEnd);
 
-        /** Calls when the bounds is changing due to animation or dragging divider bar. */
-        void onBoundsChanging(SplitLayout layout);
+        /**
+         * Calls when resizing the split bounds.
+         *
+         * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
+         * SurfaceControl, SurfaceControl)
+         */
+        void onLayoutSizeChanging(SplitLayout layout);
 
-        /** Calls when the target bounds changed. */
-        void onBoundsChanged(SplitLayout layout);
+        /**
+         * Calls when finish resizing the split bounds.
+         *
+         * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo,
+         * ActivityManager.RunningTaskInfo)
+         * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
+         * SurfaceControl, SurfaceControl)
+         */
+        void onLayoutSizeChanged(SplitLayout layout);
+
+        /**
+         * Calls when re-positioning the split bounds. Like moving split bounds while showing IME
+         * panel.
+         *
+         * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
+         * SurfaceControl, SurfaceControl)
+         */
+        void onLayoutPositionChanging(SplitLayout layout);
+
+        /**
+         * Notifies the target offset for shifting layout. So layout handler can shift configuration
+         * bounds correspondingly to make sure client apps won't get configuration changed or
+         * relaunched. If the layout is no longer shifted, layout handler should restore shifted
+         * configuration bounds.
+         *
+         * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int,
+         * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo)
+         */
+        void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout);
 
         /** Calls when user double tapped on the divider bar. */
         default void onDoubleTappedDivider() {
@@ -355,6 +539,106 @@
         int getSplitItemPosition(WindowContainerToken token);
     }
 
+    /**
+     * Calculates and applies proper dismissing parallax offset and dimming value to hint users
+     * dismissing gesture.
+     */
+    private class DismissingParallaxPolicy {
+        // The current dismissing side.
+        int mDismissingSide = DOCKED_INVALID;
+
+        // The parallax offset to hint the dismissing side and progress.
+        final Point mDismissingParallaxOffset = new Point();
+
+        // The dimming value to hint the dismissing side and progress.
+        float mDismissingDimValue = 0.0f;
+
+        /**
+         * Applies a parallax to the task to hint dismissing progress.
+         *
+         * @param position    the split position to apply dismissing parallax effect
+         * @param isLandscape indicates whether it's splitting horizontally or vertically
+         */
+        void applyDividerPosition(int position, boolean isLandscape) {
+            mDismissingSide = DOCKED_INVALID;
+            mDismissingParallaxOffset.set(0, 0);
+            mDismissingDimValue = 0;
+
+            int totalDismissingDistance = 0;
+            if (position <= mDividerSnapAlgorithm.getFirstSplitTarget().position) {
+                mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP;
+                totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position
+                        - mDividerSnapAlgorithm.getFirstSplitTarget().position;
+            } else if (position >= mDividerSnapAlgorithm.getLastSplitTarget().position) {
+                mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM;
+                totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position
+                        - mDividerSnapAlgorithm.getDismissEndTarget().position;
+            }
+
+            if (mDismissingSide != DOCKED_INVALID) {
+                float fraction = Math.max(0,
+                        Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f));
+                mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
+                fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
+                if (isLandscape) {
+                    mDismissingParallaxOffset.x = (int) (fraction * totalDismissingDistance);
+                } else {
+                    mDismissingParallaxOffset.y = (int) (fraction * totalDismissingDistance);
+                }
+            }
+        }
+
+        /**
+         * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
+         * slowing down parallax effect
+         */
+        private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
+            float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;
+
+            // Less parallax at the top, just because.
+            if (dockSide == WindowManager.DOCKED_TOP) {
+                result /= 2f;
+            }
+            return result;
+        }
+
+        /** Applies parallax offset and dimming value to the root surface at the dismissing side. */
+        boolean adjustDismissingSurface(SurfaceControl.Transaction t,
+                SurfaceControl leash1, SurfaceControl leash2,
+                SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
+            SurfaceControl targetLeash, targetDimLayer;
+            switch (mDismissingSide) {
+                case DOCKED_TOP:
+                case DOCKED_LEFT:
+                    targetLeash = leash1;
+                    targetDimLayer = dimLayer1;
+                    mTempRect.set(mBounds1);
+                    break;
+                case DOCKED_BOTTOM:
+                case DOCKED_RIGHT:
+                    targetLeash = leash2;
+                    targetDimLayer = dimLayer2;
+                    mTempRect.set(mBounds2);
+                    break;
+                case DOCKED_INVALID:
+                default:
+                    t.setAlpha(dimLayer1, 0).hide(dimLayer1);
+                    t.setAlpha(dimLayer2, 0).hide(dimLayer2);
+                    return false;
+            }
+
+            t.setPosition(targetLeash,
+                    mTempRect.left + mDismissingParallaxOffset.x,
+                    mTempRect.top + mDismissingParallaxOffset.y);
+            // Transform the screen-based split bounds to surface-based crop bounds.
+            mTempRect.offsetTo(-mDismissingParallaxOffset.x, -mDismissingParallaxOffset.y);
+            t.setWindowCrop(targetLeash, mTempRect);
+            t.setAlpha(targetDimLayer, mDismissingDimValue)
+                    .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
+            return true;
+        }
+    }
+
     /** Records IME top offset changes and updates SplitLayout correspondingly. */
     private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor {
         /**
@@ -409,6 +693,18 @@
                     && !isFloating && !isLandscape(mRootBounds) && showing;
             mTargetYOffset = needOffset ? getTargetYOffset() : 0;
 
+            if (mTargetYOffset != mLastYOffset) {
+                // Freeze the configuration size with offset to prevent app get a configuration
+                // changed or relaunch. This is required to make sure client apps will calculate
+                // insets properly after layout shifted.
+                if (mTargetYOffset == 0) {
+                    mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
+                } else {
+                    mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset - mLastYOffset,
+                            SplitLayout.this);
+                }
+            }
+
             // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to
             // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
             // because DividerView won't receive onImeVisibilityChanged callback after it being
@@ -423,7 +719,7 @@
         public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
             if (displayId != mDisplayId) return;
             onProgress(getProgress(imeTop));
-            mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
+            mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
         }
 
         @Override
@@ -431,7 +727,7 @@
                 SurfaceControl.Transaction t) {
             if (displayId != mDisplayId || cancel) return;
             onProgress(1.0f);
-            mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
+            mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
         }
 
         @Override
@@ -441,7 +737,7 @@
             if (!controlling && mImeShown) {
                 reset();
                 mSplitWindowManager.setInteractive(true);
-                mSplitLayoutHandler.onBoundsChanging(SplitLayout.this);
+                mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
             }
         }
 
@@ -473,24 +769,66 @@
             return start + (end - start) * progress;
         }
 
-        private void reset() {
+        void reset() {
             mImeShown = false;
             mYOffsetForIme = mLastYOffset = mTargetYOffset = 0;
             mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f;
             mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f;
         }
 
-        /* Adjust bounds with IME offset. */
-        private Rect adjustForIme(Rect bounds) {
-            final Rect temp = new Rect(bounds);
-            if (mYOffsetForIme != 0) temp.offset(0, mYOffsetForIme);
-            return temp;
+        /**
+         * Applies adjusted task layout for showing IME.
+         *
+         * @return {@code false} if there's no need to adjust, otherwise {@code true}
+         */
+        boolean applyTaskLayoutForIme(WindowContainerTransaction wct,
+                WindowContainerToken token1, WindowContainerToken token2) {
+            if (mYOffsetForIme == 0) return false;
+
+            mTempRect.set(mBounds1);
+            mTempRect.offset(0, mYOffsetForIme);
+            wct.setBounds(token1, mTempRect);
+
+            mTempRect.set(mBounds2);
+            mTempRect.offset(0, mYOffsetForIme);
+            wct.setBounds(token2, mTempRect);
+
+            return true;
         }
 
-        private void applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1,
-                SurfaceControl dimLayer2) {
-            t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f);
-            t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f);
+        /**
+         * Adjusts surface layout while showing IME.
+         *
+         * @return {@code false} if there's no need to adjust, otherwise {@code true}
+         */
+        boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t,
+                SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2,
+                SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
+            final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f;
+            boolean adjusted = false;
+            if (mYOffsetForIme != 0) {
+                if (dividerLeash != null) {
+                    mTempRect.set(mDividerBounds);
+                    mTempRect.offset(0, mYOffsetForIme);
+                    t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
+                }
+
+                mTempRect.set(mBounds1);
+                mTempRect.offset(0, mYOffsetForIme);
+                t.setPosition(leash1, mTempRect.left, mTempRect.top);
+
+                mTempRect.set(mBounds2);
+                mTempRect.offset(0, mYOffsetForIme);
+                t.setPosition(leash2, mTempRect.left, mTempRect.top);
+                adjusted = true;
+            }
+
+            if (showDim) {
+                t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f);
+                t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f);
+                adjusted = true;
+            }
+            return adjusted;
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 0cea0ef..47dceb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -36,6 +36,7 @@
 import android.os.RemoteException;
 import android.util.Slog;
 import android.view.IWindow;
+import android.view.InsetsState;
 import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
@@ -63,6 +64,7 @@
 
     public interface ParentContainerCallbacks {
         void attachToParentSurface(SurfaceControl.Builder b);
+        void onLeashReady(SurfaceControl leash);
     }
 
     public SplitWindowManager(String windowName, Context context, Configuration config,
@@ -95,15 +97,16 @@
         final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
                 .setContainerLayer()
                 .setName(TAG)
-                .setHidden(false)
+                .setHidden(true)
                 .setCallsite("SplitWindowManager#attachToParentSurface");
         mParentContainerCallbacks.attachToParentSurface(builder);
         mLeash = builder.build();
+        mParentContainerCallbacks.onLeashReady(mLeash);
         b.setParent(mLeash);
     }
 
     /** Inflates {@link DividerView} on to the root surface. */
-    void init(SplitLayout splitLayout) {
+    void init(SplitLayout splitLayout, InsetsState insetsState) {
         if (mDividerView != null || mViewHost != null) {
             throw new UnsupportedOperationException(
                     "Try to inflate divider view again without release first");
@@ -123,7 +126,7 @@
         lp.setTitle(mWindowName);
         lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
         mViewHost.setView(mDividerView, lp);
-        mDividerView.setup(splitLayout, mViewHost);
+        mDividerView.setup(splitLayout, mViewHost, insetsState);
     }
 
     /**
@@ -169,4 +172,10 @@
     SurfaceControl getSurfaceControl() {
         return mLeash;
     }
+
+    void onInsetsChanged(InsetsState insetsState) {
+        if (mDividerView != null) {
+            mDividerView.onInsetsChanged(insetsState, true /* animate */);
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java
new file mode 100644
index 0000000..defbd5a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.displayareahelper;
+
+import android.view.SurfaceControl;
+
+import java.util.function.Consumer;
+
+/**
+ * Interface that allows to perform various display area related actions
+ */
+public interface DisplayAreaHelper {
+
+    /**
+     * Updates SurfaceControl builder to reparent it to the root display area
+     * @param displayId id of the display to which root display area it should be reparented to
+     * @param builder surface control builder that should be updated
+     * @param onUpdated callback that is invoked after updating the builder, called on
+     *                  the shell main thread
+     */
+    default void attachToRootDisplayArea(int displayId, SurfaceControl.Builder builder,
+            Consumer<SurfaceControl.Builder> onUpdated) {
+    }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java
new file mode 100644
index 0000000..ef9ad6d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.displayareahelper;
+
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.RootDisplayAreaOrganizer;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+public class DisplayAreaHelperController implements DisplayAreaHelper {
+
+    private final Executor mExecutor;
+    private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
+
+    public DisplayAreaHelperController(Executor executor,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer) {
+        mExecutor = executor;
+        mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer;
+    }
+
+    @Override
+    public void attachToRootDisplayArea(int displayId, SurfaceControl.Builder builder,
+            Consumer<SurfaceControl.Builder> onUpdated) {
+        mExecutor.execute(() -> {
+            mRootDisplayAreaOrganizer.attachToDisplayArea(displayId, builder);
+            onUpdated.accept(builder);
+        });
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index 58bf22a..0c12d6c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -34,6 +34,7 @@
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
+import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -48,6 +49,8 @@
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayController;
@@ -67,14 +70,17 @@
 
     private final Context mContext;
     private final DisplayController mDisplayController;
+    private final DragAndDropEventLogger mLogger;
     private SplitScreenController mSplitScreen;
 
     private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>();
     private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
 
-    public DragAndDropController(Context context, DisplayController displayController) {
+    public DragAndDropController(Context context, DisplayController displayController,
+            UiEventLogger uiEventLogger) {
         mContext = context;
         mDisplayController = displayController;
+        mLogger = new DragAndDropEventLogger(uiEventLogger);
     }
 
     public void initialize(Optional<SplitScreenController> splitscreen) {
@@ -175,9 +181,10 @@
                     Slog.w(TAG, "Unexpected drag start during an active drag");
                     return false;
                 }
+                InstanceId loggerSessionId = mLogger.logStart(event);
                 pd.activeDragCount++;
                 pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId),
-                        event.getClipData());
+                        event.getClipData(), loggerSessionId);
                 setDropTargetWindowVisibility(pd, View.VISIBLE);
                 break;
             case ACTION_DRAG_ENTERED:
@@ -198,7 +205,9 @@
             case ACTION_DRAG_ENDED:
                 // TODO(b/169894807): Ensure sure it's not possible to get ENDED without DROP
                 // or EXITED
-                if (!pd.dragLayout.hasDropped()) {
+                if (pd.dragLayout.hasDropped()) {
+                    mLogger.logDrop();
+                } else {
                     pd.activeDragCount--;
                     pd.dragLayout.hide(event, () -> {
                         if (pd.activeDragCount == 0) {
@@ -208,6 +217,7 @@
                         }
                     });
                 }
+                mLogger.logEnd();
                 break;
         }
         return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java
new file mode 100644
index 0000000..6e4b815
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop;
+
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.pm.ActivityInfo;
+import android.view.DragEvent;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+/**
+ * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
+ */
+public class DragAndDropEventLogger {
+
+    private final UiEventLogger mUiEventLogger;
+    // Used to generate instance ids for this drag if one is not provided
+    private final InstanceIdSequence mIdSequence;
+
+    // Tracks the current drag session
+    private ActivityInfo mActivityInfo;
+    private InstanceId mInstanceId;
+
+    public DragAndDropEventLogger(UiEventLogger uiEventLogger) {
+        mUiEventLogger = uiEventLogger;
+        mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
+    }
+
+    /**
+     * Logs the start of a drag.
+     */
+    public InstanceId logStart(DragEvent event) {
+        final ClipDescription description = event.getClipDescription();
+        final ClipData data = event.getClipData();
+        final ClipData.Item item = data.getItemAt(0);
+        mInstanceId = item.getIntent().getParcelableExtra(
+                ClipDescription.EXTRA_LOGGING_INSTANCE_ID);
+        if (mInstanceId == null) {
+            mInstanceId = mIdSequence.newInstanceId();
+        }
+        mActivityInfo = item.getActivityInfo();
+        mUiEventLogger.logWithInstanceId(getStartEnum(description),
+                mActivityInfo.applicationInfo.uid,
+                mActivityInfo.applicationInfo.packageName, mInstanceId);
+        return mInstanceId;
+    }
+
+    /**
+     * Logs a successful drop.
+     */
+    public void logDrop() {
+        mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_DROPPED,
+                mActivityInfo.applicationInfo.uid,
+                mActivityInfo.applicationInfo.packageName, mInstanceId);
+    }
+
+    /**
+     * Logs the end of a drag.
+     */
+    public void logEnd() {
+        mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_END,
+                mActivityInfo.applicationInfo.uid,
+                mActivityInfo.applicationInfo.packageName, mInstanceId);
+    }
+
+    /**
+     * Returns the start logging enum for the given drag description.
+     */
+    private DragAndDropUiEventEnum getStartEnum(ClipDescription description) {
+        if (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY)) {
+            return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_ACTIVITY;
+        } else if (description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT)) {
+            return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_SHORTCUT;
+        } else if (description.hasMimeType(MIMETYPE_APPLICATION_TASK)) {
+            return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_TASK;
+        }
+        throw new IllegalArgumentException("Not an app drag");
+    }
+
+    /**
+     * Enums for logging Drag & Drop UiEvents
+     */
+    public enum DragAndDropUiEventEnum implements UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "Starting a global drag and drop of an activity")
+        GLOBAL_APP_DRAG_START_ACTIVITY(884),
+
+        @UiEvent(doc = "Starting a global drag and drop of a shortcut")
+        GLOBAL_APP_DRAG_START_SHORTCUT(885),
+
+        @UiEvent(doc = "Starting a global drag and drop of a task")
+        GLOBAL_APP_DRAG_START_TASK(888),
+
+        @UiEvent(doc = "A global app drag was successfully dropped")
+        GLOBAL_APP_DRAG_DROPPED(887),
+
+        @UiEvent(doc = "Ending a global app drag and drop")
+        GLOBAL_APP_DRAG_END(886);
+
+        private final int mId;
+
+        DragAndDropUiEventEnum(int id) {
+            mId = id;
+        }
+
+        @Override
+        public int getId() {
+            return mId;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index 9bcc3ac..fbf04d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -63,6 +63,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.internal.logging.InstanceId;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
@@ -86,6 +87,7 @@
     private final SplitScreenController mSplitScreen;
     private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>();
 
+    private InstanceId mLoggerSessionId;
     private DragSession mSession;
 
     public DragAndDropPolicy(Context context, SplitScreenController splitScreen) {
@@ -104,7 +106,8 @@
     /**
      * Starts a new drag session with the given initial drag data.
      */
-    void start(DisplayLayout displayLayout, ClipData data) {
+    void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) {
+        mLoggerSessionId = loggerSessionId;
         mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data);
         // TODO(b/169894807): Also update the session data with task stack changes
         mSession.update();
@@ -151,10 +154,8 @@
                 final Rect rightHitRegion = new Rect();
                 final Rect rightDrawRegion = bottomOrRightBounds;
 
-                displayRegion.splitVertically(leftHitRegion, fullscreenHitRegion, rightHitRegion);
+                displayRegion.splitVertically(leftHitRegion, rightHitRegion);
 
-                mTargets.add(
-                        new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion));
 
@@ -165,10 +166,8 @@
                 final Rect bottomDrawRegion = bottomOrRightBounds;
 
                 displayRegion.splitHorizontally(
-                        topHitRegion, fullscreenHitRegion, bottomHitRegion);
+                        topHitRegion, bottomHitRegion);
 
-                mTargets.add(
-                        new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion));
             }
@@ -211,6 +210,8 @@
                 // Launch in the side stage if we are not in split-screen already.
                 stage = STAGE_TYPE_SIDE;
             }
+            // Add some data for logging splitscreen once it is invoked
+            mSplitScreen.logOnDroppedToSplit(position, mLoggerSessionId);
         }
 
         final ClipDescription description = data.getDescription();
@@ -269,7 +270,6 @@
          * Updates the session data based on the current state of the system.
          */
         void update() {
-
             List<ActivityManager.RunningTaskInfo> tasks =
                     mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */);
             if (!tasks.isEmpty()) {
@@ -299,7 +299,12 @@
                 @StageType int stage, @SplitPosition int position,
                 @Nullable Bundle options);
         void enterSplitScreen(int taskId, boolean leftOrTop);
-        void exitSplitScreen();
+
+        /**
+         * Exits splitscreen, with an associated exit trigger from the SplitscreenUIChanged proto
+         * for logging.
+         */
+        void exitSplitScreen(int toTopTaskId, int exitTrigger);
     }
 
     /**
@@ -352,7 +357,7 @@
         }
 
         @Override
-        public void exitSplitScreen() {
+        public void exitSplitScreen(int toTopTaskId, int exitTrigger) {
             throw new UnsupportedOperationException("exitSplitScreen not implemented by starter");
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index b342336..efc9ed0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -38,6 +38,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.logging.InstanceId;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayLayout;
@@ -98,8 +99,9 @@
         return mHasDropped;
     }
 
-    public void prepare(DisplayLayout displayLayout, ClipData initialData) {
-        mPolicy.start(displayLayout, initialData);
+    public void prepare(DisplayLayout displayLayout, ClipData initialData,
+            InstanceId loggerSessionId) {
+        mPolicy.start(displayLayout, initialData, loggerSessionId);
         mHasDropped = false;
         mCurrentTarget = null;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java
index 64f7be5..73deea5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java
@@ -86,7 +86,7 @@
     public DropOutlineDrawable(Context context) {
         super();
         // TODO(b/169894807): Use corner specific radii and maybe lower radius for non-edge corners
-        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context.getResources());
+        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
         mColor = context.getColor(R.color.drop_outline_background);
         mMaxAlpha = Color.alpha(mColor);
         // Initialize as hidden
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
new file mode 100644
index 0000000..5fb3297
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.freeform;
+
+import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
+import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.provider.Settings;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+
+/**
+ * {@link ShellTaskOrganizer.TaskListener} for {@link
+ * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}.
+ */
+public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
+    private static final String TAG = "FreeformTaskListener";
+
+    private final SyncTransactionQueue mSyncQueue;
+
+    private final SparseArray<State> mTasks = new SparseArray<>();
+
+    private static class State {
+        RunningTaskInfo mTaskInfo;
+        SurfaceControl mLeash;
+    }
+
+    public FreeformTaskListener(SyncTransactionQueue syncQueue) {
+        mSyncQueue = syncQueue;
+    }
+
+    @Override
+    public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+        if (mTasks.get(taskInfo.taskId) != null) {
+            throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId);
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d",
+                taskInfo.taskId);
+        final State state = new State();
+        state.mTaskInfo = taskInfo;
+        state.mLeash = leash;
+        mTasks.put(taskInfo.taskId, state);
+
+        final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
+        mSyncQueue.runInSync(t -> {
+            Point taskPosition = taskInfo.positionInParent;
+            t.setPosition(leash, taskPosition.x, taskPosition.y)
+                    .setWindowCrop(leash, taskBounds.width(), taskBounds.height())
+                    .show(leash);
+        });
+    }
+
+    @Override
+    public void onTaskVanished(RunningTaskInfo taskInfo) {
+        State state = mTasks.get(taskInfo.taskId);
+        if (state == null) {
+            Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
+            return;
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d",
+                taskInfo.taskId);
+        mTasks.remove(taskInfo.taskId);
+    }
+
+    @Override
+    public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+        State state = mTasks.get(taskInfo.taskId);
+        if (state == null) {
+            throw new RuntimeException(
+                    "Task info changed before appearing: #" + taskInfo.taskId);
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d",
+                taskInfo.taskId);
+        state.mTaskInfo = taskInfo;
+
+        final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
+        final SurfaceControl leash = state.mLeash;
+        mSyncQueue.runInSync(t -> {
+            Point taskPosition = taskInfo.positionInParent;
+            t.setPosition(leash, taskPosition.x, taskPosition.y)
+                    .setWindowCrop(leash, taskBounds.width(), taskBounds.height())
+                    .show(leash);
+        });
+    }
+
+    @Override
+    public void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + this);
+        pw.println(innerPrefix + mTasks.size() + " tasks");
+    }
+
+    @Override
+    public String toString() {
+        return TAG;
+    }
+
+    /**
+     * Checks if freeform support is enabled in system.
+     *
+     * @param context context used to check settings and package manager.
+     * @return {@code true} if freeform is enabled, {@code false} if not.
+     */
+    public static boolean isFreeformEnabled(Context context) {
+        return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
+                || Settings.Global.getInt(context.getContentResolver(),
+                DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
+    }
+
+    /**
+     * Creates {@link FreeformTaskListener} if freeform is enabled.
+     */
+    public static FreeformTaskListener create(Context context,
+            SyncTransactionQueue syncQueue) {
+        if (!isFreeformEnabled(context)) {
+            return null;
+        }
+
+        return new FreeformTaskListener(syncQueue);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
new file mode 100644
index 0000000..3f17f2b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.fullscreen;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
+import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
+import android.graphics.Point;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.transition.Transitions;
+
+import java.io.PrintWriter;
+import java.util.Optional;
+
+/**
+  * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}.
+  */
+public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
+    private static final String TAG = "FullscreenTaskListener";
+
+    private final SyncTransactionQueue mSyncQueue;
+
+    private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>();
+    private final AnimatableTasksListener mAnimatableTasksListener = new AnimatableTasksListener();
+    private final FullscreenUnfoldController mFullscreenUnfoldController;
+
+    public FullscreenTaskListener(SyncTransactionQueue syncQueue,
+            Optional<FullscreenUnfoldController> unfoldController) {
+        mSyncQueue = syncQueue;
+        mFullscreenUnfoldController = unfoldController.orElse(null);
+    }
+
+    @Override
+    public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+        if (mDataByTaskId.get(taskInfo.taskId) != null) {
+            throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId);
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d",
+                taskInfo.taskId);
+        final Point positionInParent = taskInfo.positionInParent;
+        mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent));
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
+        mSyncQueue.runInSync(t -> {
+            // Reset several properties back to fullscreen (PiP, for example, leaves all these
+            // properties in a bad state).
+            t.setWindowCrop(leash, null);
+            t.setPosition(leash, positionInParent.x, positionInParent.y);
+            t.setAlpha(leash, 1f);
+            t.setMatrix(leash, 1, 0, 0, 1);
+            t.show(leash);
+        });
+
+        mAnimatableTasksListener.onTaskAppeared(taskInfo);
+    }
+
+    @Override
+    public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
+
+        mAnimatableTasksListener.onTaskInfoChanged(taskInfo);
+
+        final TaskData data = mDataByTaskId.get(taskInfo.taskId);
+        final Point positionInParent = taskInfo.positionInParent;
+        if (!positionInParent.equals(data.positionInParent)) {
+            data.positionInParent.set(positionInParent.x, positionInParent.y);
+            mSyncQueue.runInSync(t -> {
+                t.setPosition(data.surface, positionInParent.x, positionInParent.y);
+            });
+        }
+    }
+
+    @Override
+    public void onTaskVanished(RunningTaskInfo taskInfo) {
+        if (mDataByTaskId.get(taskInfo.taskId) == null) {
+            Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
+            return;
+        }
+
+        mAnimatableTasksListener.onTaskVanished(taskInfo);
+        mDataByTaskId.remove(taskInfo.taskId);
+
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
+                taskInfo.taskId);
+    }
+
+    @Override
+    public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
+        if (!mDataByTaskId.contains(taskId)) {
+            throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
+        }
+        b.setParent(mDataByTaskId.get(taskId).surface);
+    }
+
+    @Override
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + this);
+        pw.println(innerPrefix + mDataByTaskId.size() + " Tasks");
+    }
+
+    @Override
+    public String toString() {
+        return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN);
+    }
+
+    /**
+     * Per-task data for each managed task.
+     */
+    private static class TaskData {
+        public final SurfaceControl surface;
+        public final Point positionInParent;
+
+        public TaskData(SurfaceControl surface, Point positionInParent) {
+            this.surface = surface;
+            this.positionInParent = positionInParent;
+        }
+    }
+
+    class AnimatableTasksListener {
+        private final SparseBooleanArray mTaskIds = new SparseBooleanArray();
+
+        public void onTaskAppeared(RunningTaskInfo taskInfo) {
+            final boolean isApplicable = isAnimatable(taskInfo);
+            if (isApplicable) {
+                mTaskIds.put(taskInfo.taskId, true);
+
+                if (mFullscreenUnfoldController != null) {
+                    SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface;
+                    mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash);
+                }
+            }
+        }
+
+        public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+            final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId);
+            final boolean isApplicable = isAnimatable(taskInfo);
+
+            if (isCurrentlyApplicable) {
+                if (isApplicable) {
+                    // Still applicable, send update
+                    if (mFullscreenUnfoldController != null) {
+                        mFullscreenUnfoldController.onTaskInfoChanged(taskInfo);
+                    }
+                } else {
+                    // Became inapplicable
+                    if (mFullscreenUnfoldController != null) {
+                        mFullscreenUnfoldController.onTaskVanished(taskInfo);
+                    }
+                    mTaskIds.put(taskInfo.taskId, false);
+                }
+            } else {
+                if (isApplicable) {
+                    // Became applicable
+                    mTaskIds.put(taskInfo.taskId, true);
+
+                    if (mFullscreenUnfoldController != null) {
+                        SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface;
+                        mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash);
+                    }
+                }
+            }
+        }
+
+        public void onTaskVanished(RunningTaskInfo taskInfo) {
+            final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId);
+            if (isCurrentlyApplicable && mFullscreenUnfoldController != null) {
+                mFullscreenUnfoldController.onTaskVanished(taskInfo);
+            }
+            mTaskIds.put(taskInfo.taskId, false);
+        }
+
+        private boolean isAnimatable(TaskInfo taskInfo) {
+            // Filter all visible tasks that are not launcher tasks
+            // We do not animate launcher as it handles the animation by itself
+            return taskInfo != null && taskInfo.isVisible && taskInfo.getConfiguration()
+                    .windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
new file mode 100644
index 0000000..fc1b704
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.fullscreen;
+
+import static android.util.MathUtils.lerp;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.animation.RectEvaluator;
+import android.animation.TypeEvaluator;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controls full screen app unfold transition: animating cropping window and scaling when
+ * folding or unfolding a foldable device.
+ */
+public final class FullscreenUnfoldController implements UnfoldListener,
+        OnInsetsChangedListener {
+
+    private static final float[] FLOAT_9 = new float[9];
+    private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
+
+    private static final float HORIZONTAL_START_MARGIN = 0.08f;
+    private static final float VERTICAL_START_MARGIN = 0.03f;
+    private static final float END_SCALE = 1f;
+    private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2;
+
+    private final Executor mExecutor;
+    private final ShellUnfoldProgressProvider mProgressProvider;
+    private final DisplayInsetsController mDisplayInsetsController;
+
+    private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+    private final UnfoldBackgroundController mBackgroundController;
+
+    private InsetsSource mTaskbarInsetsSource;
+
+    private final float mWindowCornerRadiusPx;
+    private final float mExpandedTaskBarHeight;
+
+    private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+
+    public FullscreenUnfoldController(
+            @NonNull Context context,
+            @NonNull Executor executor,
+            @NonNull UnfoldBackgroundController backgroundController,
+            @NonNull ShellUnfoldProgressProvider progressProvider,
+            @NonNull DisplayInsetsController displayInsetsController
+    ) {
+        mExecutor = executor;
+        mProgressProvider = progressProvider;
+        mDisplayInsetsController = displayInsetsController;
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+        mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+        mBackgroundController = backgroundController;
+    }
+
+    /**
+     * Initializes the controller
+     */
+    public void init() {
+        mProgressProvider.addListener(mExecutor, this);
+        mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+    }
+
+    @Override
+    public void onStateChangeProgress(float progress) {
+        if (mAnimationContextByTaskId.size() == 0) return;
+
+        mBackgroundController.ensureBackground(mTransaction);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+
+            context.mCurrentCropRect.set(RECT_EVALUATOR
+                    .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
+
+            float scale = lerp(START_SCALE, END_SCALE, progress);
+            context.mMatrix.setScale(scale, scale, context.mCurrentCropRect.exactCenterX(),
+                    context.mCurrentCropRect.exactCenterY());
+
+            mTransaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
+                    .setMatrix(context.mLeash, context.mMatrix, FLOAT_9)
+                    .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
+        }
+
+        mTransaction.apply();
+    }
+
+    @Override
+    public void onStateChangeFinished() {
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            resetSurface(context);
+        }
+
+        mBackgroundController.removeBackground(mTransaction);
+        mTransaction.apply();
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update(mTaskbarInsetsSource, context.mTaskInfo);
+        }
+    }
+
+    /**
+     * Called when a new matching task appeared
+     */
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource,
+                taskInfo);
+        mAnimationContextByTaskId.put(taskInfo.taskId, animationContext);
+    }
+
+    /**
+     * Called when matching task changed
+     */
+    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId);
+        if (animationContext != null) {
+            animationContext.update(mTaskbarInsetsSource, taskInfo);
+        }
+    }
+
+    /**
+     * Called when matching task vanished
+     */
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId);
+        if (animationContext != null) {
+            resetSurface(animationContext);
+            mAnimationContextByTaskId.remove(taskInfo.taskId);
+        }
+
+        if (mAnimationContextByTaskId.size() == 0) {
+            mBackgroundController.removeBackground(mTransaction);
+        }
+
+        mTransaction.apply();
+    }
+
+    private void resetSurface(AnimationContext context) {
+        mTransaction
+                .setWindowCrop(context.mLeash, null)
+                .setCornerRadius(context.mLeash, 0.0F)
+                .setMatrix(context.mLeash, 1.0F, 0.0F, 0.0F, 1.0F)
+                .setPosition(context.mLeash,
+                        (float) context.mTaskInfo.positionInParent.x,
+                        (float) context.mTaskInfo.positionInParent.y);
+    }
+
+    private class AnimationContext {
+        final SurfaceControl mLeash;
+        final Rect mStartCropRect = new Rect();
+        final Rect mEndCropRect = new Rect();
+        final Rect mCurrentCropRect = new Rect();
+        final Matrix mMatrix = new Matrix();
+
+        TaskInfo mTaskInfo;
+
+        private AnimationContext(SurfaceControl leash,
+                                InsetsSource taskBarInsetsSource,
+                                TaskInfo taskInfo) {
+            this.mLeash = leash;
+            update(taskBarInsetsSource, taskInfo);
+        }
+
+        private void update(InsetsSource taskBarInsetsSource, TaskInfo taskInfo) {
+            mTaskInfo = taskInfo;
+            mStartCropRect.set(mTaskInfo.getConfiguration().windowConfiguration.getBounds());
+
+            if (taskBarInsetsSource != null) {
+                // Only insets the cropping window with task bar when it's expanded
+                if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+                    mStartCropRect.inset(taskBarInsetsSource
+                            .calculateVisibleInsets(mStartCropRect));
+                }
+            }
+
+            mEndCropRect.set(mStartCropRect);
+
+            int horizontalMargin = (int) (mEndCropRect.width() * HORIZONTAL_START_MARGIN);
+            mStartCropRect.left = mEndCropRect.left + horizontalMargin;
+            mStartCropRect.right = mEndCropRect.right - horizontalMargin;
+            int verticalMargin = (int) (mEndCropRect.height() * VERTICAL_START_MARGIN);
+            mStartCropRect.top = mEndCropRect.top + verticalMargin;
+            mStartCropRect.bottom = mEndCropRect.bottom - verticalMargin;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java
index 362b40f..067f808 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java
@@ -20,6 +20,8 @@
 import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
 import static android.view.WindowManager.DOCKED_RIGHT;
 
+import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR;
+import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
 import static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION;
 import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION;
 
@@ -100,10 +102,6 @@
     private static final float MINIMIZE_DOCK_SCALE = 0f;
     private static final float ADJUSTED_FOR_IME_SCALE = 0.5f;
 
-    private static final PathInterpolator SLOWDOWN_INTERPOLATOR =
-            new PathInterpolator(0.5f, 1f, 0.5f, 1f);
-    private static final PathInterpolator DIM_INTERPOLATOR =
-            new PathInterpolator(.23f, .87f, .52f, -0.11f);
     private static final Interpolator IME_ADJUST_INTERPOLATOR =
             new PathInterpolator(0.2f, 0f, 0.1f, 1f);
 
@@ -460,6 +458,7 @@
     private void stopDragging() {
         mHandle.setTouching(false, true /* animate */);
         mWindowManager.setSlippery(true);
+        mWindowManagerProxy.setResizing(false);
         releaseBackground();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java
index 40244fb..f201634 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java
@@ -62,6 +62,7 @@
     Rect mSecondary = null;
     Rect mAdjustedPrimary = null;
     Rect mAdjustedSecondary = null;
+    final Rect mTmpBounds = new Rect();
 
     public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl,
             LegacySplitScreenTaskListener taskTiles) {
@@ -136,31 +137,41 @@
         return mMinimizedSnapAlgorithm;
     }
 
-    void resizeSplits(int position) {
+    /**
+     * Resize primary bounds and secondary bounds by divider position.
+     *
+     * @param position divider position.
+     * @return true if calculated bounds changed.
+     */
+    boolean resizeSplits(int position) {
         mPrimary = mPrimary == null ? new Rect() : mPrimary;
         mSecondary = mSecondary == null ? new Rect() : mSecondary;
-        calcSplitBounds(position, mPrimary, mSecondary);
+        int dockSide = getPrimarySplitSide();
+        boolean boundsChanged;
+
+        mTmpBounds.set(mPrimary);
+        DockedDividerUtils.calculateBoundsForPosition(position, dockSide, mPrimary,
+                mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
+        boundsChanged = !mPrimary.equals(mTmpBounds);
+
+        mTmpBounds.set(mSecondary);
+        DockedDividerUtils.calculateBoundsForPosition(position,
+                DockedDividerUtils.invertDockSide(dockSide), mSecondary, mDisplayLayout.width(),
+                mDisplayLayout.height(), mDividerSize);
+        boundsChanged |= !mSecondary.equals(mTmpBounds);
+        return boundsChanged;
     }
 
     void resizeSplits(int position, WindowContainerTransaction t) {
-        resizeSplits(position);
-        t.setBounds(mTiles.mPrimary.token, mPrimary);
-        t.setBounds(mTiles.mSecondary.token, mSecondary);
+        if (resizeSplits(position)) {
+            t.setBounds(mTiles.mPrimary.token, mPrimary);
+            t.setBounds(mTiles.mSecondary.token, mSecondary);
 
-        t.setSmallestScreenWidthDp(mTiles.mPrimary.token,
-                getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary));
-        t.setSmallestScreenWidthDp(mTiles.mSecondary.token,
-                getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary));
-    }
-
-    void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) {
-        int dockSide = getPrimarySplitSide();
-        DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary,
-                mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize);
-
-        DockedDividerUtils.calculateBoundsForPosition(position,
-                DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(),
-                mDisplayLayout.height(), mDividerSize);
+            t.setSmallestScreenWidthDp(mTiles.mPrimary.token,
+                    getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary));
+            t.setSmallestScreenWidthDp(mTiles.mSecondary.token,
+                    getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary));
+        }
     }
 
     Rect calcResizableMinimizedHomeStackBounds() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java
index d9409ec..b1fa2ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java
@@ -204,7 +204,8 @@
 
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         if (transition != mPendingDismiss && transition != mPendingEnter) {
             // If we're not in split-mode, just abort
@@ -239,12 +240,12 @@
                 if (change.getParent() != null) {
                     // This is probably reparented, so we want the parent to be immediately visible
                     final TransitionInfo.Change parentChange = info.getChange(change.getParent());
-                    t.show(parentChange.getLeash());
-                    t.setAlpha(parentChange.getLeash(), 1.f);
+                    startTransaction.show(parentChange.getLeash());
+                    startTransaction.setAlpha(parentChange.getLeash(), 1.f);
                     // and then animate this layer outside the parent (since, for example, this is
                     // the home task animating from fullscreen to part-screen).
-                    t.reparent(leash, info.getRootLeash());
-                    t.setLayer(leash, info.getChanges().size() - i);
+                    startTransaction.reparent(leash, info.getRootLeash());
+                    startTransaction.setLayer(leash, info.getChanges().size() - i);
                     // build the finish reparent/reposition
                     mFinishTransaction.reparent(leash, parentChange.getLeash());
                     mFinishTransaction.setPosition(leash,
@@ -271,12 +272,12 @@
             if (transition == mPendingEnter
                     && mListener.mPrimary.token.equals(change.getContainer())
                     || mListener.mSecondary.token.equals(change.getContainer())) {
-                t.setWindowCrop(leash, change.getStartAbsBounds().width(),
+                startTransaction.setWindowCrop(leash, change.getStartAbsBounds().width(),
                         change.getStartAbsBounds().height());
                 if (mListener.mPrimary.token.equals(change.getContainer())) {
                     // Move layer to top since we want it above the oversized home task during
                     // animation even though home task is on top in hierarchy.
-                    t.setLayer(leash, info.getChanges().size() + 1);
+                    startTransaction.setLayer(leash, info.getChanges().size() + 1);
                 }
             }
             boolean isOpening = Transitions.isOpeningType(info.getType());
@@ -289,7 +290,7 @@
                     // Dismissing via snap-to-top/bottom means that the dismissed task is already
                     // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
                     // and don't animate it so it doesn't pop-in when reparented.
-                    t.setAlpha(leash, 0.f);
+                    startTransaction.setAlpha(leash, 0.f);
                 } else {
                     startExampleAnimation(leash, false /* show */);
                 }
@@ -311,7 +312,7 @@
             }
             mSplitScreen.finishEnterSplitTransition(homeIsVisible);
         }
-        t.apply();
+        startTransaction.apply();
         onFinish();
         return true;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index e511bff..38079af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -43,6 +43,7 @@
 import android.view.Surface;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -64,7 +65,8 @@
 /**
  * Manages and manipulates the one handed states, transitions, and gesture for phones.
  */
-public class OneHandedController implements RemoteCallable<OneHandedController> {
+public class OneHandedController implements RemoteCallable<OneHandedController>,
+        DisplayChangeController.OnDisplayChangingListener {
     private static final String TAG = "OneHandedController";
 
     private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
@@ -106,19 +108,6 @@
     private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer;
     private OneHandedUiEventLogger mOneHandedUiEventLogger;
 
-    /**
-     * Handle rotation based on OnDisplayChangingListener callback
-     */
-    private final DisplayChangeController.OnDisplayChangingListener mRotationController =
-            (display, fromRotation, toRotation, wct) -> {
-                if (!isInitialized()) {
-                    return;
-                }
-                mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct);
-                mOneHandedUiEventLogger.writeEvent(
-                        OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT);
-            };
-
     private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener =
             new DisplayController.OnDisplaysChangedListener() {
                 @Override
@@ -296,7 +285,7 @@
                 getObserver(this::onSwipeToNotificationEnabledChanged);
         mShortcutEnabledObserver = getObserver(this::onShortcutEnabledChanged);
 
-        mDisplayController.addDisplayChangingController(mRotationController);
+        mDisplayController.addDisplayChangingController(this);
         setupCallback();
         registerSettingObservers(mUserId);
         setupTimeoutListener();
@@ -548,6 +537,7 @@
                 mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
                         mContext.getContentResolver(), mUserId);
         setSwipeToNotificationEnabled(enabled);
+        notifyShortcutStateChanged(mState.getState());
 
         mOneHandedUiEventLogger.writeEvent(enabled
                 ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHOW_NOTIFICATION_ENABLED_ON
@@ -745,6 +735,27 @@
     }
 
     /**
+     * Handles rotation based on OnDisplayChangingListener callback
+     */
+    @Override
+    public void onRotateDisplay(int displayId, int fromRotation, int toRotation,
+            WindowContainerTransaction wct) {
+        if (!isInitialized()) {
+            return;
+        }
+
+        if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver(),
+                mUserId) || mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+                mContext.getContentResolver(), mUserId)) {
+            return;
+        }
+
+        mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct);
+        mOneHandedUiEventLogger.writeEvent(
+                OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT);
+    }
+
+    /**
      * The interface for calls from outside the Shell, within the host process.
      */
     @ExternalThread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
index c2bbd9e..1b2f476 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.onehanded;
 
-import static android.os.UserHandle.myUserId;
-
 import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT;
 import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER;
 
@@ -186,20 +184,8 @@
         if (mDisplayLayout.rotation() == toRotation) {
             return;
         }
-
-        if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(context.getContentResolver(),
-                myUserId())) {
-            return;
-        }
-
         mDisplayLayout.rotateTo(context.getResources(), toRotation);
         updateDisplayBounds();
-
-        if (mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
-                context.getContentResolver(), myUserId())) {
-            // If current settings is swipe notification, skip finishOffset.
-            return;
-        }
         finishOffset(0, TRANSITION_DIRECTION_EXIT);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 200af74..05111a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -38,6 +38,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.transition.Transitions;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -617,14 +618,28 @@
                     setCurrentValue(bounds);
                     final Rect insets = computeInsets(fraction);
                     final float degree, x, y;
-                    if (rotationDelta == ROTATION_90) {
-                        degree = 90 * fraction;
-                        x = fraction * (end.right - start.left) + start.left;
-                        y = fraction * (end.top - start.top) + start.top;
+                    if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+                        if (rotationDelta == ROTATION_90) {
+                            degree = 90 * (1 - fraction);
+                            x = fraction * (end.left - start.left)
+                                    + start.left + start.right * (1 - fraction);
+                            y = fraction * (end.top - start.top) + start.top;
+                        } else {
+                            degree = -90 * (1 - fraction);
+                            x = fraction * (end.left - start.left) + start.left;
+                            y = fraction * (end.top - start.top)
+                                    + start.top + start.bottom * (1 - fraction);
+                        }
                     } else {
-                        degree = -90 * fraction;
-                        x = fraction * (end.left - start.left) + start.left;
-                        y = fraction * (end.bottom - start.top) + start.top;
+                        if (rotationDelta == ROTATION_90) {
+                            degree = 90 * fraction;
+                            x = fraction * (end.right - start.left) + start.left;
+                            y = fraction * (end.top - start.top) + start.top;
+                        } else {
+                            degree = -90 * fraction;
+                            x = fraction * (end.left - start.left) + start.left;
+                            y = fraction * (end.bottom - start.top) + start.top;
+                        }
                     }
                     getSurfaceTransactionHelper()
                             .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index 728794d..180e3fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -23,6 +23,7 @@
 import android.view.SurfaceControl;
 
 import com.android.wm.shell.R;
+import com.android.wm.shell.transition.Transitions;
 
 /**
  * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition.
@@ -137,7 +138,8 @@
         // destination are different.
         final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH;
         final Rect crop = mTmpDestinationRect;
-        crop.set(0, 0, destW, destH);
+        crop.set(0, 0, Transitions.ENABLE_SHELL_TRANSITIONS ? destH
+                : destW, Transitions.ENABLE_SHELL_TRANSITIONS ? destW : destH);
         // Inverse scale for crop to fit in screen coordinates.
         crop.scale(1 / scale);
         crop.offset(insets.left, insets.top);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index f2bad6c..9686776 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -114,38 +114,6 @@
      */
     private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500;
 
-    // Not a complete set of states but serves what we want right now.
-    private enum State {
-        UNDEFINED(0),
-        TASK_APPEARED(1),
-        ENTRY_SCHEDULED(2),
-        ENTERING_PIP(3),
-        ENTERED_PIP(4),
-        EXITING_PIP(5);
-
-        private final int mStateValue;
-
-        State(int value) {
-            mStateValue = value;
-        }
-
-        private boolean isInPip() {
-            return mStateValue >= TASK_APPEARED.mStateValue
-                    && mStateValue != EXITING_PIP.mStateValue;
-        }
-
-        /**
-         * Resize request can be initiated in other component, ignore if we are no longer in PIP,
-         * still waiting for animation or we're exiting from it.
-         *
-         * @return {@code true} if the resize request should be blocked/ignored.
-         */
-        private boolean shouldBlockResizeRequest() {
-            return mStateValue < ENTERING_PIP.mStateValue
-                    || mStateValue == EXITING_PIP.mStateValue;
-        }
-    }
-
     private final Context mContext;
     private final SyncTransactionQueue mSyncTransactionQueue;
     private final PipBoundsState mPipBoundsState;
@@ -169,11 +137,6 @@
         public void onPipAnimationStart(TaskInfo taskInfo,
                 PipAnimationController.PipTransitionAnimator animator) {
             final int direction = animator.getTransitionDirection();
-            if (direction == TRANSITION_DIRECTION_TO_PIP) {
-                // TODO (b//169221267): Add jank listener for transactions without buffer updates.
-                //InteractionJankMonitor.getInstance().begin(
-                //        InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000);
-            }
             sendOnPipTransitionStarted(direction);
         }
 
@@ -201,7 +164,8 @@
             }
             final boolean isExitPipDirection = isOutPipDirection(direction)
                     || isRemovePipDirection(direction);
-            if (mState != State.EXITING_PIP || isExitPipDirection) {
+            if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP
+                    || isExitPipDirection) {
                 // Finish resize as long as we're not exiting PIP, or, if we are, only if this is
                 // the end of an exit PIP animation.
                 // This is necessary in case there was a resize animation ongoing when exit PIP
@@ -244,7 +208,7 @@
     private ActivityManager.RunningTaskInfo mDeferredTaskInfo;
     private WindowContainerToken mToken;
     private SurfaceControl mLeash;
-    private State mState = State.UNDEFINED;
+    private PipTransitionState mPipTransitionState;
     private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
     private long mLastOneShotAlphaAnimationTime;
     private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
@@ -274,21 +238,14 @@
     private @Surface.Rotation int mCurrentRotation;
 
     /**
-     * If set to {@code true}, no entering PiP transition would be kicked off and most likely
-     * it's due to the fact that Launcher is handling the transition directly when swiping
-     * auto PiP-able Activity to home.
-     * See also {@link #startSwipePipToHome(ComponentName, ActivityInfo, PictureInPictureParams)}.
-     */
-    private boolean mInSwipePipToHomeTransition;
-
-    /**
      * An optional overlay used to mask content changing between an app in/out of PiP, only set if
-     * {@link #mInSwipePipToHomeTransition} is true.
+     * {@link PipTransitionState#getInSwipePipToHomeTransition()} is true.
      */
     private SurfaceControl mSwipePipToHomeOverlay;
 
     public PipTaskOrganizer(Context context,
             @NonNull SyncTransactionQueue syncTransactionQueue,
+            @NonNull PipTransitionState pipTransitionState,
             @NonNull PipBoundsState pipBoundsState,
             @NonNull PipBoundsAlgorithm boundsHandler,
             @NonNull PipMenuController pipMenuController,
@@ -302,6 +259,7 @@
             @ShellMainThread ShellExecutor mainExecutor) {
         mContext = context;
         mSyncTransactionQueue = syncTransactionQueue;
+        mPipTransitionState = pipTransitionState;
         mPipBoundsState = pipBoundsState;
         mPipBoundsAlgorithm = boundsHandler;
         mPipMenuController = pipMenuController;
@@ -337,14 +295,14 @@
     }
 
     public boolean isInPip() {
-        return mState.isInPip();
+        return mPipTransitionState.isInPip();
     }
 
     /**
      * Returns whether the entry animation is waiting to be started.
      */
     public boolean isEntryScheduled() {
-        return mState == State.ENTRY_SCHEDULED;
+        return mPipTransitionState.getTransitionState() == PipTransitionState.ENTRY_SCHEDULED;
     }
 
     /**
@@ -372,7 +330,7 @@
      */
     public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
             PictureInPictureParams pictureInPictureParams) {
-        mInSwipePipToHomeTransition = true;
+        mPipTransitionState.setInSwipePipToHomeTransition(true);
         sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
         setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo);
         return mPipBoundsAlgorithm.getEntryDestinationBounds();
@@ -385,7 +343,7 @@
     public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds,
             SurfaceControl overlay) {
         // do nothing if there is no startSwipePipToHome being called before
-        if (mInSwipePipToHomeTransition) {
+        if (mPipTransitionState.getInSwipePipToHomeTransition()) {
             mPipBoundsState.setBounds(destinationBounds);
             mSwipePipToHomeOverlay = overlay;
         }
@@ -412,9 +370,11 @@
      * @param animationDurationMs duration in millisecond for the exiting PiP transition
      */
     public void exitPip(int animationDurationMs) {
-        if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) {
+        if (!mPipTransitionState.isInPip()
+                || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP
+                || mToken == null) {
             Log.wtf(TAG, "Not allowed to exitPip in current state"
-                    + " mState=" + mState + " mToken=" + mToken);
+                    + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken);
             return;
         }
 
@@ -438,7 +398,12 @@
         wct.setBoundsChangeTransaction(mToken, tx);
         // Set the exiting state first so if there is fixed rotation later, the running animation
         // won't be interrupted by alpha animation for existing PiP.
-        mState = State.EXITING_PIP;
+        mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP);
+
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mPipTransitionController.startTransition(destinationBounds, wct);
+            return;
+        }
         mSyncTransactionQueue.queue(wct);
         mSyncTransactionQueue.runInSync(t -> {
             // Make sure to grab the latest source hint rect as it could have been
@@ -476,9 +441,9 @@
      * Removes PiP immediately.
      */
     public void removePip() {
-        if (!mState.isInPip() ||  mToken == null) {
+        if (!mPipTransitionState.isInPip() ||  mToken == null) {
             Log.wtf(TAG, "Not allowed to removePip in current state"
-                    + " mState=" + mState + " mToken=" + mToken);
+                    + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken);
             return;
         }
 
@@ -492,10 +457,19 @@
         animator.setDuration(mExitAnimationDuration);
         animator.setInterpolator(Interpolators.ALPHA_OUT);
         animator.start();
-        mState = State.EXITING_PIP;
+        mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP);
     }
 
     private void removePipImmediately() {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            wct.setBounds(mToken, null);
+            wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+            wct.reorder(mToken, false);
+            mPipTransitionController.startTransition(null, wct);
+            return;
+        }
+
         try {
             // Reset the task bounds first to ensure the activity configuration is reset as well
             final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -514,7 +488,7 @@
         Objects.requireNonNull(info, "Requires RunningTaskInfo");
         mTaskInfo = info;
         mToken = mTaskInfo.token;
-        mState = State.TASK_APPEARED;
+        mPipTransitionState.setTransitionState(PipTransitionState.TASK_APPEARED);
         mLeash = leash;
         mPictureInPictureParams = mTaskInfo.pictureInPictureParams;
         setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams,
@@ -530,7 +504,7 @@
             mOnDisplayIdChangeCallback.accept(info.displayId);
         }
 
-        if (mInSwipePipToHomeTransition) {
+        if (mPipTransitionState.getInSwipePipToHomeTransition()) {
             if (!mWaitForFixedRotation) {
                 onEndOfSwipePipToHomeTransition();
             } else {
@@ -557,6 +531,8 @@
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
                 mPipMenuController.attach(mLeash);
+            } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+                mOneShotAnimationType = ANIM_TYPE_BOUNDS;
             }
             return;
         }
@@ -568,7 +544,7 @@
             scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */,
                     sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration,
                     null /* updateBoundsCallback */);
-            mState = State.ENTERING_PIP;
+            mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
         } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
             enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration);
             mOneShotAnimationType = ANIM_TYPE_BOUNDS;
@@ -595,7 +571,7 @@
         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
         animateResizePip(currentBounds, destinationBounds, sourceHintRect,
                 TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 0 /* startingAngle */);
-        mState = State.ENTERING_PIP;
+        mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
     }
 
     /**
@@ -620,7 +596,7 @@
                 mSurfaceControlTransactionFactory.getTransaction();
         tx.setAlpha(mLeash, 0f);
         tx.apply();
-        mState = State.ENTRY_SCHEDULED;
+        mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED);
         applyEnterPipSyncTransaction(destinationBounds, () -> {
             mPipAnimationController
                     .getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f)
@@ -631,11 +607,16 @@
                     .start();
             // mState is set right after the animation is kicked off to block any resize
             // requests such as offsetPip that may have been called prior to the transition.
-            mState = State.ENTERING_PIP;
+            mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
         }, null /* boundsChangeTransaction */);
     }
 
     private void onEndOfSwipePipToHomeTransition() {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mSwipePipToHomeOverlay = null;
+            return;
+        }
+
         final Rect destinationBounds = mPipBoundsState.getBounds();
         final SurfaceControl swipeToHomeOverlay = mSwipePipToHomeOverlay;
         final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
@@ -655,7 +636,7 @@
                         null /* callback */, false /* withStartDelay */);
             }
         }, tx);
-        mInSwipePipToHomeTransition = false;
+        mPipTransitionState.setInSwipePipToHomeTransition(false);
         mSwipePipToHomeOverlay = null;
     }
 
@@ -679,7 +660,7 @@
     private void sendOnPipTransitionStarted(
             @PipAnimationController.TransitionDirection int direction) {
         if (direction == TRANSITION_DIRECTION_TO_PIP) {
-            mState = State.ENTERING_PIP;
+            mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
         }
         mPipTransitionController.sendOnPipTransitionStarted(direction);
     }
@@ -688,7 +669,7 @@
     void sendOnPipTransitionFinished(
             @PipAnimationController.TransitionDirection int direction) {
         if (direction == TRANSITION_DIRECTION_TO_PIP) {
-            mState = State.ENTERED_PIP;
+            mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP);
         }
         mPipTransitionController.sendOnPipTransitionFinished(direction);
         // Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent.
@@ -713,7 +694,7 @@
      */
     @Override
     public void onTaskVanished(ActivityManager.RunningTaskInfo info) {
-        if (mState == State.UNDEFINED) {
+        if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
             return;
         }
         final WindowContainerToken token = info.token;
@@ -723,9 +704,9 @@
             return;
         }
         clearWaitForFixedRotation();
-        mInSwipePipToHomeTransition = false;
+        mPipTransitionState.setInSwipePipToHomeTransition(false);
         mPictureInPictureParams = null;
-        mState = State.UNDEFINED;
+        mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED);
         // Re-set the PIP bounds to none.
         mPipBoundsState.setBounds(new Rect());
         mPipUiEventLoggerLogger.setTaskInfo(null);
@@ -750,8 +731,10 @@
     @Override
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
         Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken");
-        if (mState != State.ENTERED_PIP && mState != State.EXITING_PIP) {
-            Log.d(TAG, "Defer onTaskInfoChange in current state: " + mState);
+        if (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP
+                && mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP) {
+            Log.d(TAG, "Defer onTaskInfoChange in current state: "
+                    + mPipTransitionState.getTransitionState());
             // Defer applying PiP parameters if the task is entering PiP to avoid disturbing
             // the animation.
             mDeferredTaskInfo = info;
@@ -784,7 +767,7 @@
         mNextRotation = newRotation;
         mWaitForFixedRotation = true;
 
-        if (mState.isInPip()) {
+        if (mPipTransitionState.isInPip()) {
             // Fade out the existing PiP to avoid jump cut during seamless rotation.
             fadeExistingPip(false /* show */);
         }
@@ -795,17 +778,19 @@
         if (!mWaitForFixedRotation) {
             return;
         }
-        if (mState == State.TASK_APPEARED) {
-            if (mInSwipePipToHomeTransition) {
+        if (mPipTransitionState.getTransitionState() == PipTransitionState.TASK_APPEARED) {
+            if (mPipTransitionState.getInSwipePipToHomeTransition()) {
                 onEndOfSwipePipToHomeTransition();
             } else {
                 // Schedule a regular animation to ensure all the callbacks are still being sent.
                 enterPipWithAlphaAnimation(mPipBoundsAlgorithm.getEntryDestinationBounds(),
                         mEnterAnimationDuration);
             }
-        } else if (mState == State.ENTERED_PIP && mHasFadeOut) {
+        } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERED_PIP
+                && mHasFadeOut) {
             fadeExistingPip(true /* show */);
-        } else if (mState == State.ENTERING_PIP && mDeferredAnimEndTransaction != null) {
+        } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERING_PIP
+                && mDeferredAnimEndTransaction != null) {
             final PipAnimationController.PipTransitionAnimator<?> animator =
                     mPipAnimationController.getCurrentAnimator();
             final Rect destinationBounds = animator.getDestinationBounds();
@@ -859,13 +844,15 @@
         // note that this can be called when swipe-to-home or fixed-rotation is happening.
         // Skip this entirely if that's the case.
         final boolean waitForFixedRotationOnEnteringPip = mWaitForFixedRotation
-                && (mState != State.ENTERED_PIP);
-        if ((mInSwipePipToHomeTransition || waitForFixedRotationOnEnteringPip) && fromRotation) {
+                && (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP);
+        if ((mPipTransitionState.getInSwipePipToHomeTransition()
+                || waitForFixedRotationOnEnteringPip) && fromRotation) {
             if (DEBUG) {
                 Log.d(TAG, "Skip onMovementBoundsChanged on rotation change"
-                        + " mInSwipePipToHomeTransition=" + mInSwipePipToHomeTransition
+                        + " InSwipePipToHomeTransition="
+                        + mPipTransitionState.getInSwipePipToHomeTransition()
                         + " mWaitForFixedRotation=" + mWaitForFixedRotation
-                        + " mState=" + mState);
+                        + " getTransitionState=" + mPipTransitionState.getTransitionState());
             }
             return;
         }
@@ -873,7 +860,7 @@
                 mPipAnimationController.getCurrentAnimator();
         if (animator == null || !animator.isRunning()
                 || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) {
-            final boolean rotatingPip = mState.isInPip() && fromRotation;
+            final boolean rotatingPip = mPipTransitionState.isInPip() && fromRotation;
             if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) {
                 // The position will be used by fade-in animation when the fixed rotation is done.
                 mPipBoundsState.setBounds(destinationBoundsOut);
@@ -1006,7 +993,7 @@
             Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect,
             @PipAnimationController.TransitionDirection int direction, int durationMs,
             Consumer<Rect> updateBoundsCallback) {
-        if (!mState.isInPip()) {
+        if (!mPipTransitionState.isInPip()) {
             // TODO: tend to use shouldBlockResizeRequest here as well but need to consider
             // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window
             // container transaction callback and we want to set the mState immediately.
@@ -1036,7 +1023,7 @@
         final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
         mSurfaceTransactionHelper
                 .crop(tx, mLeash, toBounds)
-                .round(tx, mLeash, mState.isInPip());
+                .round(tx, mLeash, mPipTransitionState.isInPip());
         if (mPipMenuController.isMenuVisible()) {
             mPipMenuController.resizePipMenu(mLeash, tx, toBounds);
         } else {
@@ -1114,7 +1101,7 @@
     public void scheduleFinishResizePip(Rect destinationBounds,
             @PipAnimationController.TransitionDirection int direction,
             Consumer<Rect> updateBoundsCallback) {
-        if (mState.shouldBlockResizeRequest()) {
+        if (mPipTransitionState.shouldBlockResizeRequest()) {
             return;
         }
 
@@ -1131,7 +1118,7 @@
         mSurfaceTransactionHelper
                 .crop(tx, mLeash, destinationBounds)
                 .resetScale(tx, mLeash, destinationBounds)
-                .round(tx, mLeash, mState.isInPip());
+                .round(tx, mLeash, mPipTransitionState.isInPip());
         return tx;
     }
 
@@ -1140,7 +1127,7 @@
      */
     public void scheduleOffsetPip(Rect originalBounds, int offset, int duration,
             Consumer<Rect> updateBoundsCallback) {
-        if (mState.shouldBlockResizeRequest()) {
+        if (mPipTransitionState.shouldBlockResizeRequest()) {
             return;
         }
         if (mWaitForFixedRotation) {
@@ -1384,7 +1371,7 @@
         final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f);
         animator.setDuration(mCrossFadeAnimationDuration);
         animator.addUpdateListener(animation -> {
-            if (mState == State.UNDEFINED) {
+            if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
                 // Could happen if onTaskVanished happens during the animation since we may have
                 // set a start delay on this animation.
                 Log.d(TAG, "Task vanished, skip fadeOutAndRemoveOverlay");
@@ -1410,7 +1397,7 @@
     }
 
     private void removeContentOverlay(SurfaceControl surface, Runnable callback) {
-        if (mState == State.UNDEFINED) {
+        if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
             // Avoid double removal, which is fatal.
             return;
         }
@@ -1432,7 +1419,7 @@
         pw.println(innerPrefix + "mToken=" + mToken
                 + " binder=" + (mToken != null ? mToken.asBinder() : null));
         pw.println(innerPrefix + "mLeash=" + mLeash);
-        pw.println(innerPrefix + "mState=" + mState);
+        pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState());
         pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType);
         pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 4759550..6fec1fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -18,6 +18,10 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.util.RotationUtils.deltaRotation;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PIP;
+import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
 import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
 import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS;
@@ -25,9 +29,12 @@
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
 import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection;
 import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
 
 import android.app.TaskInfo;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.view.Surface;
@@ -35,6 +42,7 @@
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransactionCallback;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -49,74 +57,218 @@
  */
 public class PipTransition extends PipTransitionController {
 
+    private final PipTransitionState mPipTransitionState;
     private final int mEnterExitAnimationDuration;
     private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
     private Transitions.TransitionFinishCallback mFinishCallback;
+    private Rect mExitDestinationBounds = new Rect();
 
     public PipTransition(Context context,
-            PipBoundsState pipBoundsState, PipMenuController pipMenuController,
+            PipBoundsState pipBoundsState,
+            PipTransitionState pipTransitionState,
+            PipMenuController pipMenuController,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             PipAnimationController pipAnimationController,
             Transitions transitions,
             @NonNull ShellTaskOrganizer shellTaskOrganizer) {
         super(pipBoundsState, pipMenuController, pipBoundsAlgorithm,
                 pipAnimationController, transitions, shellTaskOrganizer);
+        mPipTransitionState = pipTransitionState;
         mEnterExitAnimationDuration = context.getResources()
                 .getInteger(R.integer.config_pipResizeAnimationDuration);
     }
 
     @Override
+    public void setIsFullAnimation(boolean isFullAnimation) {
+        setOneShotAnimationType(isFullAnimation ? ANIM_TYPE_BOUNDS : ANIM_TYPE_ALPHA);
+    }
+
+    /**
+     * Sets the preferred animation type for one time.
+     * This is typically used to set the animation type to
+     * {@link PipAnimationController#ANIM_TYPE_ALPHA}.
+     */
+    private void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) {
+        mOneShotAnimationType = animationType;
+    }
+
+    @Override
+    public void startTransition(Rect destinationBounds, WindowContainerTransaction out) {
+        if (destinationBounds != null) {
+            mExitDestinationBounds.set(destinationBounds);
+            mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this);
+        } else {
+            mTransitions.startTransition(TRANSIT_REMOVE_PIP, out, this);
+        }
+    }
+
+    @Override
     public boolean startAnimation(@android.annotation.NonNull IBinder transition,
             @android.annotation.NonNull TransitionInfo info,
-            @android.annotation.NonNull SurfaceControl.Transaction t,
+            @android.annotation.NonNull SurfaceControl.Transaction startTransaction,
+            @android.annotation.NonNull SurfaceControl.Transaction finishTransaction,
             @android.annotation.NonNull Transitions.TransitionFinishCallback finishCallback) {
+
+        if (info.getType() == TRANSIT_EXIT_PIP && info.getChanges().size() == 1) {
+            final TransitionInfo.Change change = info.getChanges().get(0);
+            mFinishCallback = finishCallback;
+            startTransaction.apply();
+            boolean success = startExpandAnimation(change.getTaskInfo(), change.getLeash(),
+                    new Rect(mExitDestinationBounds));
+            mExitDestinationBounds.setEmpty();
+            return success;
+        }
+
+        if (info.getType() == TRANSIT_REMOVE_PIP) {
+            startTransaction.apply();
+            finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
+                    mPipBoundsState.getDisplayBounds());
+            finishCallback.onTransitionFinished(null, null);
+            return true;
+        }
+
+        // We only support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps
+        // that enter PiP instantly on opening, mostly from CTS/Flicker tests)
+        if (info.getType() != TRANSIT_PIP && info.getType() != TRANSIT_OPEN) {
+            return false;
+        }
+
+        // Search for an Enter PiP transition (along with a show wallpaper one)
+        TransitionInfo.Change enterPip = null;
+        TransitionInfo.Change wallpaper = null;
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
             if (change.getTaskInfo() != null
                     && change.getTaskInfo().configuration.windowConfiguration.getWindowingMode()
                     == WINDOWING_MODE_PINNED) {
-                mFinishCallback = finishCallback;
-                return startEnterAnimation(change.getTaskInfo(), change.getLeash(), t);
+                enterPip = change;
+            } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+                wallpaper = change;
             }
         }
-        return false;
+        if (enterPip == null) {
+            return false;
+        }
+
+        // Show the wallpaper if there is a wallpaper change.
+        if (wallpaper != null) {
+            startTransaction.show(wallpaper.getLeash());
+            startTransaction.setAlpha(wallpaper.getLeash(), 1.f);
+        }
+
+        mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
+        mFinishCallback = finishCallback;
+        return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(),
+                startTransaction, finishTransaction, enterPip.getStartRotation(),
+                enterPip.getEndRotation());
     }
 
     @Nullable
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
             @NonNull TransitionRequestInfo request) {
-        return null;
+        if (request.getType() == TRANSIT_PIP) {
+            WindowContainerTransaction wct = new WindowContainerTransaction();
+            mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED);
+            if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+                wct.setActivityWindowingMode(request.getTriggerTask().token,
+                        WINDOWING_MODE_UNDEFINED);
+                final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
+                wct.setBounds(request.getTriggerTask().token, destinationBounds);
+            }
+            return wct;
+        } else {
+            return null;
+        }
     }
 
     @Override
     public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds,
             @PipAnimationController.TransitionDirection int direction,
             SurfaceControl.Transaction tx) {
+
+        if (isInPipDirection(direction)) {
+            mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP);
+        }
         WindowContainerTransaction wct = new WindowContainerTransaction();
         prepareFinishResizeTransaction(taskInfo, destinationBounds,
                 direction, tx, wct);
-        mFinishCallback.onTransitionFinished(wct, null);
+        mFinishCallback.onTransitionFinished(wct, new WindowContainerTransactionCallback() {
+            @Override
+            public void onTransactionReady(int id, @NonNull SurfaceControl.Transaction t) {
+                t.merge(tx);
+                t.apply();
+            }
+        });
         finishResizeForMenu(destinationBounds);
     }
 
+    private boolean startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
+            final Rect destinationBounds) {
+        PipAnimationController.PipTransitionAnimator animator =
+                mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(),
+                        mPipBoundsState.getBounds(), destinationBounds, null,
+                        TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, Surface.ROTATION_0);
+
+        animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
+                .setPipAnimationCallback(mPipAnimationCallback)
+                .setDuration(mEnterExitAnimationDuration)
+                .start();
+
+        return true;
+    }
+
     private boolean startEnterAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
-            final SurfaceControl.Transaction t) {
+            final SurfaceControl.Transaction startTransaction,
+            final SurfaceControl.Transaction finishTransaction,
+            final int startRotation, final int endRotation) {
         setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
                 taskInfo.topActivityInfo);
         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
         final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds();
         PipAnimationController.PipTransitionAnimator animator;
+        finishTransaction.setPosition(leash, destinationBounds.left, destinationBounds.top);
+        if (taskInfo.pictureInPictureParams != null
+                && taskInfo.pictureInPictureParams.isAutoEnterEnabled()
+                && mPipTransitionState.getInSwipePipToHomeTransition()) {
+            mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+
+            // PiP menu is attached late in the process here to avoid any artifacts on the leash
+            // caused by addShellRoot when in gesture navigation mode.
+            mPipMenuController.attach(leash);
+            SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+            tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, new float[9])
+                    .setPosition(leash, destinationBounds.left, destinationBounds.top)
+                    .setWindowCrop(leash, destinationBounds.width(), destinationBounds.height());
+            startTransaction.merge(tx);
+            startTransaction.apply();
+            mPipBoundsState.setBounds(destinationBounds);
+            onFinishResize(taskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, tx);
+            sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+            mFinishCallback = null;
+            mPipTransitionState.setInSwipePipToHomeTransition(false);
+            return true;
+        }
+
+        int rotationDelta = deltaRotation(endRotation, startRotation);
+        if (rotationDelta != Surface.ROTATION_0) {
+            Matrix tmpTransform = new Matrix();
+            tmpTransform.postRotate(rotationDelta == Surface.ROTATION_90
+                    ? Surface.ROTATION_270 : Surface.ROTATION_90);
+            startTransaction.setMatrix(leash, tmpTransform, new float[9]);
+        }
         if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
             final Rect sourceHintRect =
                     PipBoundsAlgorithm.getValidSourceHintRect(
                             taskInfo.pictureInPictureParams, currentBounds);
             animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
                     currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
-                    0 /* startingAngle */, Surface.ROTATION_0);
+                    0 /* startingAngle */, rotationDelta);
         } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
-            t.setAlpha(leash, 0f);
-            t.apply();
+            startTransaction.setAlpha(leash, 0f);
+            // PiP menu is attached late in the process here to avoid any artifacts on the leash
+            // caused by addShellRoot when in gesture navigation mode.
+            mPipMenuController.attach(leash);
             animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
                     0f, 1f);
             mOneShotAnimationType = ANIM_TYPE_BOUNDS;
@@ -124,10 +276,12 @@
             throw new RuntimeException("Unrecognized animation type: "
                     + mOneShotAnimationType);
         }
+        startTransaction.apply();
         animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(mEnterExitAnimationDuration)
                 .start();
+
         return true;
     }
 
@@ -158,6 +312,5 @@
         }
 
         wct.setBounds(taskInfo.token, taskBounds);
-        wct.setBoundsChangeTransaction(taskInfo.token, tx);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index d801c91..dbf603c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -19,7 +19,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
-import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
 
 import android.app.PictureInPictureParams;
 import android.app.TaskInfo;
@@ -29,6 +28,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.transition.Transitions;
@@ -46,6 +46,7 @@
     protected final PipBoundsState mPipBoundsState;
     protected final ShellTaskOrganizer mShellTaskOrganizer;
     protected final PipMenuController mPipMenuController;
+    protected final Transitions mTransitions;
     private final Handler mMainHandler;
     private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
 
@@ -55,12 +56,6 @@
                 public void onPipAnimationStart(TaskInfo taskInfo,
                         PipAnimationController.PipTransitionAnimator animator) {
                     final int direction = animator.getTransitionDirection();
-                    if (direction == TRANSITION_DIRECTION_TO_PIP) {
-                        // TODO (b//169221267): Add jank listener for transactions without buffer
-                        //  updates.
-                        //InteractionJankMonitor.getInstance().begin(
-                        //        InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000);
-                    }
                     sendOnPipTransitionStarted(direction);
                 }
 
@@ -74,12 +69,6 @@
                     }
                     onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx);
                     sendOnPipTransitionFinished(direction);
-                    if (direction == TRANSITION_DIRECTION_TO_PIP) {
-                        // TODO (b//169221267): Add jank listener for transactions without buffer
-                        //  updates.
-                        //InteractionJankMonitor.getInstance().end(
-                        //        InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
-                    }
                 }
 
                 @Override
@@ -98,6 +87,22 @@
             SurfaceControl.Transaction tx) {
     }
 
+    /**
+     * Called to inform the transition that the animation should start with the assumption that
+     * PiP is not animating from its original bounds, but rather a continuation of another
+     * animation. For example, gesture navigation would first fade out the PiP activity, and the
+     * transition should be responsible to animate in (such as fade in) the PiP.
+     */
+    public void setIsFullAnimation(boolean isFullAnimation) {
+    }
+
+    /**
+     * Called when the Shell wants to starts a transition/animation.
+     */
+    public void startTransition(Rect destinationBounds, WindowContainerTransaction out) {
+        // Default implementation does nothing.
+    }
+
     public PipTransitionController(PipBoundsState pipBoundsState,
             PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm,
             PipAnimationController pipAnimationController, Transitions transitions,
@@ -107,6 +112,7 @@
         mShellTaskOrganizer = shellTaskOrganizer;
         mPipBoundsAlgorithm = pipBoundsAlgorithm;
         mPipAnimationController = pipAnimationController;
+        mTransitions = transitions;
         mMainHandler = new Handler(Looper.getMainLooper());
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             transitions.addHandler(this);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
new file mode 100644
index 0000000..85e56b7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip;
+
+import android.annotation.IntDef;
+import android.app.PictureInPictureParams;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and
+ * {@link PipTransition}.
+ */
+public class PipTransitionState {
+
+    public static final int UNDEFINED = 0;
+    public static final int TASK_APPEARED = 1;
+    public static final int ENTRY_SCHEDULED = 2;
+    public static final int ENTERING_PIP = 3;
+    public static final int ENTERED_PIP = 4;
+    public static final int EXITING_PIP = 5;
+
+    /**
+     * If set to {@code true}, no entering PiP transition would be kicked off and most likely
+     * it's due to the fact that Launcher is handling the transition directly when swiping
+     * auto PiP-able Activity to home.
+     * See also {@link PipTaskOrganizer#startSwipePipToHome(ComponentName, ActivityInfo,
+     * PictureInPictureParams)}.
+     */
+    private boolean mInSwipePipToHomeTransition;
+
+    // Not a complete set of states but serves what we want right now.
+    @IntDef(prefix = { "TRANSITION_STATE_" }, value =  {
+            UNDEFINED,
+            TASK_APPEARED,
+            ENTRY_SCHEDULED,
+            ENTERING_PIP,
+            ENTERED_PIP,
+            EXITING_PIP
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TransitionState {}
+
+    private @TransitionState int mState;
+
+    public PipTransitionState() {
+        mState = UNDEFINED;
+    }
+
+    public void setTransitionState(@TransitionState int state) {
+        mState = state;
+    }
+
+    public @TransitionState int getTransitionState() {
+        return mState;
+    }
+
+    public boolean isInPip() {
+        return mState >= TASK_APPEARED
+                && mState != EXITING_PIP;
+    }
+
+    public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) {
+        mInSwipePipToHomeTransition = inSwipePipToHomeTransition;
+    }
+
+    public boolean getInSwipePipToHomeTransition() {
+        return mInSwipePipToHomeTransition;
+    }
+    /**
+     * Resize request can be initiated in other component, ignore if we are no longer in PIP,
+     * still waiting for animation or we're exiting from it.
+     *
+     * @return {@code true} if the resize request should be blocked/ignored.
+     */
+    public boolean shouldBlockResizeRequest() {
+        return mState < ENTERING_PIP
+                || mState == EXITING_PIP;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index a646b07..ae8c1b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -60,8 +60,7 @@
     private static final boolean DEBUG = false;
 
     public static final int MENU_STATE_NONE = 0;
-    public static final int MENU_STATE_CLOSE = 1;
-    public static final int MENU_STATE_FULL = 2;
+    public static final int MENU_STATE_FULL = 1;
 
     /**
      * A listener interface to receive notification on changes in PIP.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 63f1985..8e5c5c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -21,7 +21,16 @@
 import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
 import static android.view.WindowManager.INPUT_CONSUMER_PIP;
 
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE;
 import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
 
 import android.app.ActivityManager;
@@ -52,6 +61,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.wm.shell.R;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.common.DisplayChangeController;
@@ -67,6 +77,7 @@
 import com.android.wm.shell.pip.IPipAnimationListener;
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipMediaController;
@@ -74,6 +85,7 @@
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipUtils;
+import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -445,11 +457,18 @@
             return;
         }
         Runnable updateDisplayLayout = () -> {
+            final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS
+                    && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation();
             mPipBoundsState.setDisplayLayout(layout);
+            final WindowContainerTransaction wct =
+                    fromRotation ? new WindowContainerTransaction() : null;
             updateMovementBounds(null /* toBounds */,
-                    false /* fromRotation */, false /* fromImeAdjustment */,
+                    fromRotation, false /* fromImeAdjustment */,
                     false /* fromShelfAdjustment */,
-                    null /* windowContainerTransaction */);
+                    wct /* windowContainerTransaction */);
+            if (wct != null) {
+                mPipTaskOrganizer.applyFinishBoundsResize(wct, TRANSITION_DIRECTION_SAME);
+            }
         };
 
         if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) {
@@ -528,6 +547,8 @@
 
     private void setPinnedStackAnimationType(int animationType) {
         mPipTaskOrganizer.setOneShotAnimationType(animationType);
+        mPipTransitionController.setIsFullAnimation(
+                animationType == PipAnimationController.ANIM_TYPE_BOUNDS);
     }
 
     private void setPinnedStackAnimationListener(IPipAnimationListener callback) {
@@ -564,8 +585,37 @@
         mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds, overlay);
     }
 
+    private String getTransitionTag(int direction) {
+        switch (direction) {
+            case TRANSITION_DIRECTION_TO_PIP:
+                return "TRANSITION_TO_PIP";
+            case TRANSITION_DIRECTION_LEAVE_PIP:
+                return "TRANSITION_LEAVE_PIP";
+            case TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN:
+                return "TRANSITION_LEAVE_PIP_TO_SPLIT_SCREEN";
+            case TRANSITION_DIRECTION_REMOVE_STACK:
+                return "TRANSITION_REMOVE_STACK";
+            case TRANSITION_DIRECTION_SNAP_AFTER_RESIZE:
+                return "TRANSITION_SNAP_AFTER_RESIZE";
+            case TRANSITION_DIRECTION_USER_RESIZE:
+                return "TRANSITION_USER_RESIZE";
+            case TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND:
+                return "TRANSITION_EXPAND_OR_UNEXPAND";
+            default:
+                return "TRANSITION_LEAVE_UNKNOWN";
+        }
+    }
+
     @Override
     public void onPipTransitionStarted(int direction, Rect pipBounds) {
+        // Begin InteractionJankMonitor with PIP transition CUJs
+        final InteractionJankMonitor.Configuration.Builder builder =
+                InteractionJankMonitor.Configuration.Builder.withSurface(
+                        CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl())
+                .setTag(getTransitionTag(direction))
+                .setTimeout(2000);
+        InteractionJankMonitor.getInstance().begin(builder);
+
         if (isOutPipDirection(direction)) {
             // Exiting PIP, save the reentry state to restore to when re-entering.
             saveReentryState(pipBounds);
@@ -604,6 +654,9 @@
     }
 
     private void onPipTransitionFinishedOrCanceled(int direction) {
+        // End InteractionJankMonitor with PIP transition by CUJs
+        InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION);
+
         // Re-enable touches after the animation completes
         mTouchHandler.setTouchEnabled(true);
         mTouchHandler.onPinnedStackAnimationEnded(direction);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
index 1da9577..0fbdf90 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -30,6 +31,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
@@ -93,6 +95,7 @@
     private int mTargetSize;
     private int mDismissAreaHeight;
     private float mMagneticFieldRadiusPercent = 1f;
+    private WindowInsets mWindowInsets;
 
     private SurfaceControl mTaskLeash;
     private boolean mHasDismissTargetSurface;
@@ -123,6 +126,13 @@
                 mContext.getDrawable(R.drawable.floating_dismiss_gradient_transition));
         mTargetViewContainer.setClipChildren(false);
         mTargetViewContainer.addView(mTargetView);
+        mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+            if (!windowInsets.equals(mWindowInsets)) {
+                mWindowInsets = windowInsets;
+                updateMagneticTargetSize();
+            }
+            return windowInsets;
+        });
 
         mMagnetizedPip = mMotionHelper.getMagnetizedPip();
         mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0);
@@ -158,14 +168,16 @@
 
             @Override
             public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
-                mMainExecutor.executeDelayed(() -> {
-                    mMotionHelper.notifyDismissalPending();
-                    mMotionHelper.animateDismiss();
-                    hideDismissTargetMaybe();
+                if (mEnableDismissDragToEdge) {
+                    mMainExecutor.executeDelayed(() -> {
+                        mMotionHelper.notifyDismissalPending();
+                        mMotionHelper.animateDismiss();
+                        hideDismissTargetMaybe();
 
-                    mPipUiEventLogger.log(
-                            PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE);
-                }, 0);
+                        mPipUiEventLogger.log(
+                                PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE);
+                    }, 0);
+                }
             }
         });
 
@@ -199,10 +211,13 @@
         final Resources res = mContext.getResources();
         mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
         mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
+        final WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+        final Insets navInset = insets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.navigationBars());
         final FrameLayout.LayoutParams newParams =
                 new FrameLayout.LayoutParams(mTargetSize, mTargetSize);
         newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
-        newParams.bottomMargin = mContext.getResources().getDimensionPixelSize(
+        newParams.bottomMargin = navInset.bottom + mContext.getResources().getDimensionPixelSize(
                 R.dimen.floating_dismiss_bottom_margin);
         mTargetView.setLayoutParams(newParams);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
index 67b1e6d..8ef2b6b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -23,7 +23,6 @@
 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
 
-import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE;
 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
 
@@ -203,7 +202,7 @@
 
             @Override
             public boolean performAccessibilityAction(View host, int action, Bundle args) {
-                if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) {
+                if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) {
                     mController.showMenu();
                 }
                 return super.performAccessibilityAction(host, action, args);
@@ -271,13 +270,12 @@
                     mDismissButton.getAlpha(), 1f);
             ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA,
                     mResizeHandle.getAlpha(),
-                    ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle
-                            ? 1f : 0f);
+                    ENABLE_RESIZE_HANDLE && showResizeHandle ? 1f : 0f);
             if (menuState == MENU_STATE_FULL) {
                 mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim,
                         resizeAnim);
             } else {
-                mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim);
+                mMenuContainerAnimator.playTogether(resizeAnim);
             }
             mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
             mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS);
@@ -429,7 +427,7 @@
 
         FrameLayout.LayoutParams expandedLp =
                 (FrameLayout.LayoutParams) expandContainer.getLayoutParams();
-        if (mActions.isEmpty() || menuState == MENU_STATE_CLOSE || menuState == MENU_STATE_NONE) {
+        if (mActions.isEmpty() || menuState == MENU_STATE_NONE) {
             actionsContainer.setVisibility(View.INVISIBLE);
 
             // Update the expand container margin to adjust the center of the expand button to
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 7867f93..9f2f6a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -22,7 +22,6 @@
 import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT;
 import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE;
 import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT;
-import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE;
 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE;
@@ -81,7 +80,6 @@
 
     private final PhonePipMenuController mMenuController;
     private final AccessibilityManager mAccessibilityManager;
-    private boolean mShowPipMenuOnAnimationEnd = false;
 
     /**
      * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the
@@ -280,7 +278,6 @@
     public void onActivityPinned() {
         mPipDismissTargetHandler.createOrUpdateDismissTarget();
 
-        mShowPipMenuOnAnimationEnd = true;
         mPipResizeGestureHandler.onActivityPinned();
         mFloatingContentCoordinator.onContentAdded(mMotionHelper);
     }
@@ -304,13 +301,6 @@
             // Set the initial bounds as the user resize bounds.
             mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
         }
-
-        if (mShowPipMenuOnAnimationEnd) {
-            mMenuController.showMenu(MENU_STATE_CLOSE, mPipBoundsState.getBounds(),
-                    true /* allowMenuTimeout */, false /* willResizeMenu */,
-                    shouldShowResizeHandle());
-            mShowPipMenuOnAnimationEnd = false;
-        }
     }
 
     public void onConfigurationChanged() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index b7caf72..551476d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -58,7 +58,8 @@
 
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @android.annotation.NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         return false;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
index 1fc4d12..ab3cbd6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
@@ -47,6 +47,8 @@
 
     /** Callback for size compat UI interaction. */
     public interface SizeCompatUICallback {
+        /** Called when the size compat restart button appears. */
+        void onSizeCompatRestartButtonAppeared(int taskId);
         /** Called when the size compat restart button is clicked. */
         void onSizeCompatRestartButtonClicked(int taskId);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
index 20021eb..7cf9559 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
@@ -106,6 +106,8 @@
             mShouldShowHint = false;
             createSizeCompatHint();
         }
+
+        mCallback.onSizeCompatRestartButtonAppeared(mTaskId);
     }
 
     /** Creates the restart button hint window. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index 8f0892f..3d3a630 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -20,7 +20,9 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.UserHandle;
-import android.window.IRemoteTransition;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.window.RemoteTransition;
 
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
 
@@ -50,9 +52,10 @@
     oneway void removeFromSideStage(int taskId) = 4;
 
     /**
-     * Removes the split-screen stages.
+     * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID
+     * to indicate leaving no top task after leaving split-screen.
      */
-    oneway void exitSplitScreen() = 5;
+    oneway void exitSplitScreen(int toTopTaskId) = 5;
 
     /**
      * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible.
@@ -77,9 +80,24 @@
             int position, in Bundle options) = 9;
 
     /**
-     * Starts tasks simultaneously in one transition. The first task in the list will be in the
-     * main-stage and on the left/top.
+     * Starts tasks simultaneously in one transition.
      */
     oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
-            in Bundle sideOptions, int sidePosition, in IRemoteTransition remoteTransition) = 10;
+            in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10;
+
+    /**
+     * Version of startTasks using legacy transition system.
+     */
+     oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
+                            int sideTaskId, in Bundle sideOptions, int sidePosition,
+                            in RemoteAnimationAdapter adapter) = 11;
+
+    /**
+     * Blocking call that notifies and gets additional split-screen targets when entering
+     * recents (for example: the dividerBar).
+     * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled).
+     * @param appTargets apps that will be re-parented to display area
+     */
+    RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
+                                                   in RemoteAnimationTarget[] appTargets) = 12;
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index d0998eb..a47a152 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 
+import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
@@ -38,33 +39,34 @@
 
     MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession) {
-        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession);
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
     }
 
     boolean isActive() {
         return mIsActive;
     }
 
-    void activate(Rect rootBounds, WindowContainerTransaction wct) {
+    void activate(Rect rootBounds, WindowContainerTransaction wct, boolean includingTopTask) {
         if (mIsActive) return;
 
         final WindowContainerToken rootToken = mRootTaskInfo.token;
         wct.setBounds(rootToken, rootBounds)
                 .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW)
-                .setLaunchRoot(
-                        rootToken,
-                        CONTROLLED_WINDOWING_MODES,
-                        CONTROLLED_ACTIVITY_TYPES)
-                .reparentTasks(
-                        null /* currentParent */,
-                        rootToken,
-                        CONTROLLED_WINDOWING_MODES,
-                        CONTROLLED_ACTIVITY_TYPES,
-                        true /* onTop */)
                 // Moving the root task to top after the child tasks were re-parented , or the root
                 // task cannot be visible and focused.
                 .reorder(rootToken, true /* onTop */);
+        if (includingTopTask) {
+            wct.reparentTasks(
+                    null /* currentParent */,
+                    rootToken,
+                    CONTROLLED_WINDOWING_MODES,
+                    CONTROLLED_ACTIVITY_TYPES,
+                    true /* onTop */,
+                    true /* reparentTopOnly */);
+        }
 
         mIsActive = true;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java
new file mode 100644
index 0000000..a459c8d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.view.IWindow;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.widget.FrameLayout;
+
+import com.android.wm.shell.R;
+
+/**
+ * Handles drawing outline of the bounds of provided root surface. The outline will be drown with
+ * the consideration of display insets like status bar, navigation bar and display cutout.
+ */
+class OutlineManager extends WindowlessWindowManager {
+    private static final String WINDOW_NAME = "SplitOutlineLayer";
+    private final Context mContext;
+    private final Rect mRootBounds = new Rect();
+    private final Rect mTempRect = new Rect();
+    private final Rect mLastOutlineBounds = new Rect();
+    private final InsetsState mInsetsState = new InsetsState();
+    private final int mExpandedTaskBarHeight;
+    private OutlineView mOutlineView;
+    private SurfaceControlViewHost mViewHost;
+    private SurfaceControl mHostLeash;
+    private SurfaceControl mLeash;
+
+    OutlineManager(Context context, Configuration configuration) {
+        super(configuration, null /* rootSurface */, null /* hostInputToken */);
+        mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
+                null /* options */);
+        mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+    }
+
+    @Override
+    protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+        b.setParent(mHostLeash);
+    }
+
+    void inflate(SurfaceControl rootLeash, Rect rootBounds) {
+        if (mLeash != null || mViewHost != null) return;
+
+        mHostLeash = rootLeash;
+        mRootBounds.set(rootBounds);
+        mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+
+        final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext)
+                .inflate(R.layout.split_outline, null);
+        mOutlineView = rootLayout.findViewById(R.id.split_outline);
+
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
+                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
+        lp.width = mRootBounds.width();
+        lp.height = mRootBounds.height();
+        lp.token = new Binder();
+        lp.setTitle(WINDOW_NAME);
+        lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
+        // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
+        //  TRUSTED_OVERLAY for windowless window without input channel.
+        mViewHost.setView(rootLayout, lp);
+        mLeash = getSurfaceControl(mViewHost.getWindowToken());
+
+        drawOutline();
+    }
+
+    void release() {
+        if (mViewHost != null) {
+            mViewHost.release();
+            mViewHost = null;
+        }
+        mRootBounds.setEmpty();
+        mLastOutlineBounds.setEmpty();
+        mOutlineView = null;
+        mHostLeash = null;
+        mLeash = null;
+    }
+
+    @Nullable
+    SurfaceControl getOutlineLeash() {
+        return mLeash;
+    }
+
+    void setVisibility(boolean visible) {
+        if (mOutlineView != null) {
+            mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    void setRootBounds(Rect rootBounds) {
+        if (mViewHost == null || mViewHost.getView() == null) {
+            return;
+        }
+
+        if (!mRootBounds.equals(rootBounds)) {
+            WindowManager.LayoutParams lp =
+                    (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
+            lp.width = rootBounds.width();
+            lp.height = rootBounds.height();
+            mViewHost.relayout(lp);
+            mRootBounds.set(rootBounds);
+            drawOutline();
+        }
+    }
+
+    void onInsetsChanged(InsetsState insetsState) {
+        if (!mInsetsState.equals(insetsState)) {
+            mInsetsState.set(insetsState);
+            drawOutline();
+        }
+    }
+
+    private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) {
+        outBounds.set(rootBounds);
+        final InsetsSource taskBarInsetsSource =
+                insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        // Only insets the divider bar with task bar when it's expanded so that the rounded corners
+        // will be drawn against task bar.
+        if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+            outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds));
+        }
+
+        // Offset the coordinate from screen based to surface based.
+        outBounds.offset(-rootBounds.left, -rootBounds.top);
+    }
+
+    void drawOutline() {
+        if (mOutlineView == null) {
+            return;
+        }
+
+        computeOutlineBounds(mRootBounds, mInsetsState, mTempRect);
+        if (mTempRect.equals(mLastOutlineBounds)) {
+            return;
+        }
+
+        ViewGroup.MarginLayoutParams lp =
+                (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams();
+        lp.leftMargin = mTempRect.left;
+        lp.topMargin = mTempRect.top;
+        lp.width = mTempRect.width();
+        lp.height = mTempRect.height();
+        mOutlineView.setLayoutParams(lp);
+        mLastOutlineBounds.set(mTempRect);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java
new file mode 100644
index 0000000..94dd9b2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
+import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
+import static android.view.RoundedCorner.POSITION_TOP_LEFT;
+import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.RoundedCorner;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.R;
+
+/** View for drawing split outline. */
+public class OutlineView extends View {
+    private final Paint mPaint = new Paint();
+    private final Path mPath = new Path();
+    private final float[] mRadii = new float[8];
+
+    public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeWidth(
+                getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
+        mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        // TODO(b/200850654): match the screen corners with the actual display decor.
+        mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT);
+        mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT);
+        mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT);
+        mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT);
+    }
+
+    private int getCornerRadius(@RoundedCorner.Position int position) {
+        final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position);
+        return roundedCorner == null ? 0 : roundedCorner.getRadius();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (changed) {
+            mPath.reset();
+            mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.drawPath(mPath, mPaint);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
index 82f95a4..dc8fb9f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
@@ -16,27 +16,41 @@
 
 package com.android.wm.shell.splitscreen;
 
+import android.annotation.CallSuper;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.content.Context;
 import android.graphics.Rect;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.SyncTransactionQueue;
 
 /**
  * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up
  * here. All other task are launch in the {@link MainStage}.
+ *
  * @see StageCoordinator
  */
-class SideStage extends StageTaskListener {
+class SideStage extends StageTaskListener implements
+        DisplayInsetsController.OnInsetsChangedListener {
     private static final String TAG = SideStage.class.getSimpleName();
+    private final Context mContext;
+    private OutlineManager mOutlineManager;
 
-    SideStage(ShellTaskOrganizer taskOrganizer, int displayId,
+    SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession) {
-        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession);
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
+        mContext = context;
     }
 
     void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds,
@@ -44,7 +58,7 @@
         final WindowContainerToken rootToken = mRootTaskInfo.token;
         wct.setBounds(rootToken, rootBounds)
                 .reparent(task.token, rootToken, true /* onTop*/)
-                // Moving the root task to top after the child tasks were repareted , or the root
+                // Moving the root task to top after the child tasks were reparented , or the root
                 // task cannot be visible and focused.
                 .reorder(rootToken, true /* onTop */);
     }
@@ -69,4 +83,62 @@
         wct.reparent(task.token, newParent, false /* onTop */);
         return true;
     }
+
+    @Nullable
+    public SurfaceControl getOutlineLeash() {
+        return mOutlineManager.getOutlineLeash();
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        super.onTaskAppeared(taskInfo, leash);
+        if (isRootTask(taskInfo)) {
+            mOutlineManager = new OutlineManager(mContext, taskInfo.configuration);
+            enableOutline(true);
+        }
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        super.onTaskInfoChanged(taskInfo);
+        if (isRootTask(taskInfo)) {
+            mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds());
+        }
+    }
+
+    private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) {
+        return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId;
+    }
+
+    void enableOutline(boolean enable) {
+        if (mOutlineManager == null) {
+            return;
+        }
+
+        if (enable) {
+            if (mRootTaskInfo != null) {
+                mOutlineManager.inflate(mRootLeash,
+                        mRootTaskInfo.configuration.windowConfiguration.getBounds());
+            }
+        } else {
+            mOutlineManager.release();
+        }
+    }
+
+    void setOutlineVisibility(boolean visible) {
+        mOutlineManager.setVisibility(visible);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mOutlineManager.onInsetsChanged(insetsState);
+    }
+
+    @Override
+    public void insetsControlChanged(InsetsState insetsState,
+            InsetsSourceControl[] activeControls) {
+        insetsChanged(insetsState);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 002bfb6..e86462f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -17,10 +17,13 @@
 package com.android.wm.shell.splitscreen;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
 
+import java.util.concurrent.Executor;
+
 /**
  * Interface to engage split-screen feature.
  * TODO: Figure out which of these are actually needed outside of the Shell
@@ -53,10 +56,18 @@
 
     /** Callback interface for listening to changes in a split-screen stage. */
     interface SplitScreenListener {
-        void onStagePositionChanged(@StageType int stage, @SplitPosition int position);
-        void onTaskStageChanged(int taskId, @StageType int stage, boolean visible);
+        default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {}
+        default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {}
+        default void onSplitVisibilityChanged(boolean visible) {}
     }
 
+    /** Registers listener that gets split screen callback. */
+    void registerSplitScreenListener(@NonNull SplitScreenListener listener,
+            @NonNull Executor executor);
+
+    /** Unregisters listener that gets split screen callback. */
+    void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
+
     /**
      * Returns a binder that can be passed to an external process to manipulate SplitScreen.
      */
@@ -64,6 +75,18 @@
         return null;
     }
 
+    /**
+     * Called when the keyguard occluded state changes.
+     * @param occluded Indicates if the keyguard is now occluded.
+     */
+    void onKeyguardOccludedChanged(boolean occluded);
+
+    /**
+     * Called when the visibility of the keyguard changes.
+     * @param showing Indicates if the keyguard is now visible.
+     */
+    void onKeyguardVisibilityChanged(boolean showing);
+
     /** Get a string representation of a stage type */
     static String stageTypeToString(@StageType int stage) {
         switch (stage) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 9a457b5..ec71fbe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -16,15 +16,14 @@
 
 package com.android.wm.shell.splitscreen;
 
+import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.ActivityManager.START_TASK_TO_FRONT;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
-import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
-import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
-import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
@@ -38,16 +37,27 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 import android.util.Slog;
-import android.window.IRemoteTransition;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.WindowManager;
+import android.window.RemoteTransition;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.logging.InstanceId;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -55,16 +65,22 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
 import com.android.wm.shell.draganddrop.DragAndDropPolicy;
-import com.android.wm.shell.splitscreen.ISplitScreenListener;
+import com.android.wm.shell.transition.LegacyTransitions;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import javax.inject.Provider;
 
 /**
  * Class manages split-screen multitasking mode and implements the main interface
  * {@link SplitScreen}.
  * @see StageCoordinator
  */
+// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
 public class SplitScreenController implements DragAndDropPolicy.Starter,
         RemoteCallable<SplitScreenController> {
     private static final String TAG = SplitScreenController.class.getSimpleName();
@@ -76,8 +92,11 @@
     private final ShellExecutor mMainExecutor;
     private final SplitScreenImpl mImpl = new SplitScreenImpl();
     private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
     private final Transitions mTransitions;
     private final TransactionPool mTransactionPool;
+    private final SplitscreenEventLogger mLogger;
+    private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
 
     private StageCoordinator mStageCoordinator;
 
@@ -85,15 +104,20 @@
             SyncTransactionQueue syncQueue, Context context,
             RootTaskDisplayAreaOrganizer rootTDAOrganizer,
             ShellExecutor mainExecutor, DisplayImeController displayImeController,
-            Transitions transitions, TransactionPool transactionPool) {
+            DisplayInsetsController displayInsetsController,
+            Transitions transitions, TransactionPool transactionPool,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mContext = context;
         mRootTDAOrganizer = rootTDAOrganizer;
         mMainExecutor = mainExecutor;
         mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
         mTransitions = transitions;
         mTransactionPool = transactionPool;
+        mUnfoldControllerProvider = unfoldControllerProvider;
+        mLogger = new SplitscreenEventLogger();
     }
 
     public SplitScreen asSplitScreen() {
@@ -114,8 +138,9 @@
         if (mStageCoordinator == null) {
             // TODO: Multi-display
             mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
-                    mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, mTransitions,
-                    mTransactionPool);
+                    mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
+                    mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
+                    mUnfoldControllerProvider);
         }
     }
 
@@ -140,8 +165,12 @@
         return mStageCoordinator.removeFromSideStage(taskId);
     }
 
+    public void setSideStageOutline(boolean enable) {
+        mStageCoordinator.setSideStageOutline(enable);
+    }
+
     public void setSideStagePosition(@SplitPosition int sideStagePosition) {
-        mStageCoordinator.setSideStagePosition(sideStagePosition);
+        mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */);
     }
 
     public void setSideStageVisibility(boolean visible) {
@@ -153,8 +182,16 @@
                 leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
     }
 
-    public void exitSplitScreen() {
-        mStageCoordinator.exitSplitScreen();
+    public void exitSplitScreen(int toTopTaskId, int exitReason) {
+        mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
+    }
+
+    public void onKeyguardOccludedChanged(boolean occluded) {
+        mStageCoordinator.onKeyguardOccludedChanged(occluded);
+    }
+
+    public void onKeyguardVisibilityChanged(boolean showing) {
+        mStageCoordinator.onKeyguardVisibilityChanged(showing);
     }
 
     public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
@@ -175,10 +212,14 @@
 
     public void startTask(int taskId, @SplitScreen.StageType int stage,
             @SplitPosition int position, @Nullable Bundle options) {
-        options = resolveStartStage(stage, position, options);
+        options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
 
         try {
-            ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
+            final int result =
+                    ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
+            if (result == START_SUCCESS || result == START_TASK_TO_FRONT) {
+                mStageCoordinator.evictOccludedChildren(position);
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to launch task", e);
         }
@@ -187,13 +228,14 @@
     public void startShortcut(String packageName, String shortcutId,
             @SplitScreen.StageType int stage, @SplitPosition int position,
             @Nullable Bundle options, UserHandle user) {
-        options = resolveStartStage(stage, position, options);
+        options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
 
         try {
             LauncherApps launcherApps =
                     mContext.getSystemService(LauncherApps.class);
             launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
                     options, user);
+            mStageCoordinator.evictOccludedChildren(position);
         } catch (ActivityNotFoundException e) {
             Slog.e(TAG, "Failed to launch shortcut", e);
         }
@@ -202,64 +244,85 @@
     public void startIntent(PendingIntent intent, Intent fillInIntent,
             @SplitScreen.StageType int stage, @SplitPosition int position,
             @Nullable Bundle options) {
-        options = resolveStartStage(stage, position, options);
-
-        try {
-            intent.send(mContext, 0, fillInIntent, null, null, null, options);
-        } catch (PendingIntent.CanceledException e) {
-            Slog.e(TAG, "Failed to launch activity", e);
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+            startIntentLegacy(intent, fillInIntent, stage, position, options);
+            return;
         }
+        mStageCoordinator.startIntent(intent, fillInIntent, stage, position, options,
+                null /* remote */);
     }
 
-    private Bundle resolveStartStage(@SplitScreen.StageType int stage,
-            @SplitPosition int position, @Nullable Bundle options) {
-        switch (stage) {
-            case STAGE_TYPE_UNDEFINED: {
-                // Use the stage of the specified position is valid.
-                if (position != SPLIT_POSITION_UNDEFINED) {
-                    if (position == mStageCoordinator.getSideStagePosition()) {
-                        options = resolveStartStage(STAGE_TYPE_SIDE, position, options);
-                    } else {
-                        options = resolveStartStage(STAGE_TYPE_MAIN, position, options);
-                    }
-                } else {
-                    // Exit split-screen and launch fullscreen since stage wasn't specified.
-                    mStageCoordinator.exitSplitScreen();
-                }
-                break;
-            }
-            case STAGE_TYPE_SIDE: {
-                if (position != SPLIT_POSITION_UNDEFINED) {
-                    mStageCoordinator.setSideStagePosition(position);
-                } else {
-                    position = mStageCoordinator.getSideStagePosition();
-                }
-                if (options == null) {
-                    options = new Bundle();
-                }
-                mStageCoordinator.updateActivityOptions(options, position);
-                break;
-            }
-            case STAGE_TYPE_MAIN: {
-                if (position != SPLIT_POSITION_UNDEFINED) {
-                    // Set the side stage opposite of what we want to the main stage.
-                    final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT
-                            ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
-                    mStageCoordinator.setSideStagePosition(sideStagePosition);
-                } else {
-                    position = mStageCoordinator.getMainStagePosition();
-                }
-                if (options == null) {
-                    options = new Bundle();
-                }
-                mStageCoordinator.updateActivityOptions(options, position);
-                break;
-            }
-            default:
-                throw new IllegalArgumentException("Unknown stage=" + stage);
-        }
+    private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options) {
+        LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
+            @Override
+            public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+                    IRemoteAnimationFinishedCallback finishedCallback,
+                    SurfaceControl.Transaction t) {
+                mStageCoordinator.updateSurfaceBounds(null /* layout */, t);
 
-        return options;
+                if (apps != null) {
+                    for (int i = 0; i < apps.length; ++i) {
+                        if (apps[i].mode == MODE_OPENING) {
+                            t.show(apps[i].leash);
+                        }
+                    }
+                }
+
+                t.apply();
+                if (finishedCallback != null) {
+                    try {
+                        finishedCallback.onAnimationFinished();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error finishing legacy transition: ", e);
+                    }
+                }
+
+                // Launching a new app into a specific split evicts tasks previously in the same
+                // split.
+                mStageCoordinator.evictOccludedChildren(position);
+            }
+        };
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        options = mStageCoordinator.resolveStartStage(stage, position, options, wct);
+        wct.sendPendingIntent(intent, fillInIntent, options);
+        mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
+    }
+
+    RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) {
+        if (!isSplitScreenVisible()) return null;
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+                .setContainerLayer()
+                .setName("RecentsAnimationSplitTasks")
+                .setHidden(false)
+                .setCallsite("SplitScreenController#onGoingtoRecentsLegacy");
+        mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder);
+        SurfaceControl sc = builder.build();
+        SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+
+        // Ensure that we order these in the parent in the right z-order as their previous order
+        Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex);
+        int layer = 1;
+        for (RemoteAnimationTarget appTarget : apps) {
+            transaction.reparent(appTarget.leash, sc);
+            transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left,
+                    appTarget.screenSpaceBounds.top);
+            transaction.setLayer(appTarget.leash, layer++);
+        }
+        transaction.apply();
+        transaction.close();
+        return new RemoteAnimationTarget[]{
+                mStageCoordinator.getDividerBarLegacyTarget(),
+                mStageCoordinator.getOutlineLegacyTarget()};
+    }
+
+    /**
+     * Sets drag info to be logged when splitscreen is entered.
+     */
+    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
     }
 
     public void dump(@NonNull PrintWriter pw, String prefix) {
@@ -275,6 +338,38 @@
     @ExternalThread
     private class SplitScreenImpl implements SplitScreen {
         private ISplitScreenImpl mISplitScreen;
+        private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>();
+        private final SplitScreen.SplitScreenListener mListener = new SplitScreenListener() {
+            @Override
+            public void onStagePositionChanged(int stage, int position) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onStagePositionChanged(stage, position);
+                    });
+                }
+            }
+
+            @Override
+            public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible);
+                    });
+                }
+            }
+
+            @Override
+            public void onSplitVisibilityChanged(boolean visible) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onSplitVisibilityChanged(visible);
+                    });
+                }
+            }
+        };
 
         @Override
         public ISplitScreen createExternalInterface() {
@@ -284,6 +379,48 @@
             mISplitScreen = new ISplitScreenImpl(SplitScreenController.this);
             return mISplitScreen;
         }
+
+        @Override
+        public void onKeyguardOccludedChanged(boolean occluded) {
+            mMainExecutor.execute(() -> {
+                SplitScreenController.this.onKeyguardOccludedChanged(occluded);
+            });
+        }
+
+        @Override
+        public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) {
+            if (mExecutors.containsKey(listener)) return;
+
+            mMainExecutor.execute(() -> {
+                if (mExecutors.size() == 0) {
+                    SplitScreenController.this.registerSplitScreenListener(mListener);
+                }
+
+                mExecutors.put(listener, executor);
+            });
+
+            executor.execute(() -> {
+                mStageCoordinator.sendStatusToListener(listener);
+            });
+        }
+
+        @Override
+        public void unregisterSplitScreenListener(SplitScreenListener listener) {
+            mMainExecutor.execute(() -> {
+                mExecutors.remove(listener);
+
+                if (mExecutors.size() == 0) {
+                    SplitScreenController.this.unregisterSplitScreenListener(mListener);
+                }
+            });
+        }
+
+        @Override
+        public void onKeyguardVisibilityChanged(boolean showing) {
+            mMainExecutor.execute(() -> {
+                SplitScreenController.this.onKeyguardVisibilityChanged(showing);
+            });
+        }
     }
 
     /**
@@ -377,10 +514,11 @@
         }
 
         @Override
-        public void exitSplitScreen() {
+        public void exitSplitScreen(int toTopTaskId) {
             executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
                     (controller) -> {
-                        controller.exitSplitScreen();
+                        controller.exitSplitScreen(toTopTaskId,
+                                FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT);
                     });
         }
 
@@ -417,10 +555,20 @@
         }
 
         @Override
+        public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
+                int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+                RemoteAnimationAdapter adapter) {
+            executeRemoteCallWithTaskPermission(mController, "startTasks",
+                    (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
+                            mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
+                            adapter));
+        }
+
+        @Override
         public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
                 int sideTaskId, @Nullable Bundle sideOptions,
                 @SplitPosition int sidePosition,
-                @Nullable IRemoteTransition remoteTransition) {
+                @Nullable RemoteTransition remoteTransition) {
             executeRemoteCallWithTaskPermission(mController, "startTasks",
                     (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
                             sideTaskId, sideOptions, sidePosition, remoteTransition));
@@ -444,5 +592,15 @@
                         controller.startIntent(intent, fillInIntent, stage, position, options);
                     });
         }
+
+        @Override
+        public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
+                RemoteAnimationTarget[] apps) {
+            final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null};
+            executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy",
+                    (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps),
+                    true /* blocking */);
+            return out[0];
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index c37789e..86e7b0e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -35,7 +35,7 @@
 import android.os.IBinder;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
@@ -84,17 +84,19 @@
     }
 
     void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback,
             @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) {
         mFinishCallback = finishCallback;
         mAnimatingTransition = transition;
         if (mRemoteHandler != null) {
-            mRemoteHandler.startAnimation(transition, info, t, mRemoteFinishCB);
+            mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction,
+                    mRemoteFinishCB);
             mRemoteHandler = null;
             return;
         }
-        playInternalAnimation(transition, info, t, mainRoot, sideRoot);
+        playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot);
     }
 
     private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@@ -167,7 +169,7 @@
 
     /** Starts a transition to enter split with a remote transition animator. */
     IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
-            @NonNull WindowContainerTransaction wct, @Nullable IRemoteTransition remoteTransition,
+            @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition,
             @NonNull Transitions.TransitionHandler handler) {
         if (remoteTransition != null) {
             // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
new file mode 100644
index 0000000..319079b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+
+/**
+ * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
+ */
+public class SplitscreenEventLogger {
+
+    // Used to generate instance ids for this drag if one is not provided
+    private final InstanceIdSequence mIdSequence;
+
+    // The instance id for the current splitscreen session (from start to end)
+    private InstanceId mLoggerSessionId;
+
+    // Drag info
+    private @SplitPosition int mDragEnterPosition;
+    private InstanceId mDragEnterSessionId;
+
+    // For deduping async events
+    private int mLastMainStagePosition = -1;
+    private int mLastMainStageUid = -1;
+    private int mLastSideStagePosition = -1;
+    private int mLastSideStageUid = -1;
+    private float mLastSplitRatio = -1f;
+
+    public SplitscreenEventLogger() {
+        mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
+    }
+
+    /**
+     * Return whether a splitscreen session has started.
+     */
+    public boolean hasStartedSession() {
+        return mLoggerSessionId != null;
+    }
+
+    /**
+     * May be called before logEnter() to indicate that the session was started from a drag.
+     */
+    public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) {
+        mDragEnterPosition = position;
+        mDragEnterSessionId = dragSessionId;
+    }
+
+    /**
+     * Logs when the user enters splitscreen.
+     */
+    public void logEnter(float splitRatio,
+            @SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid,
+            boolean isLandscape) {
+        mLoggerSessionId = mIdSequence.newInstanceId();
+        int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED
+                ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape)
+                : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+        updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid);
+        updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid);
+        updateSplitRatioState(splitRatio);
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER,
+                enterReason,
+                0 /* exitReason */,
+                splitRatio,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the user exits splitscreen.  Only one of the main or side stages should be
+     * specified to indicate which position was focused as a part of exiting (both can be unset).
+     */
+    public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if ((mainStagePosition != SPLIT_POSITION_UNDEFINED
+                && sideStagePosition != SPLIT_POSITION_UNDEFINED)
+                        || (mainStageUid != 0 && sideStageUid != 0)) {
+            throw new IllegalArgumentException("Only main or side stage should be set");
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT,
+                0 /* enterReason */,
+                exitReason,
+                0f /* splitRatio */,
+                getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid,
+                getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+
+        // Reset states
+        mLoggerSessionId = null;
+        mDragEnterPosition = SPLIT_POSITION_UNDEFINED;
+        mDragEnterSessionId = null;
+        mLastMainStagePosition = -1;
+        mLastMainStageUid = -1;
+        mLastSideStagePosition = -1;
+        mLastSideStageUid = -1;
+    }
+
+    /**
+     * Logs when an app in the main stage changes.
+     */
+    public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid,
+            boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition,
+                isLandscape), mainStageUid)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                0 /* sideStagePosition */,
+                0 /* sideStageUid */,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when an app in the side stage changes.
+     */
+    public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid,
+            boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition,
+                isLandscape), sideStageUid)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                0 /* mainStagePosition */,
+                0 /* mainStageUid */,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the splitscreen ratio changes.
+     */
+    public void logResize(float splitRatio) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (splitRatio <= 0f || splitRatio >= 1f) {
+            // Don't bother reporting resizes that end up dismissing the split, that will be logged
+            // via the exit event
+            return;
+        }
+        if (!updateSplitRatioState(splitRatio)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                mLastSplitRatio,
+                0 /* mainStagePosition */, 0 /* mainStageUid */,
+                0 /* sideStagePosition */, 0 /* sideStageUid */,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the apps in splitscreen are swapped.
+     */
+    public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+
+        updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid);
+        updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid);
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    private boolean updateMainStageState(int mainStagePosition, int mainStageUid) {
+        boolean changed = (mLastMainStagePosition != mainStagePosition)
+                || (mLastMainStageUid != mainStageUid);
+        if (!changed) {
+            return false;
+        }
+
+        mLastMainStagePosition = mainStagePosition;
+        mLastMainStageUid = mainStageUid;
+        return true;
+    }
+
+    private boolean updateSideStageState(int sideStagePosition, int sideStageUid) {
+        boolean changed = (mLastSideStagePosition != sideStagePosition)
+                || (mLastSideStageUid != sideStageUid);
+        if (!changed) {
+            return false;
+        }
+
+        mLastSideStagePosition = sideStagePosition;
+        mLastSideStageUid = sideStageUid;
+        return true;
+    }
+
+    private boolean updateSplitRatioState(float splitRatio) {
+        boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0;
+        if (!changed) {
+            return false;
+        }
+
+        mLastSplitRatio = splitRatio;
+        return true;
+    }
+
+    public int getDragEnterReasonFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM;
+        }
+    }
+
+    private int getMainStagePositionFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (position == SPLIT_POSITION_UNDEFINED) {
+            return 0;
+        }
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM;
+        }
+    }
+
+    private int getSideStagePositionFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (position == SPLIT_POSITION_UNDEFINED) {
+            return 0;
+        }
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 0264c5a..0cff18e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -18,13 +18,19 @@
 
 import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.transitTypeToString;
+import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
 
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
@@ -35,6 +41,7 @@
 import static com.android.wm.shell.splitscreen.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
 import static com.android.wm.shell.transition.Transitions.isClosingType;
 import static com.android.wm.shell.transition.Transitions.isOpeningType;
@@ -42,16 +49,28 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.app.WindowConfiguration;
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.Rect;
+import android.hardware.devicestate.DeviceStateManager;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.util.Log;
+import android.util.Slog;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.WindowManager;
 import android.window.DisplayAreaInfo;
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerToken;
@@ -59,20 +78,26 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+import com.android.wm.shell.common.split.SplitWindowManager;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Provider;
 
 /**
  * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
@@ -99,8 +124,10 @@
 
     private final MainStage mMainStage;
     private final StageListenerImpl mMainStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mMainUnfoldController;
     private final SideStage mSideStage;
     private final StageListenerImpl mSideStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mSideUnfoldController;
     @SplitPosition
     private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
 
@@ -114,14 +141,20 @@
     private final Context mContext;
     private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
     private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
     private final SplitScreenTransitions mSplitTransitions;
-    private boolean mExitSplitScreenOnHide = true;
+    private final SplitscreenEventLogger mLogger;
+    private boolean mExitSplitScreenOnHide;
+    private boolean mKeyguardOccluded;
 
     // TODO(b/187041611): remove this flag after totally deprecated legacy split
     /** Whether the device is supporting legacy split or not. */
     private boolean mUseLegacySplit;
 
-    @SplitScreen.StageType int mDismissTop = NO_DISMISS;
+    @SplitScreen.StageType private int mDismissTop = NO_DISMISS;
+
+    /** The target stage to dismiss to when unlock after folded. */
+    @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
 
     private final Runnable mOnTransitionAnimationComplete = () -> {
         // If still playing, let it finish.
@@ -134,29 +167,57 @@
         mDismissTop = NO_DISMISS;
     };
 
+    private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
+            new SplitWindowManager.ParentContainerCallbacks() {
+        @Override
+        public void attachToParentSurface(SurfaceControl.Builder b) {
+            mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b);
+        }
+
+        @Override
+        public void onLeashReady(SurfaceControl leash) {
+            mSyncQueue.runInSync(t -> applyDividerVisibility(t));
+        }
+    };
+
     StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
-            DisplayImeController displayImeController, Transitions transitions,
-            TransactionPool transactionPool) {
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController, Transitions transitions,
+            TransactionPool transactionPool, SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
         mRootTDAOrganizer = rootTDAOrganizer;
         mTaskOrganizer = taskOrganizer;
+        mLogger = logger;
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+
         mMainStage = new MainStage(
                 mTaskOrganizer,
                 mDisplayId,
                 mMainStageListener,
                 mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession,
+                mMainUnfoldController);
         mSideStage = new SideStage(
+                mContext,
                 mTaskOrganizer,
                 mDisplayId,
                 mSideStageListener,
                 mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession,
+                mSideUnfoldController);
         mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage);
         mRootTDAOrganizer.registerListener(displayId, this);
+        final DeviceStateManager deviceStateManager =
+                mContext.getSystemService(DeviceStateManager.class);
+        deviceStateManager.registerCallback(taskOrganizer.getExecutor(),
+                new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged));
         mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
                 mOnTransitionAnimationComplete);
         transitions.addHandler(this);
@@ -166,7 +227,10 @@
     StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
             MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
-            SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) {
+            DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
+            Transitions transitions, TransactionPool transactionPool,
+            SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -175,10 +239,14 @@
         mMainStage = mainStage;
         mSideStage = sideStage;
         mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
         mRootTDAOrganizer.registerListener(displayId, this);
         mSplitLayout = splitLayout;
         mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
                 mOnTransitionAnimationComplete);
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mLogger = logger;
         transitions.addHandler(this);
     }
 
@@ -194,10 +262,11 @@
     boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
             @SplitPosition int sideStagePosition) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        setSideStagePosition(sideStagePosition);
-        mMainStage.activate(getMainStageBounds(), wct);
+        setSideStagePosition(sideStagePosition, wct);
+        mMainStage.activate(getMainStageBounds(), wct, true /* reparent */);
         mSideStage.addTask(task, getSideStageBounds(), wct);
-        mTaskOrganizer.applyTransaction(wct);
+        mSyncQueue.queue(wct);
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t));
         return true;
     }
 
@@ -215,18 +284,22 @@
         return result;
     }
 
+    void setSideStageOutline(boolean enable) {
+        mSideStage.enableOutline(enable);
+    }
+
     /** Starts 2 tasks in one transition. */
     void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
             @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
-            @Nullable IRemoteTransition remoteTransition) {
+            @Nullable RemoteTransition remoteTransition) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         mainOptions = mainOptions != null ? mainOptions : new Bundle();
         sideOptions = sideOptions != null ? sideOptions : new Bundle();
-        setSideStagePosition(sidePosition);
+        setSideStagePosition(sidePosition, wct);
 
         // Build a request WCT that will launch both apps such that task 0 is on the main stage
         // while task 1 is on the side stage.
-        mMainStage.activate(getMainStageBounds(), wct);
+        mMainStage.activate(getMainStageBounds(), wct, false /* reparent */);
         mSideStage.setBounds(getSideStageBounds(), wct);
 
         // Make sure the launch options will put tasks in the corresponding split roots
@@ -241,6 +314,144 @@
                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
     }
 
+    /** Starts 2 tasks in one legacy transition. */
+    void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
+            int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+            RemoteAnimationAdapter adapter) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        // Need to add another wrapper here in shell so that we can inject the divider bar
+        // and also manage the process elevation via setRunningRemote
+        IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+            @Override
+            public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+                    RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers,
+                    RemoteAnimationTarget[] nonApps,
+                    final IRemoteAnimationFinishedCallback finishedCallback) {
+                RemoteAnimationTarget[] augmentedNonApps =
+                        new RemoteAnimationTarget[nonApps.length + 1];
+                for (int i = 0; i < nonApps.length; ++i) {
+                    augmentedNonApps[i] = nonApps[i];
+                }
+                augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget();
+                try {
+                    ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
+                            adapter.getCallingApplication());
+                    adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps,
+                            finishedCallback);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+            }
+
+            @Override
+            public void onAnimationCancelled() {
+                try {
+                    adapter.getRunner().onAnimationCancelled();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+            }
+        };
+        RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
+                wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
+
+        if (mainOptions == null) {
+            mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle();
+        } else {
+            ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions);
+            mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
+        }
+
+        sideOptions = sideOptions != null ? sideOptions : new Bundle();
+        setSideStagePosition(sidePosition, wct);
+
+        // Build a request WCT that will launch both apps such that task 0 is on the main stage
+        // while task 1 is on the side stage.
+        mMainStage.activate(getMainStageBounds(), wct, false /* reparent */);
+        mSideStage.setBounds(getSideStageBounds(), wct);
+
+        // Make sure the launch options will put tasks in the corresponding split roots
+        addActivityOptions(mainOptions, mMainStage);
+        addActivityOptions(sideOptions, mSideStage);
+
+        // Add task launch requests
+        wct.startTask(mainTaskId, mainOptions);
+        wct.startTask(sideTaskId, sideOptions);
+
+        // Using legacy transitions, so we can't use blast sync since it conflicts.
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    public void startIntent(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @androidx.annotation.Nullable Bundle options,
+            @Nullable RemoteTransition remoteTransition) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        options = resolveStartStage(stage, position, options, wct);
+        wct.sendPendingIntent(intent, fillInIntent, options);
+        mSplitTransitions.startEnterTransition(
+                TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this);
+    }
+
+    void evictOccludedChildren(@SplitPosition int position) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        (position == mSideStagePosition ? mSideStage : mMainStage).evictOccludedChildren(wct);
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    Bundle resolveStartStage(@SplitScreen.StageType int stage,
+            @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
+            @androidx.annotation.Nullable WindowContainerTransaction wct) {
+        switch (stage) {
+            case STAGE_TYPE_UNDEFINED: {
+                // Use the stage of the specified position is valid.
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    if (position == getSideStagePosition()) {
+                        options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct);
+                    } else {
+                        options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct);
+                    }
+                } else {
+                    // Exit split-screen and launch fullscreen since stage wasn't specified.
+                    prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
+                }
+                break;
+            }
+            case STAGE_TYPE_SIDE: {
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    setSideStagePosition(position, wct);
+                } else {
+                    position = getSideStagePosition();
+                }
+                if (options == null) {
+                    options = new Bundle();
+                }
+                updateActivityOptions(options, position);
+                break;
+            }
+            case STAGE_TYPE_MAIN: {
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    // Set the side stage opposite of what we want to the main stage.
+                    final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT
+                            ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
+                    setSideStagePosition(sideStagePosition, wct);
+                } else {
+                    position = getMainStagePosition();
+                }
+                if (options == null) {
+                    options = new Bundle();
+                }
+                updateActivityOptions(options, position);
+                break;
+            }
+            default:
+                throw new IllegalArgumentException("Unknown stage=" + stage);
+        }
+
+        return options;
+    }
+
     @SplitLayout.SplitPosition
     int getSideStagePosition() {
         return mSideStagePosition;
@@ -252,18 +463,25 @@
                 ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
     }
 
-    void setSideStagePosition(@SplitPosition int sideStagePosition) {
-        setSideStagePosition(sideStagePosition, true /* updateVisibility */);
+    void setSideStagePosition(@SplitPosition int sideStagePosition,
+            @Nullable WindowContainerTransaction wct) {
+        setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
     }
 
-    private void setSideStagePosition(@SplitPosition int sideStagePosition,
-            boolean updateVisibility) {
+    private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds,
+            @Nullable WindowContainerTransaction wct) {
         if (mSideStagePosition == sideStagePosition) return;
         mSideStagePosition = sideStagePosition;
         sendOnStagePositionChanged();
 
-        if (mSideStageListener.mVisible && updateVisibility) {
-            onStageVisibilityChanged(mSideStageListener);
+        if (mSideStageListener.mVisible && updateBounds) {
+            if (wct == null) {
+                // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds.
+                onLayoutSizeChanged(mSplitLayout);
+            } else {
+                updateWindowBounds(mSplitLayout, wct);
+                updateUnfoldBounds();
+            }
         }
     }
 
@@ -275,24 +493,69 @@
         mTaskOrganizer.applyTransaction(wct);
     }
 
-    void exitSplitScreen() {
-        exitSplitScreen(null /* childrenToTop */);
+    void onKeyguardOccludedChanged(boolean occluded) {
+        // Do not exit split directly, because it needs to wait for task info update to determine
+        // which task should remain on top after split dismissed.
+        mKeyguardOccluded = occluded;
+    }
+
+    void onKeyguardVisibilityChanged(boolean showing) {
+        if (!showing && mMainStage.isActive()
+                && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
+            exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
+                    SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED);
+        }
     }
 
     void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
         mExitSplitScreenOnHide = exitSplitScreenOnHide;
     }
 
-    private void exitSplitScreen(StageTaskListener childrenToTop) {
+    void exitSplitScreen(int toTopTaskId, int exitReason) {
+        StageTaskListener childrenToTop = null;
+        if (mMainStage.containsTask(toTopTaskId)) {
+            childrenToTop = mMainStage;
+        } else if (mSideStage.containsTask(toTopTaskId)) {
+            childrenToTop = mSideStage;
+        }
+
         final WindowContainerTransaction wct = new WindowContainerTransaction();
+        if (childrenToTop != null) {
+            childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct);
+        }
+        applyExitSplitScreen(childrenToTop, wct, exitReason);
+    }
+
+    private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        applyExitSplitScreen(childrenToTop, wct, exitReason);
+    }
+
+    private void applyExitSplitScreen(StageTaskListener childrenToTop,
+            WindowContainerTransaction wct, int exitReason) {
         mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
         mMainStage.deactivate(wct, childrenToTop == mMainStage);
         mTaskOrganizer.applyTransaction(wct);
-        // Reset divider position.
+        mSyncQueue.runInSync(t -> t
+                .setWindowCrop(mMainStage.mRootLeash, null)
+                .setWindowCrop(mSideStage.mRootLeash, null));
+        // Hide divider and reset its position.
+        setDividerVisibility(false);
         mSplitLayout.resetDividerPosition();
+        mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+        if (childrenToTop != null) {
+            logExitToStage(exitReason, childrenToTop == mMainStage);
+        } else {
+            logExit(exitReason);
+        }
     }
 
-    private void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop,
+    /**
+     * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates
+     * an existing WindowContainerTransaction (rather than applying immediately). This is intended
+     * to be used when exiting split might be bundled with other window operations.
+     */
+    void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop,
             @NonNull WindowContainerTransaction wct) {
         mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
         mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN);
@@ -309,29 +572,26 @@
 
     void updateActivityOptions(Bundle opts, @SplitPosition int position) {
         addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage);
-
-        if (!mMainStage.isActive()) {
-            // Activate the main stage in anticipation of an app launch.
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
-            mMainStage.activate(getMainStageBounds(), wct);
-            mSideStage.setBounds(getSideStageBounds(), wct);
-            mTaskOrganizer.applyTransaction(wct);
-        }
     }
 
     void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
         if (mListeners.contains(listener)) return;
         mListeners.add(listener);
-        listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
-        listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
-        mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
-        mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
+        sendStatusToListener(listener);
     }
 
     void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
         mListeners.remove(listener);
     }
 
+    void sendStatusToListener(SplitScreen.SplitScreenListener listener) {
+        listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
+        listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
+        listener.onSplitVisibilityChanged(isSplitScreenVisible());
+        mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
+        mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
+    }
+
     private void sendOnStagePositionChanged() {
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             final SplitScreen.SplitScreenListener l = mListeners.get(i);
@@ -340,9 +600,8 @@
         }
     }
 
-    private void onStageChildTaskStatusChanged(
-            StageListenerImpl stageListener, int taskId, boolean present, boolean visible) {
-
+    private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
+            boolean present, boolean visible) {
         int stage;
         if (present) {
             stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
@@ -350,12 +609,31 @@
             // No longer on any stage
             stage = STAGE_TYPE_UNDEFINED;
         }
+        if (stage == STAGE_TYPE_MAIN) {
+            mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        } else {
+            mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        }
 
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             mListeners.get(i).onTaskStageChanged(taskId, stage, visible);
         }
     }
 
+    private void sendSplitVisibilityChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            final SplitScreen.SplitScreenListener l = mListeners.get(i);
+            l.onSplitVisibilityChanged(mDividerVisible);
+        }
+
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+            mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+        }
+    }
+
     private void onStageRootTaskAppeared(StageListenerImpl stageListener) {
         if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) {
             mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit);
@@ -392,107 +670,109 @@
         mDividerVisible = visible;
         if (visible) {
             mSplitLayout.init();
+            updateUnfoldBounds();
         } else {
             mSplitLayout.release();
         }
+        sendSplitVisibilityChanged();
     }
 
     private void onStageVisibilityChanged(StageListenerImpl stageListener) {
         final boolean sideStageVisible = mSideStageListener.mVisible;
         final boolean mainStageVisible = mMainStageListener.mVisible;
-        // Divider is only visible if both the main stage and side stages are visible
-        setDividerVisibility(isSplitScreenVisible());
-
-        if (mExitSplitScreenOnHide && !mainStageVisible && !sideStageVisible) {
-            // Exit split-screen if both stage are not visible.
-            // TODO: This is only a temporary request from UX and is likely to be removed soon...
-            exitSplitScreen();
+        final boolean bothStageVisible = sideStageVisible && mainStageVisible;
+        final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible;
+        final boolean sameVisibility = sideStageVisible == mainStageVisible;
+        // Only add or remove divider when both visible or both invisible to avoid sometimes we only
+        // got one stage visibility changed for a moment and it will cause flicker.
+        if (sameVisibility) {
+            setDividerVisibility(bothStageVisible);
         }
 
-        if (mainStageVisible) {
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
-            if (sideStageVisible) {
-                // The main stage configuration should to follow split layout when side stage is
-                // visible.
-                mMainStage.updateConfiguration(
-                        WINDOWING_MODE_MULTI_WINDOW, getMainStageBounds(), wct);
-            } else {
-                // We want the main stage configuration to be fullscreen when the side stage isn't
-                // visible.
-                mMainStage.updateConfiguration(WINDOWING_MODE_FULLSCREEN, null, wct);
+        if (bothStageInvisible) {
+            if (mExitSplitScreenOnHide
+            // Don't dismiss staged split when both stages are not visible due to sleeping display,
+            // like the cases keyguard showing or screen off.
+            || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) {
+                exitSplitScreen(null /* childrenToTop */,
+                        SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
             }
-            // TODO: Change to `mSyncQueue.queue(wct)` once BLAST is stable.
-            mTaskOrganizer.applyTransaction(wct);
+        } else if (mKeyguardOccluded) {
+            // At least one of the stages is visible while keyguard occluded. Dismiss split because
+            // there's show-when-locked activity showing on top of keyguard. Also make sure the
+            // task contains show-when-locked activity remains on top after split dismissed.
+            final StageTaskListener toTop =
+                    mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null);
+            exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP);
         }
 
         mSyncQueue.runInSync(t -> {
-            final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
-            final SurfaceControl sideStageLeash = mSideStage.mRootLeash;
-            final SurfaceControl mainStageLeash = mMainStage.mRootLeash;
-
-            if (dividerLeash != null) {
-                if (mDividerVisible) {
-                    t.show(dividerLeash)
-                            .setLayer(dividerLeash, Integer.MAX_VALUE)
-                            .setPosition(dividerLeash,
-                                    mSplitLayout.getDividerBounds().left,
-                                    mSplitLayout.getDividerBounds().top);
-                } else {
-                    t.hide(dividerLeash);
-                }
-            }
-
-            if (sideStageVisible) {
-                final Rect sideStageBounds = getSideStageBounds();
-                t.show(sideStageLeash)
-                        .setPosition(sideStageLeash,
-                                sideStageBounds.left, sideStageBounds.top)
-                        .setWindowCrop(sideStageLeash,
-                                sideStageBounds.width(), sideStageBounds.height());
-            } else {
-                t.hide(sideStageLeash);
-            }
-
-            if (mainStageVisible) {
-                final Rect mainStageBounds = getMainStageBounds();
-                t.show(mainStageLeash);
-                if (sideStageVisible) {
-                    t.setPosition(mainStageLeash, mainStageBounds.left, mainStageBounds.top)
-                            .setWindowCrop(mainStageLeash,
-                                    mainStageBounds.width(), mainStageBounds.height());
-                } else {
-                    // Clear window crop and position if side stage isn't visible.
-                    t.setPosition(mainStageLeash, 0, 0)
-                            .setWindowCrop(mainStageLeash, null);
-                }
-            } else {
-                t.hide(mainStageLeash);
+            // Same above, we only set root tasks and divider leash visibility when both stage
+            // change to visible or invisible to avoid flicker.
+            if (sameVisibility) {
+                t.setVisibility(mSideStage.mRootLeash, bothStageVisible)
+                        .setVisibility(mMainStage.mRootLeash, bothStageVisible);
+                applyDividerVisibility(t);
+                applyOutlineVisibility(t);
             }
         });
     }
 
+    private void applyDividerVisibility(SurfaceControl.Transaction t) {
+        final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+        if (dividerLeash == null) {
+            return;
+        }
+
+        if (mDividerVisible) {
+            t.show(dividerLeash)
+                    .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER)
+                    .setPosition(dividerLeash,
+                            mSplitLayout.getDividerBounds().left,
+                            mSplitLayout.getDividerBounds().top);
+        } else {
+            t.hide(dividerLeash);
+        }
+    }
+
+    private void applyOutlineVisibility(SurfaceControl.Transaction t) {
+        final SurfaceControl outlineLeash = mSideStage.getOutlineLeash();
+        if (outlineLeash == null) {
+            return;
+        }
+
+        if (mDividerVisible) {
+            t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER);
+        } else {
+            t.hide(outlineLeash);
+        }
+    }
+
     private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
         final boolean hasChildren = stageListener.mHasChildren;
         final boolean isSideStage = stageListener == mSideStageListener;
         if (!hasChildren) {
             if (isSideStage && mMainStageListener.mVisible) {
                 // Exit to main stage if side stage no longer has children.
-                exitSplitScreen(mMainStage);
+                exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
             } else if (!isSideStage && mSideStageListener.mVisible) {
                 // Exit to side stage if main stage no longer has children.
-                exitSplitScreen(mSideStage);
+                exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
             }
         } else if (isSideStage) {
             final WindowContainerTransaction wct = new WindowContainerTransaction();
             // Make sure the main stage is active.
-            mMainStage.activate(getMainStageBounds(), wct);
+            mMainStage.activate(getMainStageBounds(), wct, true /* reparent */);
             mSideStage.setBounds(getSideStageBounds(), wct);
-            // Reorder side stage to the top whenever there's a new child task appeared in side
-            // stage. This is needed to prevent main stage occludes side stage and makes main stage
-            // flipping between fullscreen and multi-window windowing mode.
-            wct.reorder(mSideStage.mRootTaskInfo.token, true);
             mTaskOrganizer.applyTransaction(wct);
         }
+        if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren
+                && mSideStageListener.mHasChildren) {
+            mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
+                    getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                    getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        }
     }
 
     @VisibleForTesting
@@ -511,38 +791,67 @@
             onSnappedToDismissTransition(mainStageToTop);
             return;
         }
-        exitSplitScreen(mainStageToTop ? mMainStage : mSideStage);
+        exitSplitScreen(mainStageToTop ? mMainStage : mSideStage,
+                SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER);
     }
 
     @Override
     public void onDoubleTappedDivider() {
         setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
-                ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT);
+                ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */);
+        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                mSplitLayout.isLandscape());
     }
 
     @Override
-    public void onBoundsChanging(SplitLayout layout) {
-        final StageTaskListener topLeftStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
-        final StageTaskListener bottomRightStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
-
-        mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash,
-                bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer));
+    public void onLayoutPositionChanging(SplitLayout layout) {
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
     }
 
     @Override
-    public void onBoundsChanged(SplitLayout layout) {
-        final StageTaskListener topLeftStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
-        final StageTaskListener bottomRightStage =
-                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+    public void onLayoutSizeChanging(SplitLayout layout) {
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+        mSideStage.setOutlineVisibility(false);
+    }
 
+    @Override
+    public void onLayoutSizeChanged(SplitLayout layout) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
+        updateWindowBounds(layout, wct);
+        updateUnfoldBounds();
         mSyncQueue.queue(wct);
-        mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash,
-                bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer));
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+        mSideStage.setOutlineVisibility(true);
+        mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
+    }
+
+    private void updateUnfoldBounds() {
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onLayoutChanged(getMainStageBounds());
+            mSideUnfoldController.onLayoutChanged(getSideStageBounds());
+        }
+    }
+
+    /**
+     * Populates `wct` with operations that match the split windows to the current layout.
+     * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
+     */
+    private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
+    }
+
+    void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash,
+                bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer);
     }
 
     @Override
@@ -561,13 +870,30 @@
     }
 
     @Override
+    public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo,
+                bottomRightStage.mRootTaskInfo);
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    @Override
     public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) {
         mDisplayAreaInfo = displayAreaInfo;
         if (mSplitLayout == null) {
             mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
-                    mDisplayAreaInfo.configuration, this,
-                    b -> mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b),
+                    mDisplayAreaInfo.configuration, this, mParentContainerCallbacks,
                     mDisplayImeController, mTaskOrganizer);
+            mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
+
+            if (mMainUnfoldController != null && mSideUnfoldController != null) {
+                mMainUnfoldController.init();
+                mSideUnfoldController.init();
+            }
         }
     }
 
@@ -580,8 +906,20 @@
     public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
         mDisplayAreaInfo = displayAreaInfo;
         if (mSplitLayout != null
-                && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)) {
-            onBoundsChanged(mSplitLayout);
+                && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)
+                && mMainStage.isActive()) {
+            onLayoutSizeChanged(mSplitLayout);
+        }
+    }
+
+    private void onFoldedStateChanged(boolean folded) {
+        mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+        if (!folded) return;
+
+        if (mMainStage.isFocused()) {
+            mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN;
+        } else if (mSideStage.isFocused()) {
+            mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE;
         }
     }
 
@@ -672,7 +1010,8 @@
     @Override
     public boolean startAnimation(@NonNull IBinder transition,
             @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         if (transition != mSplitTransitions.mPendingDismiss
                 && transition != mSplitTransitions.mPendingEnter) {
@@ -717,14 +1056,14 @@
 
         boolean shouldAnimate = true;
         if (mSplitTransitions.mPendingEnter == transition) {
-            shouldAnimate = startPendingEnterAnimation(transition, info, t);
+            shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
         } else if (mSplitTransitions.mPendingDismiss == transition) {
-            shouldAnimate = startPendingDismissAnimation(transition, info, t);
+            shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction);
         }
         if (!shouldAnimate) return false;
 
-        mSplitTransitions.playAnimation(transition, info, t, finishCallback,
-                mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+        mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
+                finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
         return true;
     }
 
@@ -754,7 +1093,8 @@
 
             // Update local states (before animating).
             setDividerVisibility(true);
-            setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateVisibility */);
+            setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */,
+                    null /* wct */);
             setSplitsVisible(true);
 
             addDividerBarToTransition(info, t, true /* show */);
@@ -854,12 +1194,34 @@
         // Be default, make it visible. The remote animator can adjust alpha if it plans to animate.
         if (show) {
             t.setAlpha(leash, 1.f);
-            t.setLayer(leash, Integer.MAX_VALUE);
+            t.setLayer(leash, SPLIT_DIVIDER_LAYER);
             t.setPosition(leash, bounds.left, bounds.top);
             t.show(leash);
         }
     }
 
+    RemoteAnimationTarget getDividerBarLegacyTarget() {
+        final Rect bounds = mSplitLayout.getDividerBounds();
+        return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
+                mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */,
+                null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+                new android.graphics.Point(0, 0) /* position */, bounds, bounds,
+                new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
+                null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
+    }
+
+    RemoteAnimationTarget getOutlineLegacyTarget() {
+        final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds();
+        // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to
+        // distinguish as a split auxiliary target in Launcher.
+        return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
+                mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */,
+                null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+                new android.graphics.Point(0, 0) /* position */, bounds, bounds,
+                new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
+                null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
+    }
+
     @Override
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
@@ -884,6 +1246,36 @@
         mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible;
     }
 
+    /**
+     * Sets drag info to be logged when splitscreen is next entered.
+     */
+    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        mLogger.enterRequestedByDrag(position, dragSessionId);
+    }
+
+    /**
+     * Logs the exit of splitscreen.
+     */
+    private void logExit(int exitReason) {
+        mLogger.logExit(exitReason,
+                SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */,
+                SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */,
+                mSplitLayout.isLandscape());
+    }
+
+    /**
+     * Logs the exit of splitscreen to a specific stage. This must be called before the exit is
+     * executed.
+     */
+    private void logExitToStage(int exitReason, boolean toMainStage) {
+        mLogger.logExit(exitReason,
+                toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED,
+                toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */,
+                !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED,
+                !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */,
+                mSplitLayout.isLandscape());
+    }
+
     class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
         boolean mHasRootTask = false;
         boolean mVisible = false;
@@ -923,7 +1315,8 @@
         @Override
         public void onNoLongerSupportMultiWindow() {
             if (mMainStage.isActive()) {
-                StageCoordinator.this.exitSplitScreen();
+                StageCoordinator.this.exitSplitScreen(null /* childrenToTop */,
+                        SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW);
             }
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 0fd8eca..071badf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -24,6 +24,7 @@
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
 import android.annotation.CallSuper;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -67,12 +68,13 @@
         void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
 
         void onRootTaskVanished();
+
         void onNoLongerSupportMultiWindow();
     }
 
     private final StageListenerCallbacks mCallbacks;
-    private final SyncTransactionQueue mSyncQueue;
     private final SurfaceSession mSurfaceSession;
+    protected final SyncTransactionQueue mSyncQueue;
 
     protected ActivityManager.RunningTaskInfo mRootTaskInfo;
     protected SurfaceControl mRootLeash;
@@ -80,12 +82,16 @@
     protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();
     private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>();
 
+    private final StageTaskUnfoldController mStageTaskUnfoldController;
+
     StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession) {
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
         mCallbacks = callbacks;
         mSyncQueue = syncQueue;
         mSurfaceSession = surfaceSession;
+        mStageTaskUnfoldController = stageTaskUnfoldController;
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
     }
 
@@ -97,6 +103,39 @@
         return mChildrenTaskInfo.contains(taskId);
     }
 
+    /**
+     * Returns the top activity uid for the top child task.
+     */
+    int getTopChildTaskUid() {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
+            if (info.topActivityInfo == null) {
+                continue;
+            }
+            return info.topActivityInfo.applicationInfo.uid;
+        }
+        return 0;
+    }
+
+    /** @return {@code true} if this listener contains the currently focused task. */
+    boolean isFocused() {
+        if (mRootTaskInfo == null) {
+            return false;
+        }
+
+        if (mRootTaskInfo.isFocused) {
+            return true;
+        }
+
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            if (mChildrenTaskInfo.valueAt(i).isFocused) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     @Override
     @CallSuper
     public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
@@ -105,8 +144,11 @@
             mRootTaskInfo = taskInfo;
             mCallbacks.onRootTaskAppeared();
             sendStatusChanged();
-            mSyncQueue.runInSync(t -> mDimLayer =
-                    SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession));
+            mSyncQueue.runInSync(t -> {
+                t.hide(mRootLeash);
+                mDimLayer =
+                        SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession);
+            });
         } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
             final int taskId = taskInfo.taskId;
             mChildrenLeashes.put(taskId, leash);
@@ -122,6 +164,10 @@
             throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
                     + "\n mRootTaskInfo: " + mRootTaskInfo);
         }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
+        }
     }
 
     @Override
@@ -174,6 +220,10 @@
             throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
                     + "\n mRootTaskInfo: " + mRootTaskInfo);
         }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskVanished(taskInfo);
+        }
     }
 
     @Override
@@ -191,6 +241,22 @@
         wct.setBounds(mRootTaskInfo.token, bounds);
     }
 
+    void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) {
+        if (!containsTask(taskId)) {
+            return;
+        }
+        wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */);
+    }
+
+    void evictOccludedChildren(WindowContainerTransaction wct) {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) {
+            final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
+            if (!taskInfo.isVisible) {
+                wct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
+            }
+        }
+    }
+
     void setVisibility(boolean visible, WindowContainerTransaction wct) {
         wct.reorder(mRootTaskInfo.token, visible /* onTop */);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
new file mode 100644
index 0000000..e904f6a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.animation.RectEvaluator;
+import android.animation.TypeEvaluator;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controls transformations of the split screen task surfaces in response
+ * to the unfolding/folding action on foldable devices
+ */
+public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
+
+    private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
+
+    private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+    private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final UnfoldBackgroundController mBackgroundController;
+    private final Executor mExecutor;
+    private final int mExpandedTaskBarHeight;
+    private final float mWindowCornerRadiusPx;
+    private final Rect mStageBounds = new Rect();
+    private final TransactionPool mTransactionPool;
+
+    private InsetsSource mTaskbarInsetsSource;
+    private boolean mBothStagesVisible;
+
+    public StageTaskUnfoldController(@NonNull Context context,
+            @NonNull TransactionPool transactionPool,
+            @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
+            @NonNull DisplayInsetsController displayInsetsController,
+            @NonNull UnfoldBackgroundController backgroundController,
+            @NonNull Executor executor) {
+        mUnfoldProgressProvider = unfoldProgressProvider;
+        mTransactionPool = transactionPool;
+        mExecutor = executor;
+        mBackgroundController = backgroundController;
+        mDisplayInsetsController = displayInsetsController;
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+        mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+    }
+
+    /**
+     * Initializes the controller, starts listening for the external events
+     */
+    public void init() {
+        mUnfoldProgressProvider.addListener(mExecutor, this);
+        mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    /**
+     * Called when split screen task appeared
+     * @param taskInfo info for the appeared task
+     * @param leash surface leash for the appeared task
+     */
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        AnimationContext context = new AnimationContext(leash);
+        mAnimationContextByTaskId.put(taskInfo.taskId, context);
+    }
+
+    /**
+     * Called when a split screen task vanished
+     * @param taskInfo info for the vanished task
+     */
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
+        if (context != null) {
+            final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+            resetSurface(transaction, context);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+        }
+        mAnimationContextByTaskId.remove(taskInfo.taskId);
+    }
+
+    @Override
+    public void onStateChangeProgress(float progress) {
+        if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
+
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        mBackgroundController.ensureBackground(transaction);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+
+            context.mCurrentCropRect.set(RECT_EVALUATOR
+                    .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
+
+            transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
+                    .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
+        }
+
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    @Override
+    public void onStateChangeFinished() {
+        resetTransformations();
+    }
+
+    /**
+     * Called when split screen visibility changes
+     * @param bothStagesVisible true if both stages of the split screen are visible
+     */
+    public void onSplitVisibilityChanged(boolean bothStagesVisible) {
+        mBothStagesVisible = bothStagesVisible;
+        if (!bothStagesVisible) {
+            resetTransformations();
+        }
+    }
+
+    /**
+     * Called when split screen stage bounds changed
+     * @param bounds new bounds for this stage
+     */
+    public void onLayoutChanged(Rect bounds) {
+        mStageBounds.set(bounds);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    private void resetTransformations() {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            resetSurface(transaction, context);
+        }
+        mBackgroundController.removeBackground(transaction);
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
+        transaction
+                .setWindowCrop(context.mLeash, null)
+                .setCornerRadius(context.mLeash, 0.0F);
+    }
+
+    private class AnimationContext {
+        final SurfaceControl mLeash;
+        final Rect mStartCropRect = new Rect();
+        final Rect mEndCropRect = new Rect();
+        final Rect mCurrentCropRect = new Rect();
+
+        private AnimationContext(SurfaceControl leash) {
+            this.mLeash = leash;
+            update();
+        }
+
+        private void update() {
+            mStartCropRect.set(mStageBounds);
+
+            if (mTaskbarInsetsSource != null) {
+                // Only insets the cropping window with taskbar when taskbar is expanded
+                if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+                    mStartCropRect.inset(mTaskbarInsetsSource
+                            .calculateVisibleInsets(mStartCropRect));
+                }
+            }
+
+            // Offset to surface coordinates as layout bounds are in screen coordinates
+            mStartCropRect.offsetTo(0, 0);
+
+            mEndCropRect.set(mStartCropRect);
+
+            int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
+            int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
+            mStartCropRect.inset(margin, margin, margin, margin);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl
new file mode 100644
index 0000000..45f6d3c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.window.RemoteTransition;
+
+import com.android.wm.shell.stagesplit.ISplitScreenListener;
+
+/**
+ * Interface that is exposed to remote callers to manipulate the splitscreen feature.
+ */
+interface ISplitScreen {
+
+    /**
+     * Registers a split screen listener.
+     */
+    oneway void registerSplitScreenListener(in ISplitScreenListener listener) = 1;
+
+    /**
+     * Unregisters a split screen listener.
+     */
+    oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2;
+
+    /**
+     * Hides the side-stage if it is currently visible.
+     */
+    oneway void setSideStageVisibility(boolean visible) = 3;
+
+    /**
+     * Removes a task from the side stage.
+     */
+    oneway void removeFromSideStage(int taskId) = 4;
+
+    /**
+     * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID
+     * to indicate leaving no top task after leaving split-screen.
+     */
+    oneway void exitSplitScreen(int toTopTaskId) = 5;
+
+    /**
+     * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible.
+     */
+    oneway void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) = 6;
+
+    /**
+     * Starts a task in a stage.
+     */
+    oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7;
+
+    /**
+     * Starts a shortcut in a stage.
+     */
+    oneway void startShortcut(String packageName, String shortcutId, int stage, int position,
+            in Bundle options, in UserHandle user) = 8;
+
+    /**
+     * Starts an activity in a stage.
+     */
+    oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage,
+            int position, in Bundle options) = 9;
+
+    /**
+     * Starts tasks simultaneously in one transition.
+     */
+    oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
+            in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10;
+
+    /**
+     * Version of startTasks using legacy transition system.
+     */
+     oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
+                            int sideTaskId, in Bundle sideOptions, int sidePosition,
+                            in RemoteAnimationAdapter adapter) = 11;
+
+    /**
+     * Blocking call that notifies and gets additional split-screen targets when entering
+     * recents (for example: the dividerBar).
+     * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled).
+     * @param appTargets apps that will be re-parented to display area
+     */
+    RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
+                                                   in RemoteAnimationTarget[] appTargets) = 12;
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl
new file mode 100644
index 0000000..46e4299
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+/**
+ * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
+ */
+oneway interface ISplitScreenListener {
+
+    /**
+     * Called when the stage position changes.
+     */
+    void onStagePositionChanged(int stage, int position);
+
+    /**
+     * Called when a task changes stages.
+     */
+    void onTaskStageChanged(int taskId, int stage, boolean visible);
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java
new file mode 100644
index 0000000..83855be
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.view.SurfaceSession;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Main stage for split-screen mode. When split-screen is active all standard activity types launch
+ * on the main stage, except for task that are explicitly pinned to the {@link SideStage}.
+ * @see StageCoordinator
+ */
+class MainStage extends StageTaskListener {
+    private static final String TAG = MainStage.class.getSimpleName();
+
+    private boolean mIsActive = false;
+
+    MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
+            StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
+    }
+
+    boolean isActive() {
+        return mIsActive;
+    }
+
+    void activate(Rect rootBounds, WindowContainerTransaction wct) {
+        if (mIsActive) return;
+
+        final WindowContainerToken rootToken = mRootTaskInfo.token;
+        wct.setBounds(rootToken, rootBounds)
+                .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW)
+                .setLaunchRoot(
+                        rootToken,
+                        CONTROLLED_WINDOWING_MODES,
+                        CONTROLLED_ACTIVITY_TYPES)
+                .reparentTasks(
+                        null /* currentParent */,
+                        rootToken,
+                        CONTROLLED_WINDOWING_MODES,
+                        CONTROLLED_ACTIVITY_TYPES,
+                        true /* onTop */)
+                // Moving the root task to top after the child tasks were re-parented , or the root
+                // task cannot be visible and focused.
+                .reorder(rootToken, true /* onTop */);
+
+        mIsActive = true;
+    }
+
+    void deactivate(WindowContainerTransaction wct) {
+        deactivate(wct, false /* toTop */);
+    }
+
+    void deactivate(WindowContainerTransaction wct, boolean toTop) {
+        if (!mIsActive) return;
+        mIsActive = false;
+
+        if (mRootTaskInfo == null) return;
+        final WindowContainerToken rootToken = mRootTaskInfo.token;
+        wct.setLaunchRoot(
+                        rootToken,
+                        null,
+                        null)
+                .reparentTasks(
+                        rootToken,
+                        null /* newParent */,
+                        CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
+                        CONTROLLED_ACTIVITY_TYPES,
+                        toTop)
+                // We want this re-order to the bottom regardless since we are re-parenting
+                // all its tasks.
+                .reorder(rootToken, false /* onTop */);
+    }
+
+    void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) {
+        wct.setBounds(mRootTaskInfo.token, bounds)
+                .setWindowingMode(mRootTaskInfo.token, windowingMode);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java
new file mode 100644
index 0000000..8fbad52
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.view.IWindow;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.widget.FrameLayout;
+
+import com.android.wm.shell.R;
+
+/**
+ * Handles drawing outline of the bounds of provided root surface. The outline will be drown with
+ * the consideration of display insets like status bar, navigation bar and display cutout.
+ */
+class OutlineManager extends WindowlessWindowManager {
+    private static final String WINDOW_NAME = "SplitOutlineLayer";
+    private final Context mContext;
+    private final Rect mRootBounds = new Rect();
+    private final Rect mTempRect = new Rect();
+    private final Rect mLastOutlineBounds = new Rect();
+    private final InsetsState mInsetsState = new InsetsState();
+    private final int mExpandedTaskBarHeight;
+    private OutlineView mOutlineView;
+    private SurfaceControlViewHost mViewHost;
+    private SurfaceControl mHostLeash;
+    private SurfaceControl mLeash;
+
+    OutlineManager(Context context, Configuration configuration) {
+        super(configuration, null /* rootSurface */, null /* hostInputToken */);
+        mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
+                null /* options */);
+        mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+    }
+
+    @Override
+    protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+        b.setParent(mHostLeash);
+    }
+
+    void inflate(SurfaceControl rootLeash, Rect rootBounds) {
+        if (mLeash != null || mViewHost != null) return;
+
+        mHostLeash = rootLeash;
+        mRootBounds.set(rootBounds);
+        mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+
+        final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext)
+                .inflate(R.layout.split_outline, null);
+        mOutlineView = rootLayout.findViewById(R.id.split_outline);
+
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
+                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
+        lp.width = mRootBounds.width();
+        lp.height = mRootBounds.height();
+        lp.token = new Binder();
+        lp.setTitle(WINDOW_NAME);
+        lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
+        // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
+        //  TRUSTED_OVERLAY for windowless window without input channel.
+        mViewHost.setView(rootLayout, lp);
+        mLeash = getSurfaceControl(mViewHost.getWindowToken());
+
+        drawOutline();
+    }
+
+    void release() {
+        if (mViewHost != null) {
+            mViewHost.release();
+            mViewHost = null;
+        }
+        mRootBounds.setEmpty();
+        mLastOutlineBounds.setEmpty();
+        mOutlineView = null;
+        mHostLeash = null;
+        mLeash = null;
+    }
+
+    @Nullable
+    SurfaceControl getOutlineLeash() {
+        return mLeash;
+    }
+
+    void setVisibility(boolean visible) {
+        if (mOutlineView != null) {
+            mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    void setRootBounds(Rect rootBounds) {
+        if (mViewHost == null || mViewHost.getView() == null) {
+            return;
+        }
+
+        if (!mRootBounds.equals(rootBounds)) {
+            WindowManager.LayoutParams lp =
+                    (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
+            lp.width = rootBounds.width();
+            lp.height = rootBounds.height();
+            mViewHost.relayout(lp);
+            mRootBounds.set(rootBounds);
+            drawOutline();
+        }
+    }
+
+    void onInsetsChanged(InsetsState insetsState) {
+        if (!mInsetsState.equals(insetsState)) {
+            mInsetsState.set(insetsState);
+            drawOutline();
+        }
+    }
+
+    private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) {
+        outBounds.set(rootBounds);
+        final InsetsSource taskBarInsetsSource =
+                insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        // Only insets the divider bar with task bar when it's expanded so that the rounded corners
+        // will be drawn against task bar.
+        if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+            outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds));
+        }
+
+        // Offset the coordinate from screen based to surface based.
+        outBounds.offset(-rootBounds.left, -rootBounds.top);
+    }
+
+    void drawOutline() {
+        if (mOutlineView == null) {
+            return;
+        }
+
+        computeOutlineBounds(mRootBounds, mInsetsState, mTempRect);
+        if (mTempRect.equals(mLastOutlineBounds)) {
+            return;
+        }
+
+        ViewGroup.MarginLayoutParams lp =
+                (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams();
+        lp.leftMargin = mTempRect.left;
+        lp.topMargin = mTempRect.top;
+        lp.width = mTempRect.width();
+        lp.height = mTempRect.height();
+        mOutlineView.setLayoutParams(lp);
+        mLastOutlineBounds.set(mTempRect);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java
new file mode 100644
index 0000000..92b1381
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
+import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
+import static android.view.RoundedCorner.POSITION_TOP_LEFT;
+import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.RoundedCorner;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.R;
+
+/** View for drawing split outline. */
+public class OutlineView extends View {
+    private final Paint mPaint = new Paint();
+    private final Path mPath = new Path();
+    private final float[] mRadii = new float[8];
+
+    public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeWidth(
+                getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
+        mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        // TODO(b/200850654): match the screen corners with the actual display decor.
+        mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT);
+        mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT);
+        mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT);
+        mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT);
+    }
+
+    private int getCornerRadius(@RoundedCorner.Position int position) {
+        final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position);
+        return roundedCorner == null ? 0 : roundedCorner.getRadius();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (changed) {
+            mPath.reset();
+            mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.drawPath(mPath, mPaint);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java
new file mode 100644
index 0000000..55c4f3a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import android.annotation.CallSuper;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up
+ * here. All other task are launch in the {@link MainStage}.
+ *
+ * @see StageCoordinator
+ */
+class SideStage extends StageTaskListener implements
+        DisplayInsetsController.OnInsetsChangedListener {
+    private static final String TAG = SideStage.class.getSimpleName();
+    private final Context mContext;
+    private OutlineManager mOutlineManager;
+
+    SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
+            StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
+        mContext = context;
+    }
+
+    void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds,
+            WindowContainerTransaction wct) {
+        final WindowContainerToken rootToken = mRootTaskInfo.token;
+        wct.setBounds(rootToken, rootBounds)
+                .reparent(task.token, rootToken, true /* onTop*/)
+                // Moving the root task to top after the child tasks were reparented , or the root
+                // task cannot be visible and focused.
+                .reorder(rootToken, true /* onTop */);
+    }
+
+    boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
+        // No matter if the root task is empty or not, moving the root to bottom because it no
+        // longer preserves visible child task.
+        wct.reorder(mRootTaskInfo.token, false /* onTop */);
+        if (mChildrenTaskInfo.size() == 0) return false;
+        wct.reparentTasks(
+                mRootTaskInfo.token,
+                null /* newParent */,
+                CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
+                CONTROLLED_ACTIVITY_TYPES,
+                toTop);
+        return true;
+    }
+
+    boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) {
+        final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId);
+        if (task == null) return false;
+        wct.reparent(task.token, newParent, false /* onTop */);
+        return true;
+    }
+
+    @Nullable
+    public SurfaceControl getOutlineLeash() {
+        return mOutlineManager.getOutlineLeash();
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        super.onTaskAppeared(taskInfo, leash);
+        if (isRootTask(taskInfo)) {
+            mOutlineManager = new OutlineManager(mContext, taskInfo.configuration);
+            enableOutline(true);
+        }
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        super.onTaskInfoChanged(taskInfo);
+        if (isRootTask(taskInfo)) {
+            mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds());
+        }
+    }
+
+    private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) {
+        return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId;
+    }
+
+    void enableOutline(boolean enable) {
+        if (mOutlineManager == null) {
+            return;
+        }
+
+        if (enable) {
+            if (mRootTaskInfo != null) {
+                mOutlineManager.inflate(mRootLeash,
+                        mRootTaskInfo.configuration.windowConfiguration.getBounds());
+            }
+        } else {
+            mOutlineManager.release();
+        }
+    }
+
+    void setOutlineVisibility(boolean visible) {
+        mOutlineManager.setVisibility(visible);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mOutlineManager.onInsetsChanged(insetsState);
+    }
+
+    @Override
+    public void insetsControlChanged(InsetsState insetsState,
+            InsetsSourceControl[] activeControls) {
+        insetsChanged(insetsState);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java
new file mode 100644
index 0000000..aec81a1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface to engage split-screen feature.
+ * TODO: Figure out which of these are actually needed outside of the Shell
+ */
+@ExternalThread
+public interface SplitScreen {
+    /**
+     * Stage type isn't specified normally meaning to use what ever the default is.
+     * E.g. exit split-screen and launch the app in fullscreen.
+     */
+    int STAGE_TYPE_UNDEFINED = -1;
+    /**
+     * The main stage type.
+     * @see MainStage
+     */
+    int STAGE_TYPE_MAIN = 0;
+
+    /**
+     * The side stage type.
+     * @see SideStage
+     */
+    int STAGE_TYPE_SIDE = 1;
+
+    @IntDef(prefix = { "STAGE_TYPE_" }, value = {
+            STAGE_TYPE_UNDEFINED,
+            STAGE_TYPE_MAIN,
+            STAGE_TYPE_SIDE
+    })
+    @interface StageType {}
+
+    /** Callback interface for listening to changes in a split-screen stage. */
+    interface SplitScreenListener {
+        default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {}
+        default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {}
+        default void onSplitVisibilityChanged(boolean visible) {}
+    }
+
+    /** Registers listener that gets split screen callback. */
+    void registerSplitScreenListener(@NonNull SplitScreenListener listener,
+            @NonNull Executor executor);
+
+    /** Unregisters listener that gets split screen callback. */
+    void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
+
+    /**
+     * Returns a binder that can be passed to an external process to manipulate SplitScreen.
+     */
+    default ISplitScreen createExternalInterface() {
+        return null;
+    }
+
+    /**
+     * Called when the keyguard occluded state changes.
+     * @param occluded Indicates if the keyguard is now occluded.
+     */
+    void onKeyguardOccludedChanged(boolean occluded);
+
+    /**
+     * Called when the visibility of the keyguard changes.
+     * @param showing Indicates if the keyguard is now visible.
+     */
+    void onKeyguardVisibilityChanged(boolean showing);
+
+    /** Get a string representation of a stage type */
+    static String stageTypeToString(@StageType int stage) {
+        switch (stage) {
+            case STAGE_TYPE_UNDEFINED: return "UNDEFINED";
+            case STAGE_TYPE_MAIN: return "MAIN";
+            case STAGE_TYPE_SIDE: return "SIDE";
+            default: return "UNKNOWN(" + stage + ")";
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java
new file mode 100644
index 0000000..94db9cd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.WindowManager;
+import android.window.RemoteTransition;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+import com.android.wm.shell.draganddrop.DragAndDropPolicy;
+import com.android.wm.shell.transition.LegacyTransitions;
+import com.android.wm.shell.transition.Transitions;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import javax.inject.Provider;
+
+/**
+ * Class manages split-screen multitasking mode and implements the main interface
+ * {@link SplitScreen}.
+ * @see StageCoordinator
+ */
+// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
+public class SplitScreenController implements DragAndDropPolicy.Starter,
+        RemoteCallable<SplitScreenController> {
+    private static final String TAG = SplitScreenController.class.getSimpleName();
+
+    private final ShellTaskOrganizer mTaskOrganizer;
+    private final SyncTransactionQueue mSyncQueue;
+    private final Context mContext;
+    private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+    private final ShellExecutor mMainExecutor;
+    private final SplitScreenImpl mImpl = new SplitScreenImpl();
+    private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final Transitions mTransitions;
+    private final TransactionPool mTransactionPool;
+    private final SplitscreenEventLogger mLogger;
+    private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
+
+    private StageCoordinator mStageCoordinator;
+
+    public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
+            SyncTransactionQueue syncQueue, Context context,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+            ShellExecutor mainExecutor, DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
+            Transitions transitions, TransactionPool transactionPool,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        mTaskOrganizer = shellTaskOrganizer;
+        mSyncQueue = syncQueue;
+        mContext = context;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mMainExecutor = mainExecutor;
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mTransitions = transitions;
+        mTransactionPool = transactionPool;
+        mUnfoldControllerProvider = unfoldControllerProvider;
+        mLogger = new SplitscreenEventLogger();
+    }
+
+    public SplitScreen asSplitScreen() {
+        return mImpl;
+    }
+
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public ShellExecutor getRemoteCallExecutor() {
+        return mMainExecutor;
+    }
+
+    public void onOrganizerRegistered() {
+        if (mStageCoordinator == null) {
+            // TODO: Multi-display
+            mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
+                    mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
+                    mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
+                    mUnfoldControllerProvider);
+        }
+    }
+
+    public boolean isSplitScreenVisible() {
+        return mStageCoordinator.isSplitScreenVisible();
+    }
+
+    public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) {
+        final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId);
+        if (task == null) {
+            throw new IllegalArgumentException("Unknown taskId" + taskId);
+        }
+        return moveToSideStage(task, sideStagePosition);
+    }
+
+    public boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
+            @SplitPosition int sideStagePosition) {
+        return mStageCoordinator.moveToSideStage(task, sideStagePosition);
+    }
+
+    public boolean removeFromSideStage(int taskId) {
+        return mStageCoordinator.removeFromSideStage(taskId);
+    }
+
+    public void setSideStageOutline(boolean enable) {
+        mStageCoordinator.setSideStageOutline(enable);
+    }
+
+    public void setSideStagePosition(@SplitPosition int sideStagePosition) {
+        mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */);
+    }
+
+    public void setSideStageVisibility(boolean visible) {
+        mStageCoordinator.setSideStageVisibility(visible);
+    }
+
+    public void enterSplitScreen(int taskId, boolean leftOrTop) {
+        moveToSideStage(taskId,
+                leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
+    }
+
+    public void exitSplitScreen(int toTopTaskId, int exitReason) {
+        mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
+    }
+
+    public void onKeyguardOccludedChanged(boolean occluded) {
+        mStageCoordinator.onKeyguardOccludedChanged(occluded);
+    }
+
+    public void onKeyguardVisibilityChanged(boolean showing) {
+        mStageCoordinator.onKeyguardVisibilityChanged(showing);
+    }
+
+    public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+        mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide);
+    }
+
+    public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
+        mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds);
+    }
+
+    public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        mStageCoordinator.registerSplitScreenListener(listener);
+    }
+
+    public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        mStageCoordinator.unregisterSplitScreenListener(listener);
+    }
+
+    public void startTask(int taskId, @SplitScreen.StageType int stage,
+            @SplitPosition int position, @Nullable Bundle options) {
+        options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
+
+        try {
+            ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to launch task", e);
+        }
+    }
+
+    public void startShortcut(String packageName, String shortcutId,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options, UserHandle user) {
+        options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
+
+        try {
+            LauncherApps launcherApps =
+                    mContext.getSystemService(LauncherApps.class);
+            launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
+                    options, user);
+        } catch (ActivityNotFoundException e) {
+            Slog.e(TAG, "Failed to launch shortcut", e);
+        }
+    }
+
+    public void startIntent(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options) {
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+            startIntentLegacy(intent, fillInIntent, stage, position, options);
+            return;
+        }
+        mStageCoordinator.startIntent(intent, fillInIntent, stage, position, options,
+                null /* remote */);
+    }
+
+    private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options) {
+        LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
+            @Override
+            public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+                    IRemoteAnimationFinishedCallback finishedCallback,
+                    SurfaceControl.Transaction t) {
+                mStageCoordinator.updateSurfaceBounds(null /* layout */, t);
+
+                if (apps != null) {
+                    for (int i = 0; i < apps.length; ++i) {
+                        if (apps[i].mode == MODE_OPENING) {
+                            t.show(apps[i].leash);
+                        }
+                    }
+                }
+
+                t.apply();
+                if (finishedCallback != null) {
+                    try {
+                        finishedCallback.onAnimationFinished();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error finishing legacy transition: ", e);
+                    }
+                }
+            }
+        };
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        options = mStageCoordinator.resolveStartStage(stage, position, options, wct);
+        wct.sendPendingIntent(intent, fillInIntent, options);
+        mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
+    }
+
+    RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) {
+        if (!isSplitScreenVisible()) return null;
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+                .setContainerLayer()
+                .setName("RecentsAnimationSplitTasks")
+                .setHidden(false)
+                .setCallsite("SplitScreenController#onGoingtoRecentsLegacy");
+        mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder);
+        SurfaceControl sc = builder.build();
+        SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+
+        // Ensure that we order these in the parent in the right z-order as their previous order
+        Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex);
+        int layer = 1;
+        for (RemoteAnimationTarget appTarget : apps) {
+            transaction.reparent(appTarget.leash, sc);
+            transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left,
+                    appTarget.screenSpaceBounds.top);
+            transaction.setLayer(appTarget.leash, layer++);
+        }
+        transaction.apply();
+        transaction.close();
+        return new RemoteAnimationTarget[]{
+                mStageCoordinator.getDividerBarLegacyTarget(),
+                mStageCoordinator.getOutlineLegacyTarget()};
+    }
+
+    /**
+     * Sets drag info to be logged when splitscreen is entered.
+     */
+    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
+    }
+
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        pw.println(prefix + TAG);
+        if (mStageCoordinator != null) {
+            mStageCoordinator.dump(pw, prefix);
+        }
+    }
+
+    /**
+     * The interface for calls from outside the Shell, within the host process.
+     */
+    @ExternalThread
+    private class SplitScreenImpl implements SplitScreen {
+        private ISplitScreenImpl mISplitScreen;
+        private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>();
+        private final SplitScreenListener mListener = new SplitScreenListener() {
+            @Override
+            public void onStagePositionChanged(int stage, int position) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onStagePositionChanged(stage, position);
+                    });
+                }
+            }
+
+            @Override
+            public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible);
+                    });
+                }
+            }
+
+            @Override
+            public void onSplitVisibilityChanged(boolean visible) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onSplitVisibilityChanged(visible);
+                    });
+                }
+            }
+        };
+
+        @Override
+        public ISplitScreen createExternalInterface() {
+            if (mISplitScreen != null) {
+                mISplitScreen.invalidate();
+            }
+            mISplitScreen = new ISplitScreenImpl(SplitScreenController.this);
+            return mISplitScreen;
+        }
+
+        @Override
+        public void onKeyguardOccludedChanged(boolean occluded) {
+            mMainExecutor.execute(() -> {
+                SplitScreenController.this.onKeyguardOccludedChanged(occluded);
+            });
+        }
+
+        @Override
+        public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) {
+            if (mExecutors.containsKey(listener)) return;
+
+            mMainExecutor.execute(() -> {
+                if (mExecutors.size() == 0) {
+                    SplitScreenController.this.registerSplitScreenListener(mListener);
+                }
+
+                mExecutors.put(listener, executor);
+            });
+
+            executor.execute(() -> {
+                mStageCoordinator.sendStatusToListener(listener);
+            });
+        }
+
+        @Override
+        public void unregisterSplitScreenListener(SplitScreenListener listener) {
+            mMainExecutor.execute(() -> {
+                mExecutors.remove(listener);
+
+                if (mExecutors.size() == 0) {
+                    SplitScreenController.this.unregisterSplitScreenListener(mListener);
+                }
+            });
+        }
+
+        @Override
+        public void onKeyguardVisibilityChanged(boolean showing) {
+            mMainExecutor.execute(() -> {
+                SplitScreenController.this.onKeyguardVisibilityChanged(showing);
+            });
+        }
+    }
+
+    /**
+     * The interface for calls from outside the host process.
+     */
+    @BinderThread
+    private static class ISplitScreenImpl extends ISplitScreen.Stub {
+        private SplitScreenController mController;
+        private ISplitScreenListener mListener;
+        private final SplitScreen.SplitScreenListener mSplitScreenListener =
+                new SplitScreen.SplitScreenListener() {
+                    @Override
+                    public void onStagePositionChanged(int stage, int position) {
+                        try {
+                            if (mListener != null) {
+                                mListener.onStagePositionChanged(stage, position);
+                            }
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "onStagePositionChanged", e);
+                        }
+                    }
+
+                    @Override
+                    public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+                        try {
+                            if (mListener != null) {
+                                mListener.onTaskStageChanged(taskId, stage, visible);
+                            }
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "onTaskStageChanged", e);
+                        }
+                    }
+                };
+        private final IBinder.DeathRecipient mListenerDeathRecipient =
+                new IBinder.DeathRecipient() {
+                    @Override
+                    @BinderThread
+                    public void binderDied() {
+                        final SplitScreenController controller = mController;
+                        controller.getRemoteCallExecutor().execute(() -> {
+                            mListener = null;
+                            controller.unregisterSplitScreenListener(mSplitScreenListener);
+                        });
+                    }
+                };
+
+        public ISplitScreenImpl(SplitScreenController controller) {
+            mController = controller;
+        }
+
+        /**
+         * Invalidates this instance, preventing future calls from updating the controller.
+         */
+        void invalidate() {
+            mController = null;
+        }
+
+        @Override
+        public void registerSplitScreenListener(ISplitScreenListener listener) {
+            executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener",
+                    (controller) -> {
+                        if (mListener != null) {
+                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
+                                    0 /* flags */);
+                        }
+                        if (listener != null) {
+                            try {
+                                listener.asBinder().linkToDeath(mListenerDeathRecipient,
+                                        0 /* flags */);
+                            } catch (RemoteException e) {
+                                Slog.e(TAG, "Failed to link to death");
+                                return;
+                            }
+                        }
+                        mListener = listener;
+                        controller.registerSplitScreenListener(mSplitScreenListener);
+                    });
+        }
+
+        @Override
+        public void unregisterSplitScreenListener(ISplitScreenListener listener) {
+            executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener",
+                    (controller) -> {
+                        if (mListener != null) {
+                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
+                                    0 /* flags */);
+                        }
+                        mListener = null;
+                        controller.unregisterSplitScreenListener(mSplitScreenListener);
+                    });
+        }
+
+        @Override
+        public void exitSplitScreen(int toTopTaskId) {
+            executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
+                    (controller) -> {
+                        controller.exitSplitScreen(toTopTaskId,
+                                FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT);
+                    });
+        }
+
+        @Override
+        public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+            executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide",
+                    (controller) -> {
+                        controller.exitSplitScreenOnHide(exitSplitScreenOnHide);
+                    });
+        }
+
+        @Override
+        public void setSideStageVisibility(boolean visible) {
+            executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility",
+                    (controller) -> {
+                        controller.setSideStageVisibility(visible);
+                    });
+        }
+
+        @Override
+        public void removeFromSideStage(int taskId) {
+            executeRemoteCallWithTaskPermission(mController, "removeFromSideStage",
+                    (controller) -> {
+                        controller.removeFromSideStage(taskId);
+                    });
+        }
+
+        @Override
+        public void startTask(int taskId, int stage, int position, @Nullable Bundle options) {
+            executeRemoteCallWithTaskPermission(mController, "startTask",
+                    (controller) -> {
+                        controller.startTask(taskId, stage, position, options);
+                    });
+        }
+
+        @Override
+        public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
+                int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+                RemoteAnimationAdapter adapter) {
+            executeRemoteCallWithTaskPermission(mController, "startTasks",
+                    (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
+                            mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
+                            adapter));
+        }
+
+        @Override
+        public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
+                int sideTaskId, @Nullable Bundle sideOptions,
+                @SplitPosition int sidePosition,
+                @Nullable RemoteTransition remoteTransition) {
+            executeRemoteCallWithTaskPermission(mController, "startTasks",
+                    (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
+                            sideTaskId, sideOptions, sidePosition, remoteTransition));
+        }
+
+        @Override
+        public void startShortcut(String packageName, String shortcutId, int stage, int position,
+                @Nullable Bundle options, UserHandle user) {
+            executeRemoteCallWithTaskPermission(mController, "startShortcut",
+                    (controller) -> {
+                        controller.startShortcut(packageName, shortcutId, stage, position,
+                                options, user);
+                    });
+        }
+
+        @Override
+        public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
+                @Nullable Bundle options) {
+            executeRemoteCallWithTaskPermission(mController, "startIntent",
+                    (controller) -> {
+                        controller.startIntent(intent, fillInIntent, stage, position, options);
+                    });
+        }
+
+        @Override
+        public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
+                RemoteAnimationTarget[] apps) {
+            final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null};
+            executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy",
+                    (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps),
+                    true /* blocking */);
+            return out[0];
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java
new file mode 100644
index 0000000..af9a5aa
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
+
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
+import static com.android.wm.shell.transition.Transitions.isOpeningType;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.transition.OneShotRemoteHandler;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+
+/** Manages transition animations for split-screen. */
+class SplitScreenTransitions {
+    private static final String TAG = "SplitScreenTransitions";
+
+    /** Flag applied to a transition change to identify it as a divider bar for animation. */
+    public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+
+    private final TransactionPool mTransactionPool;
+    private final Transitions mTransitions;
+    private final Runnable mOnFinish;
+
+    IBinder mPendingDismiss = null;
+    IBinder mPendingEnter = null;
+
+    private IBinder mAnimatingTransition = null;
+    private OneShotRemoteHandler mRemoteHandler = null;
+
+    private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> {
+        if (wct != null || wctCB != null) {
+            throw new UnsupportedOperationException("finish transactions not supported yet.");
+        }
+        onFinish();
+    };
+
+    /** Keeps track of currently running animations */
+    private final ArrayList<Animator> mAnimations = new ArrayList<>();
+
+    private Transitions.TransitionFinishCallback mFinishCallback = null;
+    private SurfaceControl.Transaction mFinishTransaction;
+
+    SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
+            @NonNull Runnable onFinishCallback) {
+        mTransactionPool = pool;
+        mTransitions = transitions;
+        mOnFinish = onFinishCallback;
+    }
+
+    void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) {
+        mFinishCallback = finishCallback;
+        mAnimatingTransition = transition;
+        if (mRemoteHandler != null) {
+            mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction,
+                    mRemoteFinishCB);
+            mRemoteHandler = null;
+            return;
+        }
+        playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot);
+    }
+
+    private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
+            @NonNull WindowContainerToken sideRoot) {
+        mFinishTransaction = mTransactionPool.acquire();
+
+        // Play some place-holder fade animations
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            final SurfaceControl leash = change.getLeash();
+            final int mode = info.getChanges().get(i).getMode();
+
+            if (mode == TRANSIT_CHANGE) {
+                if (change.getParent() != null) {
+                    // This is probably reparented, so we want the parent to be immediately visible
+                    final TransitionInfo.Change parentChange = info.getChange(change.getParent());
+                    t.show(parentChange.getLeash());
+                    t.setAlpha(parentChange.getLeash(), 1.f);
+                    // and then animate this layer outside the parent (since, for example, this is
+                    // the home task animating from fullscreen to part-screen).
+                    t.reparent(leash, info.getRootLeash());
+                    t.setLayer(leash, info.getChanges().size() - i);
+                    // build the finish reparent/reposition
+                    mFinishTransaction.reparent(leash, parentChange.getLeash());
+                    mFinishTransaction.setPosition(leash,
+                            change.getEndRelOffset().x, change.getEndRelOffset().y);
+                }
+                // TODO(shell-transitions): screenshot here
+                final Rect startBounds = new Rect(change.getStartAbsBounds());
+                if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+                    // Dismissing split via snap which means the still-visible task has been
+                    // dragged to its end position at animation start so reflect that here.
+                    startBounds.offsetTo(change.getEndAbsBounds().left,
+                            change.getEndAbsBounds().top);
+                }
+                final Rect endBounds = new Rect(change.getEndAbsBounds());
+                startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+                endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+                startExampleResizeAnimation(leash, startBounds, endBounds);
+            }
+            if (change.getParent() != null) {
+                continue;
+            }
+
+            if (transition == mPendingEnter && (mainRoot.equals(change.getContainer())
+                    || sideRoot.equals(change.getContainer()))) {
+                t.setWindowCrop(leash, change.getStartAbsBounds().width(),
+                        change.getStartAbsBounds().height());
+            }
+            boolean isOpening = isOpeningType(info.getType());
+            if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
+                // fade in
+                startExampleAnimation(leash, true /* show */);
+            } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
+                // fade out
+                if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+                    // Dismissing via snap-to-top/bottom means that the dismissed task is already
+                    // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
+                    // and don't animate it so it doesn't pop-in when reparented.
+                    t.setAlpha(leash, 0.f);
+                } else {
+                    startExampleAnimation(leash, false /* show */);
+                }
+            }
+        }
+        t.apply();
+        onFinish();
+    }
+
+    /** Starts a transition to enter split with a remote transition animator. */
+    IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
+            @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition,
+            @NonNull Transitions.TransitionHandler handler) {
+        if (remoteTransition != null) {
+            // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
+            mRemoteHandler = new OneShotRemoteHandler(
+                    mTransitions.getMainExecutor(), remoteTransition);
+        }
+        final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
+        mPendingEnter = transition;
+        if (mRemoteHandler != null) {
+            mRemoteHandler.setTransition(transition);
+        }
+        return transition;
+    }
+
+    /** Starts a transition for dismissing split after dragging the divider to a screen edge */
+    IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct,
+            @NonNull Transitions.TransitionHandler handler) {
+        final IBinder transition = mTransitions.startTransition(
+                TRANSIT_SPLIT_DISMISS_SNAP, wct, handler);
+        mPendingDismiss = transition;
+        return transition;
+    }
+
+    void onFinish() {
+        if (!mAnimations.isEmpty()) return;
+        mOnFinish.run();
+        if (mFinishTransaction != null) {
+            mFinishTransaction.apply();
+            mTransactionPool.release(mFinishTransaction);
+            mFinishTransaction = null;
+        }
+        mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+        mFinishCallback = null;
+        if (mAnimatingTransition == mPendingEnter) {
+            mPendingEnter = null;
+        }
+        if (mAnimatingTransition == mPendingDismiss) {
+            mPendingDismiss = null;
+        }
+        mAnimatingTransition = null;
+    }
+
+    // TODO(shell-transitions): real animations
+    private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
+        final float end = show ? 1.f : 0.f;
+        final float start = 1.f - end;
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        final ValueAnimator va = ValueAnimator.ofFloat(start, end);
+        va.setDuration(500);
+        va.addUpdateListener(animation -> {
+            float fraction = animation.getAnimatedFraction();
+            transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
+            transaction.apply();
+        });
+        final Runnable finisher = () -> {
+            transaction.setAlpha(leash, end);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+            mTransitions.getMainExecutor().execute(() -> {
+                mAnimations.remove(va);
+                onFinish();
+            });
+        };
+        va.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) { }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finisher.run();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                finisher.run();
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) { }
+        });
+        mAnimations.add(va);
+        mTransitions.getAnimExecutor().execute(va::start);
+    }
+
+    // TODO(shell-transitions): real animations
+    private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
+            @NonNull Rect startBounds, @NonNull Rect endBounds) {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
+        va.setDuration(500);
+        va.addUpdateListener(animation -> {
+            float fraction = animation.getAnimatedFraction();
+            transaction.setWindowCrop(leash,
+                    (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
+                    (int) (startBounds.height() * (1.f - fraction)
+                            + endBounds.height() * fraction));
+            transaction.setPosition(leash,
+                    startBounds.left * (1.f - fraction) + endBounds.left * fraction,
+                    startBounds.top * (1.f - fraction) + endBounds.top * fraction);
+            transaction.apply();
+        });
+        final Runnable finisher = () -> {
+            transaction.setWindowCrop(leash, 0, 0);
+            transaction.setPosition(leash, endBounds.left, endBounds.top);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+            mTransitions.getMainExecutor().execute(() -> {
+                mAnimations.remove(va);
+                onFinish();
+            });
+        };
+        va.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finisher.run();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                finisher.run();
+            }
+        });
+        mAnimations.add(va);
+        mTransitions.getAnimExecutor().execute(va::start);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java
new file mode 100644
index 0000000..aab7902
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+
+/**
+ * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
+ */
+public class SplitscreenEventLogger {
+
+    // Used to generate instance ids for this drag if one is not provided
+    private final InstanceIdSequence mIdSequence;
+
+    // The instance id for the current splitscreen session (from start to end)
+    private InstanceId mLoggerSessionId;
+
+    // Drag info
+    private @SplitPosition int mDragEnterPosition;
+    private InstanceId mDragEnterSessionId;
+
+    // For deduping async events
+    private int mLastMainStagePosition = -1;
+    private int mLastMainStageUid = -1;
+    private int mLastSideStagePosition = -1;
+    private int mLastSideStageUid = -1;
+    private float mLastSplitRatio = -1f;
+
+    public SplitscreenEventLogger() {
+        mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
+    }
+
+    /**
+     * Return whether a splitscreen session has started.
+     */
+    public boolean hasStartedSession() {
+        return mLoggerSessionId != null;
+    }
+
+    /**
+     * May be called before logEnter() to indicate that the session was started from a drag.
+     */
+    public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) {
+        mDragEnterPosition = position;
+        mDragEnterSessionId = dragSessionId;
+    }
+
+    /**
+     * Logs when the user enters splitscreen.
+     */
+    public void logEnter(float splitRatio,
+            @SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid,
+            boolean isLandscape) {
+        mLoggerSessionId = mIdSequence.newInstanceId();
+        int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED
+                ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape)
+                : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+        updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid);
+        updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid);
+        updateSplitRatioState(splitRatio);
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER,
+                enterReason,
+                0 /* exitReason */,
+                splitRatio,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the user exits splitscreen.  Only one of the main or side stages should be
+     * specified to indicate which position was focused as a part of exiting (both can be unset).
+     */
+    public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if ((mainStagePosition != SPLIT_POSITION_UNDEFINED
+                && sideStagePosition != SPLIT_POSITION_UNDEFINED)
+                        || (mainStageUid != 0 && sideStageUid != 0)) {
+            throw new IllegalArgumentException("Only main or side stage should be set");
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT,
+                0 /* enterReason */,
+                exitReason,
+                0f /* splitRatio */,
+                getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid,
+                getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+
+        // Reset states
+        mLoggerSessionId = null;
+        mDragEnterPosition = SPLIT_POSITION_UNDEFINED;
+        mDragEnterSessionId = null;
+        mLastMainStagePosition = -1;
+        mLastMainStageUid = -1;
+        mLastSideStagePosition = -1;
+        mLastSideStageUid = -1;
+    }
+
+    /**
+     * Logs when an app in the main stage changes.
+     */
+    public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid,
+            boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition,
+                isLandscape), mainStageUid)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                0 /* sideStagePosition */,
+                0 /* sideStageUid */,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when an app in the side stage changes.
+     */
+    public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid,
+            boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition,
+                isLandscape), sideStageUid)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                0 /* mainStagePosition */,
+                0 /* mainStageUid */,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the splitscreen ratio changes.
+     */
+    public void logResize(float splitRatio) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (splitRatio <= 0f || splitRatio >= 1f) {
+            // Don't bother reporting resizes that end up dismissing the split, that will be logged
+            // via the exit event
+            return;
+        }
+        if (!updateSplitRatioState(splitRatio)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                mLastSplitRatio,
+                0 /* mainStagePosition */, 0 /* mainStageUid */,
+                0 /* sideStagePosition */, 0 /* sideStageUid */,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the apps in splitscreen are swapped.
+     */
+    public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+
+        updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid);
+        updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid);
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    private boolean updateMainStageState(int mainStagePosition, int mainStageUid) {
+        boolean changed = (mLastMainStagePosition != mainStagePosition)
+                || (mLastMainStageUid != mainStageUid);
+        if (!changed) {
+            return false;
+        }
+
+        mLastMainStagePosition = mainStagePosition;
+        mLastMainStageUid = mainStageUid;
+        return true;
+    }
+
+    private boolean updateSideStageState(int sideStagePosition, int sideStageUid) {
+        boolean changed = (mLastSideStagePosition != sideStagePosition)
+                || (mLastSideStageUid != sideStageUid);
+        if (!changed) {
+            return false;
+        }
+
+        mLastSideStagePosition = sideStagePosition;
+        mLastSideStageUid = sideStageUid;
+        return true;
+    }
+
+    private boolean updateSplitRatioState(float splitRatio) {
+        boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0;
+        if (!changed) {
+            return false;
+        }
+
+        mLastSplitRatio = splitRatio;
+        return true;
+    }
+
+    public int getDragEnterReasonFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM;
+        }
+    }
+
+    private int getMainStagePositionFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (position == SPLIT_POSITION_UNDEFINED) {
+            return 0;
+        }
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM;
+        }
+    }
+
+    private int getSideStagePositionFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (position == SPLIT_POSITION_UNDEFINED) {
+            return 0;
+        }
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java
new file mode 100644
index 0000000..574e379
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java
@@ -0,0 +1,1330 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.WindowManager.transitTypeToString;
+import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
+
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_MAIN;
+import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_SIDE;
+import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.stagesplit.SplitScreen.stageTypeToString;
+import static com.android.wm.shell.stagesplit.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR;
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
+import static com.android.wm.shell.transition.Transitions.isClosingType;
+import static com.android.wm.shell.transition.Transitions.isOpeningType;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.WindowManager;
+import android.window.DisplayAreaInfo;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+import com.android.wm.shell.common.split.SplitWindowManager;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.transition.Transitions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Provider;
+
+/**
+ * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
+ * {@link SideStage} stages.
+ * Some high-level rules:
+ * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at
+ * least one child task.
+ * - The {@link MainStage} should only have children if the coordinator is active.
+ * - The {@link SplitLayout} divider is only visible if both the {@link MainStage}
+ * and {@link SideStage} are visible.
+ * - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible.
+ * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and
+ * {@link #onStageHasChildrenChanged(StageListenerImpl).}
+ */
+class StageCoordinator implements SplitLayout.SplitLayoutHandler,
+        RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler {
+
+    private static final String TAG = StageCoordinator.class.getSimpleName();
+
+    /** internal value for mDismissTop that represents no dismiss */
+    private static final int NO_DISMISS = -2;
+
+    private final SurfaceSession mSurfaceSession = new SurfaceSession();
+
+    private final MainStage mMainStage;
+    private final StageListenerImpl mMainStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mMainUnfoldController;
+    private final SideStage mSideStage;
+    private final StageListenerImpl mSideStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mSideUnfoldController;
+    @SplitPosition
+    private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
+
+    private final int mDisplayId;
+    private SplitLayout mSplitLayout;
+    private boolean mDividerVisible;
+    private final SyncTransactionQueue mSyncQueue;
+    private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+    private final ShellTaskOrganizer mTaskOrganizer;
+    private DisplayAreaInfo mDisplayAreaInfo;
+    private final Context mContext;
+    private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
+    private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final SplitScreenTransitions mSplitTransitions;
+    private final SplitscreenEventLogger mLogger;
+    private boolean mExitSplitScreenOnHide;
+    private boolean mKeyguardOccluded;
+
+    // TODO(b/187041611): remove this flag after totally deprecated legacy split
+    /** Whether the device is supporting legacy split or not. */
+    private boolean mUseLegacySplit;
+
+    @SplitScreen.StageType private int mDismissTop = NO_DISMISS;
+
+    /** The target stage to dismiss to when unlock after folded. */
+    @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+
+    private final Runnable mOnTransitionAnimationComplete = () -> {
+        // If still playing, let it finish.
+        if (!isSplitScreenVisible()) {
+            // Update divider state after animation so that it is still around and positioned
+            // properly for the animation itself.
+            setDividerVisibility(false);
+            mSplitLayout.resetDividerPosition();
+        }
+        mDismissTop = NO_DISMISS;
+    };
+
+    private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
+            new SplitWindowManager.ParentContainerCallbacks() {
+        @Override
+        public void attachToParentSurface(SurfaceControl.Builder b) {
+            mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b);
+        }
+
+        @Override
+        public void onLeashReady(SurfaceControl leash) {
+            mSyncQueue.runInSync(t -> applyDividerVisibility(t));
+        }
+    };
+
+    StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController, Transitions transitions,
+            TransactionPool transactionPool, SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        mContext = context;
+        mDisplayId = displayId;
+        mSyncQueue = syncQueue;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mTaskOrganizer = taskOrganizer;
+        mLogger = logger;
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+
+        mMainStage = new MainStage(
+                mTaskOrganizer,
+                mDisplayId,
+                mMainStageListener,
+                mSyncQueue,
+                mSurfaceSession,
+                mMainUnfoldController);
+        mSideStage = new SideStage(
+                mContext,
+                mTaskOrganizer,
+                mDisplayId,
+                mSideStageListener,
+                mSyncQueue,
+                mSurfaceSession,
+                mSideUnfoldController);
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage);
+        mRootTDAOrganizer.registerListener(displayId, this);
+        final DeviceStateManager deviceStateManager =
+                mContext.getSystemService(DeviceStateManager.class);
+        deviceStateManager.registerCallback(taskOrganizer.getExecutor(),
+                new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged));
+        mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
+                mOnTransitionAnimationComplete);
+        transitions.addHandler(this);
+    }
+
+    @VisibleForTesting
+    StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
+            MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
+            Transitions transitions, TransactionPool transactionPool,
+            SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        mContext = context;
+        mDisplayId = displayId;
+        mSyncQueue = syncQueue;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mTaskOrganizer = taskOrganizer;
+        mMainStage = mainStage;
+        mSideStage = sideStage;
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mRootTDAOrganizer.registerListener(displayId, this);
+        mSplitLayout = splitLayout;
+        mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
+                mOnTransitionAnimationComplete);
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mLogger = logger;
+        transitions.addHandler(this);
+    }
+
+    @VisibleForTesting
+    SplitScreenTransitions getSplitTransitions() {
+        return mSplitTransitions;
+    }
+
+    boolean isSplitScreenVisible() {
+        return mSideStageListener.mVisible && mMainStageListener.mVisible;
+    }
+
+    boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
+            @SplitPosition int sideStagePosition) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        setSideStagePosition(sideStagePosition, wct);
+        mMainStage.activate(getMainStageBounds(), wct);
+        mSideStage.addTask(task, getSideStageBounds(), wct);
+        mSyncQueue.queue(wct);
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t));
+        return true;
+    }
+
+    boolean removeFromSideStage(int taskId) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+        /**
+         * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the
+         * {@link SideStage} no longer has children.
+         */
+        final boolean result = mSideStage.removeTask(taskId,
+                mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null,
+                wct);
+        mTaskOrganizer.applyTransaction(wct);
+        return result;
+    }
+
+    void setSideStageOutline(boolean enable) {
+        mSideStage.enableOutline(enable);
+    }
+
+    /** Starts 2 tasks in one transition. */
+    void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
+            @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+            @Nullable RemoteTransition remoteTransition) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mainOptions = mainOptions != null ? mainOptions : new Bundle();
+        sideOptions = sideOptions != null ? sideOptions : new Bundle();
+        setSideStagePosition(sidePosition, wct);
+
+        // Build a request WCT that will launch both apps such that task 0 is on the main stage
+        // while task 1 is on the side stage.
+        mMainStage.activate(getMainStageBounds(), wct);
+        mSideStage.setBounds(getSideStageBounds(), wct);
+
+        // Make sure the launch options will put tasks in the corresponding split roots
+        addActivityOptions(mainOptions, mMainStage);
+        addActivityOptions(sideOptions, mSideStage);
+
+        // Add task launch requests
+        wct.startTask(mainTaskId, mainOptions);
+        wct.startTask(sideTaskId, sideOptions);
+
+        mSplitTransitions.startEnterTransition(
+                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
+    }
+
+    /** Starts 2 tasks in one legacy transition. */
+    void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
+            int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+            RemoteAnimationAdapter adapter) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        // Need to add another wrapper here in shell so that we can inject the divider bar
+        // and also manage the process elevation via setRunningRemote
+        IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+            @Override
+            public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+                    RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers,
+                    RemoteAnimationTarget[] nonApps,
+                    final IRemoteAnimationFinishedCallback finishedCallback) {
+                RemoteAnimationTarget[] augmentedNonApps =
+                        new RemoteAnimationTarget[nonApps.length + 1];
+                for (int i = 0; i < nonApps.length; ++i) {
+                    augmentedNonApps[i] = nonApps[i];
+                }
+                augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget();
+                try {
+                    ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
+                            adapter.getCallingApplication());
+                    adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps,
+                            finishedCallback);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+            }
+
+            @Override
+            public void onAnimationCancelled() {
+                try {
+                    adapter.getRunner().onAnimationCancelled();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+            }
+        };
+        RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
+                wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
+
+        if (mainOptions == null) {
+            mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle();
+        } else {
+            ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions);
+            mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
+        }
+
+        sideOptions = sideOptions != null ? sideOptions : new Bundle();
+        setSideStagePosition(sidePosition, wct);
+
+        // Build a request WCT that will launch both apps such that task 0 is on the main stage
+        // while task 1 is on the side stage.
+        mMainStage.activate(getMainStageBounds(), wct);
+        mSideStage.setBounds(getSideStageBounds(), wct);
+
+        // Make sure the launch options will put tasks in the corresponding split roots
+        addActivityOptions(mainOptions, mMainStage);
+        addActivityOptions(sideOptions, mSideStage);
+
+        // Add task launch requests
+        wct.startTask(mainTaskId, mainOptions);
+        wct.startTask(sideTaskId, sideOptions);
+
+        // Using legacy transitions, so we can't use blast sync since it conflicts.
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    public void startIntent(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @androidx.annotation.Nullable Bundle options,
+            @Nullable RemoteTransition remoteTransition) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        options = resolveStartStage(stage, position, options, wct);
+        wct.sendPendingIntent(intent, fillInIntent, options);
+        mSplitTransitions.startEnterTransition(
+                TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this);
+    }
+
+    Bundle resolveStartStage(@SplitScreen.StageType int stage,
+            @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
+            @androidx.annotation.Nullable WindowContainerTransaction wct) {
+        switch (stage) {
+            case STAGE_TYPE_UNDEFINED: {
+                // Use the stage of the specified position is valid.
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    if (position == getSideStagePosition()) {
+                        options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct);
+                    } else {
+                        options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct);
+                    }
+                } else {
+                    // Exit split-screen and launch fullscreen since stage wasn't specified.
+                    prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
+                }
+                break;
+            }
+            case STAGE_TYPE_SIDE: {
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    setSideStagePosition(position, wct);
+                } else {
+                    position = getSideStagePosition();
+                }
+                if (options == null) {
+                    options = new Bundle();
+                }
+                updateActivityOptions(options, position);
+                break;
+            }
+            case STAGE_TYPE_MAIN: {
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    // Set the side stage opposite of what we want to the main stage.
+                    final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT
+                            ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
+                    setSideStagePosition(sideStagePosition, wct);
+                } else {
+                    position = getMainStagePosition();
+                }
+                if (options == null) {
+                    options = new Bundle();
+                }
+                updateActivityOptions(options, position);
+                break;
+            }
+            default:
+                throw new IllegalArgumentException("Unknown stage=" + stage);
+        }
+
+        return options;
+    }
+
+    @SplitPosition
+    int getSideStagePosition() {
+        return mSideStagePosition;
+    }
+
+    @SplitPosition
+    int getMainStagePosition() {
+        return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
+    }
+
+    void setSideStagePosition(@SplitPosition int sideStagePosition,
+            @Nullable WindowContainerTransaction wct) {
+        setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
+    }
+
+    private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds,
+            @Nullable WindowContainerTransaction wct) {
+        if (mSideStagePosition == sideStagePosition) return;
+        mSideStagePosition = sideStagePosition;
+        sendOnStagePositionChanged();
+
+        if (mSideStageListener.mVisible && updateBounds) {
+            if (wct == null) {
+                // onLayoutSizeChanged builds/applies a wct with the contents of updateWindowBounds.
+                onLayoutSizeChanged(mSplitLayout);
+            } else {
+                updateWindowBounds(mSplitLayout, wct);
+                updateUnfoldBounds();
+            }
+        }
+    }
+
+    void setSideStageVisibility(boolean visible) {
+        if (mSideStageListener.mVisible == visible) return;
+
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mSideStage.setVisibility(visible, wct);
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    void onKeyguardOccludedChanged(boolean occluded) {
+        // Do not exit split directly, because it needs to wait for task info update to determine
+        // which task should remain on top after split dismissed.
+        mKeyguardOccluded = occluded;
+    }
+
+    void onKeyguardVisibilityChanged(boolean showing) {
+        if (!showing && mMainStage.isActive()
+                && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
+            exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
+                    SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED);
+        }
+    }
+
+    void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+        mExitSplitScreenOnHide = exitSplitScreenOnHide;
+    }
+
+    void exitSplitScreen(int toTopTaskId, int exitReason) {
+        StageTaskListener childrenToTop = null;
+        if (mMainStage.containsTask(toTopTaskId)) {
+            childrenToTop = mMainStage;
+        } else if (mSideStage.containsTask(toTopTaskId)) {
+            childrenToTop = mSideStage;
+        }
+
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        if (childrenToTop != null) {
+            childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct);
+        }
+        applyExitSplitScreen(childrenToTop, wct, exitReason);
+    }
+
+    private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        applyExitSplitScreen(childrenToTop, wct, exitReason);
+    }
+
+    private void applyExitSplitScreen(
+            StageTaskListener childrenToTop,
+            WindowContainerTransaction wct, int exitReason) {
+        mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
+        mMainStage.deactivate(wct, childrenToTop == mMainStage);
+        mTaskOrganizer.applyTransaction(wct);
+        mSyncQueue.runInSync(t -> t
+                .setWindowCrop(mMainStage.mRootLeash, null)
+                .setWindowCrop(mSideStage.mRootLeash, null));
+        // Hide divider and reset its position.
+        setDividerVisibility(false);
+        mSplitLayout.resetDividerPosition();
+        mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+        if (childrenToTop != null) {
+            logExitToStage(exitReason, childrenToTop == mMainStage);
+        } else {
+            logExit(exitReason);
+        }
+    }
+
+    /**
+     * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates
+     * an existing WindowContainerTransaction (rather than applying immediately). This is intended
+     * to be used when exiting split might be bundled with other window operations.
+     */
+    void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop,
+            @NonNull WindowContainerTransaction wct) {
+        mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
+        mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN);
+    }
+
+    void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
+        outTopOrLeftBounds.set(mSplitLayout.getBounds1());
+        outBottomOrRightBounds.set(mSplitLayout.getBounds2());
+    }
+
+    private void addActivityOptions(Bundle opts, StageTaskListener stage) {
+        opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
+    }
+
+    void updateActivityOptions(Bundle opts, @SplitPosition int position) {
+        addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage);
+    }
+
+    void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        if (mListeners.contains(listener)) return;
+        mListeners.add(listener);
+        sendStatusToListener(listener);
+    }
+
+    void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        mListeners.remove(listener);
+    }
+
+    void sendStatusToListener(SplitScreen.SplitScreenListener listener) {
+        listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
+        listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
+        listener.onSplitVisibilityChanged(isSplitScreenVisible());
+        mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
+        mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
+    }
+
+    private void sendOnStagePositionChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            final SplitScreen.SplitScreenListener l = mListeners.get(i);
+            l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
+            l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
+        }
+    }
+
+    private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
+            boolean present, boolean visible) {
+        int stage;
+        if (present) {
+            stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+        } else {
+            // No longer on any stage
+            stage = STAGE_TYPE_UNDEFINED;
+        }
+        if (stage == STAGE_TYPE_MAIN) {
+            mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        } else {
+            mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        }
+
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            mListeners.get(i).onTaskStageChanged(taskId, stage, visible);
+        }
+    }
+
+    private void sendSplitVisibilityChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            final SplitScreen.SplitScreenListener l = mListeners.get(i);
+            l.onSplitVisibilityChanged(mDividerVisible);
+        }
+
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+            mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+        }
+    }
+
+    private void onStageRootTaskAppeared(StageListenerImpl stageListener) {
+        if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) {
+            mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit);
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            // Make the stages adjacent to each other so they occlude what's behind them.
+            wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+
+            // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy
+            // split to prevent new split behavior confusing users.
+            if (!mUseLegacySplit) {
+                wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
+            }
+
+            mTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
+    private void onStageRootTaskVanished(StageListenerImpl stageListener) {
+        if (stageListener == mMainStageListener || stageListener == mSideStageListener) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            // Deactivate the main stage if it no longer has a root task.
+            mMainStage.deactivate(wct);
+
+            if (!mUseLegacySplit) {
+                wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
+            }
+
+            mTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
+    private void setDividerVisibility(boolean visible) {
+        if (mDividerVisible == visible) return;
+        mDividerVisible = visible;
+        if (visible) {
+            mSplitLayout.init();
+            updateUnfoldBounds();
+        } else {
+            mSplitLayout.release();
+        }
+        sendSplitVisibilityChanged();
+    }
+
+    private void onStageVisibilityChanged(StageListenerImpl stageListener) {
+        final boolean sideStageVisible = mSideStageListener.mVisible;
+        final boolean mainStageVisible = mMainStageListener.mVisible;
+        final boolean bothStageVisible = sideStageVisible && mainStageVisible;
+        final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible;
+        final boolean sameVisibility = sideStageVisible == mainStageVisible;
+        // Only add or remove divider when both visible or both invisible to avoid sometimes we only
+        // got one stage visibility changed for a moment and it will cause flicker.
+        if (sameVisibility) {
+            setDividerVisibility(bothStageVisible);
+        }
+
+        if (bothStageInvisible) {
+            if (mExitSplitScreenOnHide
+            // Don't dismiss staged split when both stages are not visible due to sleeping display,
+            // like the cases keyguard showing or screen off.
+            || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) {
+                exitSplitScreen(null /* childrenToTop */,
+                        SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+            }
+        } else if (mKeyguardOccluded) {
+            // At least one of the stages is visible while keyguard occluded. Dismiss split because
+            // there's show-when-locked activity showing on top of keyguard. Also make sure the
+            // task contains show-when-locked activity remains on top after split dismissed.
+            final StageTaskListener toTop =
+                    mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null);
+            exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP);
+        }
+
+        mSyncQueue.runInSync(t -> {
+            // Same above, we only set root tasks and divider leash visibility when both stage
+            // change to visible or invisible to avoid flicker.
+            if (sameVisibility) {
+                t.setVisibility(mSideStage.mRootLeash, bothStageVisible)
+                        .setVisibility(mMainStage.mRootLeash, bothStageVisible);
+                applyDividerVisibility(t);
+                applyOutlineVisibility(t);
+            }
+        });
+    }
+
+    private void applyDividerVisibility(SurfaceControl.Transaction t) {
+        final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+        if (dividerLeash == null) {
+            return;
+        }
+
+        if (mDividerVisible) {
+            t.show(dividerLeash)
+                    .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER)
+                    .setPosition(dividerLeash,
+                            mSplitLayout.getDividerBounds().left,
+                            mSplitLayout.getDividerBounds().top);
+        } else {
+            t.hide(dividerLeash);
+        }
+    }
+
+    private void applyOutlineVisibility(SurfaceControl.Transaction t) {
+        final SurfaceControl outlineLeash = mSideStage.getOutlineLeash();
+        if (outlineLeash == null) {
+            return;
+        }
+
+        if (mDividerVisible) {
+            t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER);
+        } else {
+            t.hide(outlineLeash);
+        }
+    }
+
+    private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
+        final boolean hasChildren = stageListener.mHasChildren;
+        final boolean isSideStage = stageListener == mSideStageListener;
+        if (!hasChildren) {
+            if (isSideStage && mMainStageListener.mVisible) {
+                // Exit to main stage if side stage no longer has children.
+                exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
+            } else if (!isSideStage && mSideStageListener.mVisible) {
+                // Exit to side stage if main stage no longer has children.
+                exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
+            }
+        } else if (isSideStage) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            // Make sure the main stage is active.
+            mMainStage.activate(getMainStageBounds(), wct);
+            mSideStage.setBounds(getSideStageBounds(), wct);
+            mTaskOrganizer.applyTransaction(wct);
+        }
+        if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren
+                && mSideStageListener.mHasChildren) {
+            mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
+                    getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                    getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        }
+    }
+
+    @VisibleForTesting
+    IBinder onSnappedToDismissTransition(boolean mainStageToTop) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        prepareExitSplitScreen(mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE, wct);
+        return mSplitTransitions.startSnapToDismiss(wct, this);
+    }
+
+    @Override
+    public void onSnappedToDismiss(boolean bottomOrRight) {
+        final boolean mainStageToTop =
+                bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
+                        : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
+        if (ENABLE_SHELL_TRANSITIONS) {
+            onSnappedToDismissTransition(mainStageToTop);
+            return;
+        }
+        exitSplitScreen(mainStageToTop ? mMainStage : mSideStage,
+                SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER);
+    }
+
+    @Override
+    public void onDoubleTappedDivider() {
+        setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */);
+        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                mSplitLayout.isLandscape());
+    }
+
+    @Override
+    public void onLayoutPositionChanging(SplitLayout layout) {
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+    }
+
+    @Override
+    public void onLayoutSizeChanging(SplitLayout layout) {
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+        mSideStage.setOutlineVisibility(false);
+    }
+
+    @Override
+    public void onLayoutSizeChanged(SplitLayout layout) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        updateWindowBounds(layout, wct);
+        updateUnfoldBounds();
+        mSyncQueue.queue(wct);
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+        mSideStage.setOutlineVisibility(true);
+        mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
+    }
+
+    private void updateUnfoldBounds() {
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onLayoutChanged(getMainStageBounds());
+            mSideUnfoldController.onLayoutChanged(getSideStageBounds());
+        }
+    }
+
+    /**
+     * Populates `wct` with operations that match the split windows to the current layout.
+     * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
+     */
+    private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
+    }
+
+    void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash,
+                bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer);
+    }
+
+    @Override
+    public int getSplitItemPosition(WindowContainerToken token) {
+        if (token == null) {
+            return SPLIT_POSITION_UNDEFINED;
+        }
+
+        if (token.equals(mMainStage.mRootTaskInfo.getToken())) {
+            return getMainStagePosition();
+        } else if (token.equals(mSideStage.mRootTaskInfo.getToken())) {
+            return getSideStagePosition();
+        }
+
+        return SPLIT_POSITION_UNDEFINED;
+    }
+
+    @Override
+    public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo,
+                bottomRightStage.mRootTaskInfo);
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    @Override
+    public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) {
+        mDisplayAreaInfo = displayAreaInfo;
+        if (mSplitLayout == null) {
+            mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
+                    mDisplayAreaInfo.configuration, this, mParentContainerCallbacks,
+                    mDisplayImeController, mTaskOrganizer);
+            mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
+
+            if (mMainUnfoldController != null && mSideUnfoldController != null) {
+                mMainUnfoldController.init();
+                mSideUnfoldController.init();
+            }
+        }
+    }
+
+    @Override
+    public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
+        throw new IllegalStateException("Well that was unexpected...");
+    }
+
+    @Override
+    public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
+        mDisplayAreaInfo = displayAreaInfo;
+        if (mSplitLayout != null
+                && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)
+                && mMainStage.isActive()) {
+            onLayoutSizeChanged(mSplitLayout);
+        }
+    }
+
+    private void onFoldedStateChanged(boolean folded) {
+        mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+        if (!folded) return;
+
+        if (mMainStage.isFocused()) {
+            mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN;
+        } else if (mSideStage.isFocused()) {
+            mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE;
+        }
+    }
+
+    private Rect getSideStageBounds() {
+        return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2();
+    }
+
+    private Rect getMainStageBounds() {
+        return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1();
+    }
+
+    /**
+     * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain
+     * this task (yet) so this can also be used to identify which stage to put a task into.
+     */
+    private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) {
+        // TODO(b/184679596): Find a way to either include task-org information in the transition,
+        //                    or synchronize task-org callbacks so we can use stage.containsTask
+        if (mMainStage.mRootTaskInfo != null
+                && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) {
+            return mMainStage;
+        } else if (mSideStage.mRootTaskInfo != null
+                && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
+            return mSideStage;
+        }
+        return null;
+    }
+
+    @SplitScreen.StageType
+    private int getStageType(StageTaskListener stage) {
+        return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+    }
+
+    @Override
+    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+            @Nullable TransitionRequestInfo request) {
+        final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
+        if (triggerTask == null) {
+            // still want to monitor everything while in split-screen, so return non-null.
+            return isSplitScreenVisible() ? new WindowContainerTransaction() : null;
+        }
+
+        WindowContainerTransaction out = null;
+        final @WindowManager.TransitionType int type = request.getType();
+        if (isSplitScreenVisible()) {
+            // try to handle everything while in split-screen, so return a WCT even if it's empty.
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  split is active so using split"
+                            + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d"
+                            + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type),
+                    mMainStage.getChildCount(), mSideStage.getChildCount());
+            out = new WindowContainerTransaction();
+            final StageTaskListener stage = getStageOfTask(triggerTask);
+            if (stage != null) {
+                // dismiss split if the last task in one of the stages is going away
+                if (isClosingType(type) && stage.getChildCount() == 1) {
+                    // The top should be the opposite side that is closing:
+                    mDismissTop = getStageType(stage) == STAGE_TYPE_MAIN
+                            ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+                }
+            } else {
+                if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) {
+                    // Going home so dismiss both.
+                    mDismissTop = STAGE_TYPE_UNDEFINED;
+                }
+            }
+            if (mDismissTop != NO_DISMISS) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  splitTransition "
+                                + " deduced Dismiss from request. toTop=%s",
+                        stageTypeToString(mDismissTop));
+                prepareExitSplitScreen(mDismissTop, out);
+                mSplitTransitions.mPendingDismiss = transition;
+            }
+        } else {
+            // Not in split mode, so look for an open into a split stage just so we can whine and
+            // complain about how this isn't a supported operation.
+            if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)) {
+                if (getStageOfTask(triggerTask) != null) {
+                    throw new IllegalStateException("Entering split implicitly with only one task"
+                            + " isn't supported.");
+                }
+            }
+        }
+        return out;
+    }
+
+    @Override
+    public boolean startAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (transition != mSplitTransitions.mPendingDismiss
+                && transition != mSplitTransitions.mPendingEnter) {
+            // Not entering or exiting, so just do some house-keeping and validation.
+
+            // If we're not in split-mode, just abort so something else can handle it.
+            if (!isSplitScreenVisible()) return false;
+
+            for (int iC = 0; iC < info.getChanges().size(); ++iC) {
+                final TransitionInfo.Change change = info.getChanges().get(iC);
+                final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+                if (taskInfo == null || !taskInfo.hasParentTask()) continue;
+                final StageTaskListener stage = getStageOfTask(taskInfo);
+                if (stage == null) continue;
+                if (isOpeningType(change.getMode())) {
+                    if (!stage.containsTask(taskInfo.taskId)) {
+                        Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called"
+                                + " with " + taskInfo.taskId + " before startAnimation().");
+                    }
+                } else if (isClosingType(change.getMode())) {
+                    if (stage.containsTask(taskInfo.taskId)) {
+                        Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
+                                + " with " + taskInfo.taskId + " before startAnimation().");
+                    }
+                }
+            }
+            if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+                // TODO(shell-transitions): Implement a fallback behavior for now.
+                throw new IllegalStateException("Somehow removed the last task in a stage"
+                        + " outside of a proper transition");
+                // This can happen in some pathological cases. For example:
+                // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C]
+                // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time
+                // In this case, the result *should* be that we leave split.
+                // TODO(b/184679596): Find a way to either include task-org information in
+                //                    the transition, or synchronize task-org callbacks.
+            }
+
+            // Use normal animations.
+            return false;
+        }
+
+        boolean shouldAnimate = true;
+        if (mSplitTransitions.mPendingEnter == transition) {
+            shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
+        } else if (mSplitTransitions.mPendingDismiss == transition) {
+            shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction);
+        }
+        if (!shouldAnimate) return false;
+
+        mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
+                finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+        return true;
+    }
+
+    private boolean startPendingEnterAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+        if (info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN) {
+            // First, verify that we actually have opened 2 apps in split.
+            TransitionInfo.Change mainChild = null;
+            TransitionInfo.Change sideChild = null;
+            for (int iC = 0; iC < info.getChanges().size(); ++iC) {
+                final TransitionInfo.Change change = info.getChanges().get(iC);
+                final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+                if (taskInfo == null || !taskInfo.hasParentTask()) continue;
+                final @SplitScreen.StageType int stageType = getStageType(getStageOfTask(taskInfo));
+                if (stageType == STAGE_TYPE_MAIN) {
+                    mainChild = change;
+                } else if (stageType == STAGE_TYPE_SIDE) {
+                    sideChild = change;
+                }
+            }
+            if (mainChild == null || sideChild == null) {
+                throw new IllegalStateException("Launched 2 tasks in split, but didn't receive"
+                        + " 2 tasks in transition. Possibly one of them failed to launch");
+                // TODO: fallback logic. Probably start a new transition to exit split before
+                //       applying anything here. Ideally consolidate with transition-merging.
+            }
+
+            // Update local states (before animating).
+            setDividerVisibility(true);
+            setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */,
+                    null /* wct */);
+            setSplitsVisible(true);
+
+            addDividerBarToTransition(info, t, true /* show */);
+
+            // Make some noise if things aren't totally expected. These states shouldn't effect
+            // transitions locally, but remotes (like Launcher) may get confused if they were
+            // depending on listener callbacks. This can happen because task-organizer callbacks
+            // aren't serialized with transition callbacks.
+            // TODO(b/184679596): Find a way to either include task-org information in
+            //                    the transition, or synchronize task-org callbacks.
+            if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) {
+                Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
+                        + " to have been called with " + mainChild.getTaskInfo().taskId
+                        + " before startAnimation().");
+            }
+            if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) {
+                Log.w(TAG, "Expected onTaskAppeared on " + mSideStage
+                        + " to have been called with " + sideChild.getTaskInfo().taskId
+                        + " before startAnimation().");
+            }
+            return true;
+        } else {
+            // TODO: other entry method animations
+            throw new RuntimeException("Unsupported split-entry");
+        }
+    }
+
+    private boolean startPendingDismissAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+        // Make some noise if things aren't totally expected. These states shouldn't effect
+        // transitions locally, but remotes (like Launcher) may get confused if they were
+        // depending on listener callbacks. This can happen because task-organizer callbacks
+        // aren't serialized with transition callbacks.
+        // TODO(b/184679596): Find a way to either include task-org information in
+        //                    the transition, or synchronize task-org callbacks.
+        if (mMainStage.getChildCount() != 0) {
+            final StringBuilder tasksLeft = new StringBuilder();
+            for (int i = 0; i < mMainStage.getChildCount(); ++i) {
+                tasksLeft.append(i != 0 ? ", " : "");
+                tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i));
+            }
+            Log.w(TAG, "Expected onTaskVanished on " + mMainStage
+                    + " to have been called with [" + tasksLeft.toString()
+                    + "] before startAnimation().");
+        }
+        if (mSideStage.getChildCount() != 0) {
+            final StringBuilder tasksLeft = new StringBuilder();
+            for (int i = 0; i < mSideStage.getChildCount(); ++i) {
+                tasksLeft.append(i != 0 ? ", " : "");
+                tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i));
+            }
+            Log.w(TAG, "Expected onTaskVanished on " + mSideStage
+                    + " to have been called with [" + tasksLeft.toString()
+                    + "] before startAnimation().");
+        }
+
+        // Update local states.
+        setSplitsVisible(false);
+        // Wait until after animation to update divider
+
+        if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+            // Reset crops so they don't interfere with subsequent launches
+            t.setWindowCrop(mMainStage.mRootLeash, null);
+            t.setWindowCrop(mSideStage.mRootLeash, null);
+        }
+
+        if (mDismissTop == STAGE_TYPE_UNDEFINED) {
+            // Going home (dismissing both splits)
+
+            // TODO: Have a proper remote for this. Until then, though, reset state and use the
+            //       normal animation stuff (which falls back to the normal launcher remote).
+            t.hide(mSplitLayout.getDividerLeash());
+            setDividerVisibility(false);
+            mSplitTransitions.mPendingDismiss = null;
+            return false;
+        }
+
+        addDividerBarToTransition(info, t, false /* show */);
+        // We're dismissing split by moving the other one to fullscreen.
+        // Since we don't have any animations for this yet, just use the internal example
+        // animations.
+        return true;
+    }
+
+    private void addDividerBarToTransition(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, boolean show) {
+        final SurfaceControl leash = mSplitLayout.getDividerLeash();
+        final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
+        final Rect bounds = mSplitLayout.getDividerBounds();
+        barChange.setStartAbsBounds(bounds);
+        barChange.setEndAbsBounds(bounds);
+        barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
+        barChange.setFlags(FLAG_IS_DIVIDER_BAR);
+        // Technically this should be order-0, but this is running after layer assignment
+        // and it's a special case, so just add to end.
+        info.addChange(barChange);
+        // Be default, make it visible. The remote animator can adjust alpha if it plans to animate.
+        if (show) {
+            t.setAlpha(leash, 1.f);
+            t.setLayer(leash, SPLIT_DIVIDER_LAYER);
+            t.setPosition(leash, bounds.left, bounds.top);
+            t.show(leash);
+        }
+    }
+
+    RemoteAnimationTarget getDividerBarLegacyTarget() {
+        final Rect bounds = mSplitLayout.getDividerBounds();
+        return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
+                mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */,
+                null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+                new android.graphics.Point(0, 0) /* position */, bounds, bounds,
+                new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
+                null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
+    }
+
+    RemoteAnimationTarget getOutlineLegacyTarget() {
+        final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds();
+        // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to
+        // distinguish as a split auxiliary target in Launcher.
+        return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
+                mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */,
+                null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+                new android.graphics.Point(0, 0) /* position */, bounds, bounds,
+                new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
+                null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
+    }
+
+    @Override
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        final String childPrefix = innerPrefix + "  ";
+        pw.println(prefix + TAG + " mDisplayId=" + mDisplayId);
+        pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
+        pw.println(innerPrefix + "MainStage");
+        pw.println(childPrefix + "isActive=" + mMainStage.isActive());
+        mMainStageListener.dump(pw, childPrefix);
+        pw.println(innerPrefix + "SideStage");
+        mSideStageListener.dump(pw, childPrefix);
+        pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout);
+    }
+
+    /**
+     * Directly set the visibility of both splits. This assumes hasChildren matches visibility.
+     * This is intended for batch use, so it assumes other state management logic is already
+     * handled.
+     */
+    private void setSplitsVisible(boolean visible) {
+        mMainStageListener.mVisible = mSideStageListener.mVisible = visible;
+        mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible;
+    }
+
+    /**
+     * Sets drag info to be logged when splitscreen is next entered.
+     */
+    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        mLogger.enterRequestedByDrag(position, dragSessionId);
+    }
+
+    /**
+     * Logs the exit of splitscreen.
+     */
+    private void logExit(int exitReason) {
+        mLogger.logExit(exitReason,
+                SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */,
+                SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */,
+                mSplitLayout.isLandscape());
+    }
+
+    /**
+     * Logs the exit of splitscreen to a specific stage. This must be called before the exit is
+     * executed.
+     */
+    private void logExitToStage(int exitReason, boolean toMainStage) {
+        mLogger.logExit(exitReason,
+                toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED,
+                toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */,
+                !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED,
+                !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */,
+                mSplitLayout.isLandscape());
+    }
+
+    class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
+        boolean mHasRootTask = false;
+        boolean mVisible = false;
+        boolean mHasChildren = false;
+
+        @Override
+        public void onRootTaskAppeared() {
+            mHasRootTask = true;
+            StageCoordinator.this.onStageRootTaskAppeared(this);
+        }
+
+        @Override
+        public void onStatusChanged(boolean visible, boolean hasChildren) {
+            if (!mHasRootTask) return;
+
+            if (mHasChildren != hasChildren) {
+                mHasChildren = hasChildren;
+                StageCoordinator.this.onStageHasChildrenChanged(this);
+            }
+            if (mVisible != visible) {
+                mVisible = visible;
+                StageCoordinator.this.onStageVisibilityChanged(this);
+            }
+        }
+
+        @Override
+        public void onChildTaskStatusChanged(int taskId, boolean present, boolean visible) {
+            StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible);
+        }
+
+        @Override
+        public void onRootTaskVanished() {
+            reset();
+            StageCoordinator.this.onStageRootTaskVanished(this);
+        }
+
+        @Override
+        public void onNoLongerSupportMultiWindow() {
+            if (mMainStage.isActive()) {
+                StageCoordinator.this.exitSplitScreen(null /* childrenToTop */,
+                        SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW);
+            }
+        }
+
+        private void reset() {
+            mHasRootTask = false;
+            mVisible = false;
+            mHasChildren = false;
+        }
+
+        public void dump(@NonNull PrintWriter pw, String prefix) {
+            pw.println(prefix + "mHasRootTask=" + mHasRootTask);
+            pw.println(prefix + "mVisible=" + mVisible);
+            pw.println(prefix + "mHasChildren=" + mHasChildren);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java
new file mode 100644
index 0000000..8b36c94
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
+
+import android.annotation.CallSuper;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.SurfaceUtils;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+import java.io.PrintWriter;
+
+/**
+ * Base class that handle common task org. related for split-screen stages.
+ * Note that this class and its sub-class do not directly perform hierarchy operations.
+ * They only serve to hold a collection of tasks and provide APIs like
+ * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator}
+ * to perform operations in-sync with other containers.
+ *
+ * @see StageCoordinator
+ */
+class StageTaskListener implements ShellTaskOrganizer.TaskListener {
+    private static final String TAG = StageTaskListener.class.getSimpleName();
+
+    protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
+    protected static final int[] CONTROLLED_WINDOWING_MODES =
+            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
+    protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
+            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
+
+    /** Callback interface for listening to changes in a split-screen stage. */
+    public interface StageListenerCallbacks {
+        void onRootTaskAppeared();
+
+        void onStatusChanged(boolean visible, boolean hasChildren);
+
+        void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
+
+        void onRootTaskVanished();
+        void onNoLongerSupportMultiWindow();
+    }
+
+    private final StageListenerCallbacks mCallbacks;
+    private final SurfaceSession mSurfaceSession;
+    protected final SyncTransactionQueue mSyncQueue;
+
+    protected ActivityManager.RunningTaskInfo mRootTaskInfo;
+    protected SurfaceControl mRootLeash;
+    protected SurfaceControl mDimLayer;
+    protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();
+    private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>();
+
+    private final StageTaskUnfoldController mStageTaskUnfoldController;
+
+    StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId,
+            StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        mCallbacks = callbacks;
+        mSyncQueue = syncQueue;
+        mSurfaceSession = surfaceSession;
+        mStageTaskUnfoldController = stageTaskUnfoldController;
+        taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
+    }
+
+    int getChildCount() {
+        return mChildrenTaskInfo.size();
+    }
+
+    boolean containsTask(int taskId) {
+        return mChildrenTaskInfo.contains(taskId);
+    }
+
+    /**
+     * Returns the top activity uid for the top child task.
+     */
+    int getTopChildTaskUid() {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
+            if (info.topActivityInfo == null) {
+                continue;
+            }
+            return info.topActivityInfo.applicationInfo.uid;
+        }
+        return 0;
+    }
+
+    /** @return {@code true} if this listener contains the currently focused task. */
+    boolean isFocused() {
+        if (mRootTaskInfo == null) {
+            return false;
+        }
+
+        if (mRootTaskInfo.isFocused) {
+            return true;
+        }
+
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            if (mChildrenTaskInfo.valueAt(i).isFocused) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        if (mRootTaskInfo == null && !taskInfo.hasParentTask()) {
+            mRootLeash = leash;
+            mRootTaskInfo = taskInfo;
+            mCallbacks.onRootTaskAppeared();
+            sendStatusChanged();
+            mSyncQueue.runInSync(t -> {
+                t.hide(mRootLeash);
+                mDimLayer =
+                        SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession);
+            });
+        } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
+            final int taskId = taskInfo.taskId;
+            mChildrenLeashes.put(taskId, leash);
+            mChildrenTaskInfo.put(taskId, taskInfo);
+            updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
+            mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible);
+            if (ENABLE_SHELL_TRANSITIONS) {
+                // Status is managed/synchronized by the transition lifecycle.
+                return;
+            }
+            sendStatusChanged();
+        } else {
+            throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+                    + "\n mRootTaskInfo: " + mRootTaskInfo);
+        }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
+        }
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        if (!taskInfo.supportsMultiWindow) {
+            // Leave split screen if the task no longer supports multi window.
+            mCallbacks.onNoLongerSupportMultiWindow();
+            return;
+        }
+        if (mRootTaskInfo.taskId == taskInfo.taskId) {
+            mRootTaskInfo = taskInfo;
+        } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
+            mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
+            mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
+                    taskInfo.isVisible);
+            if (!ENABLE_SHELL_TRANSITIONS) {
+                updateChildTaskSurface(
+                        taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
+            }
+        } else {
+            throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+                    + "\n mRootTaskInfo: " + mRootTaskInfo);
+        }
+        if (ENABLE_SHELL_TRANSITIONS) {
+            // Status is managed/synchronized by the transition lifecycle.
+            return;
+        }
+        sendStatusChanged();
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        final int taskId = taskInfo.taskId;
+        if (mRootTaskInfo.taskId == taskId) {
+            mCallbacks.onRootTaskVanished();
+            mSyncQueue.runInSync(t -> t.remove(mDimLayer));
+            mRootTaskInfo = null;
+        } else if (mChildrenTaskInfo.contains(taskId)) {
+            mChildrenTaskInfo.remove(taskId);
+            mChildrenLeashes.remove(taskId);
+            mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible);
+            if (ENABLE_SHELL_TRANSITIONS) {
+                // Status is managed/synchronized by the transition lifecycle.
+                return;
+            }
+            sendStatusChanged();
+        } else {
+            throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+                    + "\n mRootTaskInfo: " + mRootTaskInfo);
+        }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskVanished(taskInfo);
+        }
+    }
+
+    @Override
+    public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
+        if (mRootTaskInfo.taskId == taskId) {
+            b.setParent(mRootLeash);
+        } else if (mChildrenLeashes.contains(taskId)) {
+            b.setParent(mChildrenLeashes.get(taskId));
+        } else {
+            throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
+        }
+    }
+
+    void setBounds(Rect bounds, WindowContainerTransaction wct) {
+        wct.setBounds(mRootTaskInfo.token, bounds);
+    }
+
+    void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) {
+        if (!containsTask(taskId)) {
+            return;
+        }
+        wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */);
+    }
+
+    void setVisibility(boolean visible, WindowContainerTransaction wct) {
+        wct.reorder(mRootTaskInfo.token, visible /* onTop */);
+    }
+
+    void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
+            @SplitScreen.StageType int stage) {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            int taskId = mChildrenTaskInfo.keyAt(i);
+            listener.onTaskStageChanged(taskId, stage,
+                    mChildrenTaskInfo.get(taskId).isVisible);
+        }
+    }
+
+    private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo,
+            SurfaceControl leash, boolean firstAppeared) {
+        final Point taskPositionInParent = taskInfo.positionInParent;
+        mSyncQueue.runInSync(t -> {
+            t.setWindowCrop(leash, null);
+            t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
+            if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) {
+                t.setAlpha(leash, 1f);
+                t.setMatrix(leash, 1, 0, 0, 1);
+                t.show(leash);
+            }
+        });
+    }
+
+    private void sendStatusChanged() {
+        mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0);
+    }
+
+    @Override
+    @CallSuper
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        final String childPrefix = innerPrefix + "  ";
+        pw.println(prefix + this);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java
new file mode 100644
index 0000000..62b9da6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.animation.RectEvaluator;
+import android.animation.TypeEvaluator;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controls transformations of the split screen task surfaces in response
+ * to the unfolding/folding action on foldable devices
+ */
+public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
+
+    private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
+
+    private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+    private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final UnfoldBackgroundController mBackgroundController;
+    private final Executor mExecutor;
+    private final int mExpandedTaskBarHeight;
+    private final float mWindowCornerRadiusPx;
+    private final Rect mStageBounds = new Rect();
+    private final TransactionPool mTransactionPool;
+
+    private InsetsSource mTaskbarInsetsSource;
+    private boolean mBothStagesVisible;
+
+    public StageTaskUnfoldController(@NonNull Context context,
+            @NonNull TransactionPool transactionPool,
+            @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
+            @NonNull DisplayInsetsController displayInsetsController,
+            @NonNull UnfoldBackgroundController backgroundController,
+            @NonNull Executor executor) {
+        mUnfoldProgressProvider = unfoldProgressProvider;
+        mTransactionPool = transactionPool;
+        mExecutor = executor;
+        mBackgroundController = backgroundController;
+        mDisplayInsetsController = displayInsetsController;
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+        mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+    }
+
+    /**
+     * Initializes the controller, starts listening for the external events
+     */
+    public void init() {
+        mUnfoldProgressProvider.addListener(mExecutor, this);
+        mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    /**
+     * Called when split screen task appeared
+     * @param taskInfo info for the appeared task
+     * @param leash surface leash for the appeared task
+     */
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        AnimationContext context = new AnimationContext(leash);
+        mAnimationContextByTaskId.put(taskInfo.taskId, context);
+    }
+
+    /**
+     * Called when a split screen task vanished
+     * @param taskInfo info for the vanished task
+     */
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
+        if (context != null) {
+            final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+            resetSurface(transaction, context);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+        }
+        mAnimationContextByTaskId.remove(taskInfo.taskId);
+    }
+
+    @Override
+    public void onStateChangeProgress(float progress) {
+        if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
+
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        mBackgroundController.ensureBackground(transaction);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+
+            context.mCurrentCropRect.set(RECT_EVALUATOR
+                    .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
+
+            transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
+                    .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
+        }
+
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    @Override
+    public void onStateChangeFinished() {
+        resetTransformations();
+    }
+
+    /**
+     * Called when split screen visibility changes
+     * @param bothStagesVisible true if both stages of the split screen are visible
+     */
+    public void onSplitVisibilityChanged(boolean bothStagesVisible) {
+        mBothStagesVisible = bothStagesVisible;
+        if (!bothStagesVisible) {
+            resetTransformations();
+        }
+    }
+
+    /**
+     * Called when split screen stage bounds changed
+     * @param bounds new bounds for this stage
+     */
+    public void onLayoutChanged(Rect bounds) {
+        mStageBounds.set(bounds);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    private void resetTransformations() {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            resetSurface(transaction, context);
+        }
+        mBackgroundController.removeBackground(transaction);
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
+        transaction
+                .setWindowCrop(context.mLeash, null)
+                .setCornerRadius(context.mLeash, 0.0F);
+    }
+
+    private class AnimationContext {
+        final SurfaceControl mLeash;
+        final Rect mStartCropRect = new Rect();
+        final Rect mEndCropRect = new Rect();
+        final Rect mCurrentCropRect = new Rect();
+
+        private AnimationContext(SurfaceControl leash) {
+            this.mLeash = leash;
+            update();
+        }
+
+        private void update() {
+            mStartCropRect.set(mStageBounds);
+
+            if (mTaskbarInsetsSource != null) {
+                // Only insets the cropping window with taskbar when taskbar is expanded
+                if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+                    mStartCropRect.inset(mTaskbarInsetsSource
+                            .calculateVisibleInsets(mStartCropRect));
+                }
+            }
+
+            // Offset to surface coordinates as layout bounds are in screen coordinates
+            mStartCropRect.offsetTo(0, 0);
+
+            mEndCropRect.set(mStartCropRect);
+
+            int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
+            int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
+            mStartCropRect.inset(margin, margin, margin, margin);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 29326ec..8df7cbb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -50,6 +50,7 @@
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Slog;
+import android.view.ContextThemeWrapper;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.window.SplashScreenView;
@@ -137,12 +138,14 @@
      *                                 null if failed.
      */
     void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info,
-            int taskId, Consumer<SplashScreenView> splashScreenViewConsumer) {
+            int taskId, Consumer<SplashScreenView> splashScreenViewConsumer,
+            Consumer<Runnable> uiThreadInitConsumer) {
         mSplashscreenWorkerHandler.post(() -> {
             SplashScreenView contentView;
             try {
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView");
-                contentView = makeSplashScreenContentView(context, info, suggestType);
+                contentView = makeSplashScreenContentView(context, info, suggestType,
+                        uiThreadInitConsumer);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             } catch (RuntimeException e) {
                 Slog.w(TAG, "failed creating starting window content at taskId: "
@@ -238,7 +241,7 @@
     }
 
     private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai,
-            @StartingWindowType int suggestType) {
+            @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) {
         updateDensity();
 
         getWindowAttrs(context, mTmpAttrs);
@@ -253,6 +256,7 @@
                 .setWindowBGColor(themeBGColor)
                 .overlayDrawable(legacyDrawable)
                 .chooseStyle(suggestType)
+                .setUiThreadInitConsumer(uiThreadInitConsumer)
                 .build();
     }
 
@@ -299,6 +303,11 @@
         }
     }
 
+    /** Creates the wrapper with system theme to avoid unexpected styles from app. */
+    ContextThemeWrapper createViewContextWrapper(Context appContext) {
+        return new ContextThemeWrapper(appContext, mContext.getTheme());
+    }
+
     /** The configuration of the splash screen window. */
     public static class SplashScreenWindowAttrs {
         private int mWindowBgResId = 0;
@@ -318,6 +327,7 @@
         private int mThemeColor;
         private Drawable[] mFinalIconDrawables;
         private int mFinalIconSize = mIconSize;
+        private Consumer<Runnable> mUiThreadInitTask;
 
         StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) {
             mContext = context;
@@ -339,6 +349,11 @@
             return this;
         }
 
+        StartingWindowViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) {
+            mUiThreadInitTask = uiThreadInitTask;
+            return this;
+        }
+
         SplashScreenView build() {
             Drawable iconDrawable;
             final int animationDuration;
@@ -360,7 +375,7 @@
                 createIconDrawable(iconDrawable, false);
             } else {
                 final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
-                final int densityDpi = mContext.getResources().getDisplayMetrics().densityDpi;
+                final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
                 final int scaledIconDpi =
                         (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
@@ -385,7 +400,8 @@
                 animationDuration = 0;
             }
 
-            return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration);
+            return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration,
+                    mUiThreadInitTask);
         }
 
         private class ShapeIconFactory extends BaseIconFactory {
@@ -463,7 +479,7 @@
         }
 
         private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,
-                int animationDuration) {
+                int animationDuration, Consumer<Runnable> uiThreadInitTask) {
             Drawable foreground = null;
             Drawable background = null;
             if (iconDrawable != null) {
@@ -472,13 +488,15 @@
             }
 
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "fillViewWithIcon");
-            final SplashScreenView.Builder builder = new SplashScreenView.Builder(mContext)
+            final ContextThemeWrapper wrapper = createViewContextWrapper(mContext);
+            final SplashScreenView.Builder builder = new SplashScreenView.Builder(wrapper)
                     .setBackgroundColor(mThemeColor)
                     .setOverlayDrawable(mOverlayDrawable)
                     .setIconSize(iconSize)
                     .setIconBackground(background)
                     .setCenterViewDrawable(foreground)
-                    .setAnimationDurationMillis(animationDuration);
+                    .setAnimationDurationMillis(animationDuration)
+                    .setUiThreadInitConsumer(uiThreadInitTask);
 
             if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN
                     && mTmpAttrs.mBrandingImage != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
index 951b97e..38122ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
@@ -38,6 +38,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Trace;
+import android.util.Log;
 import android.util.PathParser;
 import android.window.SplashScreenView;
 
@@ -50,6 +51,8 @@
  */
 public class SplashscreenIconDrawableFactory {
 
+    private static final String TAG = "SplashscreenIconDrawableFactory";
+
     /**
      * @return An array containing the foreground drawable at index 0 and if needed a background
      * drawable at index 1.
@@ -282,7 +285,12 @@
                     if (startListener != null) {
                         startListener.run();
                     }
-                    mAnimatableIcon.start();
+                    try {
+                        mAnimatableIcon.start();
+                    } catch (Exception ex) {
+                        Log.e(TAG, "Error while running the splash screen animated icon", ex);
+                        animation.cancel();
+                    }
                 }
 
                 @Override
@@ -304,6 +312,13 @@
             return true;
         }
 
+        @Override
+        public void stopAnimation() {
+            if (mIconAnimator != null) {
+                mIconAnimator.end();
+            }
+        }
+
         private final Callback mCallback = new Callback() {
             @Override
             public void invalidateDrawable(@NonNull Drawable who) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
index 01c9b66..76105a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
@@ -36,4 +36,12 @@
     default int getBackgroundColor(TaskInfo taskInfo) {
         return Color.BLACK;
     }
+
+    /** Set the proxy to communicate with SysUi side components. */
+    void setSysuiProxy(SysuiProxy proxy);
+
+    /** Callback to tell SysUi components execute some methods. */
+    interface SysuiProxy {
+        void requestTopUi(boolean requestTopUi, String componentTag);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index 147f5e3..979bf00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -37,7 +37,6 @@
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
-import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.os.IBinder;
 import android.os.RemoteCallback;
@@ -48,15 +47,16 @@
 import android.util.SparseArray;
 import android.view.Choreographer;
 import android.view.Display;
-import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.View;
 import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
 import android.widget.FrameLayout;
 import android.window.SplashScreenView;
 import android.window.SplashScreenView.SplashScreenViewParcelable;
 import android.window.StartingWindowInfo;
 import android.window.StartingWindowInfo.StartingWindowType;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskSnapshot;
 
 import com.android.internal.R;
@@ -115,6 +115,9 @@
     @VisibleForTesting
     final SplashscreenContentDrawer mSplashscreenContentDrawer;
     private Choreographer mChoreographer;
+    private final WindowManagerGlobal mWindowManagerGlobal;
+    private StartingSurface.SysuiProxy mSysuiProxy;
+    private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo();
 
     /**
      * @param splashScreenExecutor The thread used to control add and remove starting window.
@@ -126,6 +129,8 @@
         mSplashScreenExecutor = splashScreenExecutor;
         mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, pool);
         mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
+        mWindowManagerGlobal = WindowManagerGlobal.getInstance();
+        mDisplayManager.getDisplay(DEFAULT_DISPLAY);
     }
 
     private final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>();
@@ -137,29 +142,21 @@
     private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts =
             new SparseArray<>(1);
 
-    /** Obtain proper context for showing splash screen on the provided display. */
-    private Context getDisplayContext(Context context, int displayId) {
-        if (displayId == DEFAULT_DISPLAY) {
-            // The default context fits.
-            return context;
-        }
-
-        final Display targetDisplay = mDisplayManager.getDisplay(displayId);
-        if (targetDisplay == null) {
-            // Failed to obtain the non-default display where splash screen should be shown,
-            // lets not show at all.
-            return null;
-        }
-
-        return context.createDisplayContext(targetDisplay);
+    private Display getDisplay(int displayId) {
+        return mDisplayManager.getDisplay(displayId);
     }
 
-    private int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) {
+    int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) {
         return splashScreenThemeResId != 0
                 ? splashScreenThemeResId
                 : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource()
                         : com.android.internal.R.style.Theme_DeviceDefault_DayNight;
     }
+
+    void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) {
+        mSysuiProxy = sysuiProxy;
+    }
+
     /**
      * Called when a task need a splash screen starting window.
      *
@@ -177,7 +174,7 @@
 
         final int displayId = taskInfo.displayId;
         final int taskId = taskInfo.taskId;
-        Context context = mContext;
+
         // replace with the default theme if the application didn't set
         final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo);
         if (DEBUG_SPLASH_SCREEN) {
@@ -185,14 +182,16 @@
                     + " theme=" + Integer.toHexString(theme) + " task=" + taskInfo.taskId
                     + " suggestType=" + suggestType);
         }
-
-        // Obtain proper context to launch on the right display.
-        final Context displayContext = getDisplayContext(context, displayId);
-        if (displayContext == null) {
+        final Display display = getDisplay(displayId);
+        if (display == null) {
             // Can't show splash screen on requested display, so skip showing at all.
             return;
         }
-        context = displayContext;
+        Context context = displayId == DEFAULT_DISPLAY
+                ? mContext : mContext.createDisplayContext(display);
+        if (context == null) {
+            return;
+        }
         if (theme != context.getThemeResId()) {
             try {
                 context = context.createPackageContextAsUser(activityInfo.packageName,
@@ -303,7 +302,8 @@
         // Record whether create splash screen view success, notify to current thread after
         // create splash screen view finished.
         final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
-        final FrameLayout rootLayout = new FrameLayout(context);
+        final FrameLayout rootLayout = new FrameLayout(
+                mSplashscreenContentDrawer.createViewContextWrapper(context));
         rootLayout.setPadding(0, 0, 0, 0);
         rootLayout.setFitsSystemWindows(false);
         final Runnable setViewSynchronized = () -> {
@@ -328,12 +328,13 @@
             }
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         };
+        if (mSysuiProxy != null) {
+            mSysuiProxy.requestTopUi(true, TAG);
+        }
         mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId,
-                viewSupplier::setView);
-
+                viewSupplier::setView, viewSupplier::setUiThreadInitTask);
         try {
-            final WindowManager wm = context.getSystemService(WindowManager.class);
-            if (addWindow(taskId, appToken, rootLayout, wm, params, suggestType)) {
+            if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
                 // We use the splash screen worker thread to create SplashScreenView while adding
                 // the window, as otherwise Choreographer#doFrame might be delayed on this thread.
                 // And since Choreographer#doFrame won't happen immediately after adding the window,
@@ -366,6 +367,7 @@
     private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> {
         private SplashScreenView mView;
         private boolean mIsViewSet;
+        private Runnable mUiThreadInitTask;
         void setView(SplashScreenView view) {
             synchronized (this) {
                 mView = view;
@@ -374,6 +376,12 @@
             }
         }
 
+        void setUiThreadInitTask(Runnable initTask) {
+            synchronized (this) {
+                mUiThreadInitTask = initTask;
+            }
+        }
+
         @Override
         public @Nullable SplashScreenView get() {
             synchronized (this) {
@@ -383,6 +391,10 @@
                     } catch (InterruptedException ignored) {
                     }
                 }
+                if (mUiThreadInitTask != null) {
+                    mUiThreadInitTask.run();
+                    mUiThreadInitTask = null;
+                }
                 return mView;
             }
         }
@@ -446,12 +458,13 @@
     /**
      * Called when the content of a task is ready to show, starting window can be removed.
      */
-    public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
         if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) {
-            Slog.d(TAG, "Task start finish, remove starting surface for task " + taskId);
+            Slog.d(TAG, "Task start finish, remove starting surface for task "
+                    + removalInfo.taskId);
         }
-        removeWindowSynced(taskId, leash, frame, playRevealAnimation);
+        removeWindowSynced(removalInfo);
+
     }
 
     /**
@@ -505,15 +518,17 @@
             Slog.v(TAG, reason + "the splash screen. Releasing SurfaceControlViewHost for task:"
                     + taskId);
         }
-        viewHost.getView().post(viewHost::release);
+        SplashScreenView.releaseIconHost(viewHost);
     }
 
-    protected boolean addWindow(int taskId, IBinder appToken, View view, WindowManager wm,
+    protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
             WindowManager.LayoutParams params, @StartingWindowType int suggestType) {
         boolean shouldSaveView = true;
+        final Context context = view.getContext();
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
-            wm.addView(view, params);
+            mWindowManagerGlobal.addView(view, params, display,
+                    null /* parentWindow */, context.getUserId());
         } catch (WindowManager.BadTokenException e) {
             // ignore
             Slog.w(TAG, appToken + " already running, starting window not displayed. "
@@ -521,9 +536,9 @@
             shouldSaveView = false;
         } finally {
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-            if (view != null && view.getParent() == null) {
+            if (view.getParent() == null) {
                 Slog.w(TAG, "view not successfully added to wm, removing view");
-                wm.removeViewImmediate(view);
+                mWindowManagerGlobal.removeView(view, true /* immediate */);
                 shouldSaveView = false;
             }
         }
@@ -542,7 +557,8 @@
     }
 
     private void removeWindowNoAnimate(int taskId) {
-        removeWindowSynced(taskId, null, null, false);
+        mTmpRemovalInfo.taskId = taskId;
+        removeWindowSynced(mTmpRemovalInfo);
     }
 
     void onImeDrawnOnTask(int taskId) {
@@ -554,8 +570,8 @@
         mStartingWindowRecords.remove(taskId);
     }
 
-    protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo) {
+        final int taskId = removalInfo.taskId;
         final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
         if (record != null) {
             if (record.mDecorView != null) {
@@ -566,9 +582,9 @@
                     if (record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
                         removeWindowInner(record.mDecorView, false);
                     } else {
-                        if (playRevealAnimation) {
+                        if (removalInfo.playRevealAnimation) {
                             mSplashscreenContentDrawer.applyExitAnimation(record.mContentView,
-                                    leash, frame,
+                                    removalInfo.windowAnimationLeash, removalInfo.mainFrame,
                                     () -> removeWindowInner(record.mDecorView, true));
                         } else {
                             // the SplashScreenView has been copied to client, hide the view to skip
@@ -588,19 +604,19 @@
                     Slog.v(TAG, "Removing task snapshot window for " + taskId);
                 }
                 record.mTaskSnapshotWindow.scheduleRemove(
-                        () -> mStartingWindowRecords.remove(taskId));
+                        () -> mStartingWindowRecords.remove(taskId), removalInfo.deferRemoveForIme);
             }
         }
     }
 
     private void removeWindowInner(View decorView, boolean hideView) {
+        if (mSysuiProxy != null) {
+            mSysuiProxy.requestTopUi(false, TAG);
+        }
         if (hideView) {
             decorView.setVisibility(View.GONE);
         }
-        final WindowManager wm = decorView.getContext().getSystemService(WindowManager.class);
-        if (wm != null) {
-            wm.removeView(decorView);
-        }
+        mWindowManagerGlobal.removeView(decorView, false /* immediate */);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index dee21b0..99644f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -28,16 +28,14 @@
 import android.app.TaskInfo;
 import android.content.Context;
 import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.util.Slog;
 import android.util.SparseIntArray;
-import android.view.SurfaceControl;
 import android.window.StartingWindowInfo;
 import android.window.StartingWindowInfo.StartingWindowType;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskOrganizer;
 import android.window.TaskSnapshot;
 
@@ -68,7 +66,7 @@
 public class StartingWindowController implements RemoteCallable<StartingWindowController> {
     private static final String TAG = StartingWindowController.class.getSimpleName();
 
-    public static final boolean DEBUG_SPLASH_SCREEN = Build.isDebuggable();
+    public static final boolean DEBUG_SPLASH_SCREEN = false;
     public static final boolean DEBUG_TASK_SNAPSHOT = false;
 
     private static final long TASK_BG_COLOR_RETAIN_TIME_MS = 5000;
@@ -134,7 +132,7 @@
                 mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, appToken,
                         suggestionType);
             } else if (suggestionType == STARTING_WINDOW_TYPE_SNAPSHOT) {
-                final TaskSnapshot snapshot = windowInfo.mTaskSnapshot;
+                final TaskSnapshot snapshot = windowInfo.taskSnapshot;
                 mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, appToken,
                         snapshot);
             }
@@ -186,13 +184,12 @@
     /**
      * Called when the content of a task is ready to show, starting window can be removed.
      */
-    public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
         mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.removeStartingWindow(
-                taskId, leash, frame, playRevealAnimation));
+                removalInfo));
         mSplashScreenExecutor.executeDelayed(() -> {
             synchronized (mTaskBackgroundColors) {
-                mTaskBackgroundColors.delete(taskId);
+                mTaskBackgroundColors.delete(removalInfo.taskId);
             }
         }, TASK_BG_COLOR_RETAIN_TIME_MS);
     }
@@ -224,6 +221,11 @@
             return color != Color.TRANSPARENT
                     ? color : SplashscreenContentDrawer.getSystemBGColor();
         }
+
+        @Override
+        public void setSysuiProxy(SysuiProxy proxy) {
+            mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.setSysuiProxy(proxy));
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index dfb1ae3..3e88c46 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -130,7 +130,6 @@
 
     private final Window mWindow;
     private final Runnable mClearWindowHandler;
-    private final long mDelayRemovalTime;
     private final ShellExecutor mSplashScreenExecutor;
     private final SurfaceControl mSurfaceControl;
     private final IWindowSession mSession;
@@ -210,7 +209,7 @@
         final SurfaceControl surfaceControl = new SurfaceControl();
         final ClientWindowFrames tmpFrames = new ClientWindowFrames();
 
-        final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0];
+        final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0];
         final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration();
 
         final TaskDescription taskDescription;
@@ -221,22 +220,19 @@
             taskDescription.setBackgroundColor(WHITE);
         }
 
-        final long delayRemovalTime = snapshot.hasImeSurface() ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE
-                : DELAY_REMOVAL_TIME_GENERAL;
-
         final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow(
                 surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance,
                 windowFlags, windowPrivateFlags, taskBounds, orientation, activityType,
-                delayRemovalTime, topWindowInsetsState, clearWindowHandler, splashScreenExecutor);
+                topWindowInsetsState, clearWindowHandler, splashScreenExecutor);
         final Window window = snapshotSurface.mWindow;
 
-        final InsetsState mTmpInsetsState = new InsetsState();
+        final InsetsState tmpInsetsState = new InsetsState();
         final InputChannel tmpInputChannel = new InputChannel();
 
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay");
             final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId,
-                    mTmpInsetsState, tmpInputChannel, mTmpInsetsState, mTempControls);
+                    info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls);
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             if (res < 0) {
                 Slog.w(TAG, "Failed to add snapshot starting window res=" + res);
@@ -249,8 +245,8 @@
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
             session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, -1,
-                    tmpFrames, tmpMergedConfiguration, surfaceControl, mTmpInsetsState,
-                    mTempControls, TMP_SURFACE_SIZE);
+                    tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
+                    tmpControls, TMP_SURFACE_SIZE);
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         } catch (RemoteException e) {
             snapshotSurface.clearWindowSynced();
@@ -265,9 +261,8 @@
     public TaskSnapshotWindow(SurfaceControl surfaceControl,
             TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription,
             int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds,
-            int currentOrientation, int activityType, long delayRemovalTime,
-            InsetsState topWindowInsetsState, Runnable clearWindowHandler,
-            ShellExecutor splashScreenExecutor) {
+            int currentOrientation, int activityType, InsetsState topWindowInsetsState,
+            Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) {
         mSplashScreenExecutor = splashScreenExecutor;
         mSession = WindowManagerGlobal.getWindowSession();
         mWindow = new Window();
@@ -283,7 +278,6 @@
         mStatusBarColor = taskDescription.getStatusBarColor();
         mOrientationOnCreation = currentOrientation;
         mActivityType = activityType;
-        mDelayRemovalTime = delayRemovalTime;
         mTransaction = new SurfaceControl.Transaction();
         mClearWindowHandler = clearWindowHandler;
         mHasImeSurface = snapshot.hasImeSurface();
@@ -294,7 +288,7 @@
     }
 
     boolean hasImeSurface() {
-        return mHasImeSurface;
+	return mHasImeSurface;
     }
 
     /**
@@ -314,7 +308,7 @@
         mSystemBarBackgroundPainter.drawNavigationBarBackground(c);
     }
 
-    void scheduleRemove(Runnable onRemove) {
+    void scheduleRemove(Runnable onRemove, boolean deferRemoveForIme) {
         // Show the latest content as soon as possible for unlocking to home.
         if (mActivityType == ACTIVITY_TYPE_HOME) {
             removeImmediately();
@@ -329,9 +323,12 @@
             TaskSnapshotWindow.this.removeImmediately();
             onRemove.run();
         };
-        mSplashScreenExecutor.executeDelayed(mScheduledRunnable, mDelayRemovalTime);
+        final long delayRemovalTime = mHasImeSurface && deferRemoveForIme
+                ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE
+                : DELAY_REMOVAL_TIME_GENERAL;
+        mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime);
         if (DEBUG) {
-            Slog.d(TAG, "Defer removing snapshot surface in " + mDelayRemovalTime);
+            Slog.d(TAG, "Defer removing snapshot surface in " + delayRemovalTime);
         }
     }
 
@@ -362,7 +359,7 @@
 
     static Rect getSystemBarInsets(Rect frame, InsetsState state) {
         return state.calculateInsets(frame, WindowInsets.Type.systemBars(),
-                false /* ignoreVisibility */);
+                false /* ignoreVisibility */).toRect();
     }
 
     private void drawSnapshot() {
@@ -382,6 +379,7 @@
 
         // In case window manager leaks us, make sure we don't retain the snapshot.
         mSnapshot = null;
+        mSurfaceControl.release();
     }
 
     private void drawSizeMatchSnapshot() {
@@ -449,6 +447,7 @@
             mTransaction.setBuffer(mSurfaceControl, background);
         }
         mTransaction.apply();
+        childSurfaceControl.release();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
index 848eff4..bde2b5f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
@@ -71,23 +71,13 @@
                     + " topIsHome:" + topIsHome);
         }
 
-        final int visibleSplashScreenType = legacySplashScreen
-                ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
-                : STARTING_WINDOW_TYPE_SPLASH_SCREEN;
-
         if (!topIsHome) {
-            if (!processRunning) {
+            if (!processRunning || newTask || (taskSwitch && !activityCreated)) {
                 return useEmptySplashScreen
                         ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
-                        : visibleSplashScreenType;
-            }
-            if (newTask) {
-                return useEmptySplashScreen
-                        ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
-                        : visibleSplashScreenType;
-            }
-            if (taskSwitch && !activityCreated) {
-                return visibleSplashScreenType;
+                        : legacySplashScreen
+                                ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
+                                : STARTING_WINDOW_TYPE_SPLASH_SCREEN;
             }
         }
         if (taskSwitch && allowTaskSnapshot) {
@@ -107,7 +97,7 @@
      * rotation must be the same).
      */
     private boolean isSnapshotCompatible(StartingWindowInfo windowInfo) {
-        final TaskSnapshot snapshot = windowInfo.mTaskSnapshot;
+        final TaskSnapshot snapshot = windowInfo.taskSnapshot;
         if (snapshot == null) {
             if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) {
                 Slog.d(TAG, "isSnapshotCompatible no snapshot " + windowInfo.taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index c6fb5af..7abda99 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -16,18 +16,38 @@
 
 package com.android.wm.shell.transition;
 
+import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
+import static android.app.ActivityOptions.ANIM_CUSTOM;
+import static android.app.ActivityOptions.ANIM_NONE;
+import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
+import static android.app.ActivityOptions.ANIM_SCALE_UP;
+import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
+import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
 import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION;
+import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static android.window.TransitionInfo.isIndependent;
+
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -35,25 +55,37 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Point;
 import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
 import android.os.IBinder;
+import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.view.Choreographer;
 import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.WindowManager;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
 import android.view.animation.Transformation;
 import android.window.TransitionInfo;
+import android.window.TransitionMetrics;
 import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.AttributeCache;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.util.CounterRotator;
 
 import java.util.ArrayList;
 
@@ -61,33 +93,179 @@
 public class DefaultTransitionHandler implements Transitions.TransitionHandler {
     private static final int MAX_ANIMATION_DURATION = 3000;
 
+    /**
+     * Restrict ability of activities overriding transition animation in a way such that
+     * an activity can do it only when the transition happens within a same task.
+     *
+     * @see android.app.Activity#overridePendingTransition(int, int)
+     */
+    private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY =
+            "persist.wm.disable_custom_task_animation";
+
+    /**
+     * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY
+     */
+    static boolean sDisableCustomTaskAnimationProperty =
+            SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true);
+
     private final TransactionPool mTransactionPool;
+    private final DisplayController mDisplayController;
+    private final Context mContext;
     private final ShellExecutor mMainExecutor;
     private final ShellExecutor mAnimExecutor;
     private final TransitionAnimation mTransitionAnimation;
 
+    private final SurfaceSession mSurfaceSession = new SurfaceSession();
+
     /** Keeps track of the currently-running animations associated with each transition. */
     private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
 
     private final Rect mInsets = new Rect(0, 0, 0, 0);
     private float mTransitionAnimationScaleSetting = 1.0f;
 
-    DefaultTransitionHandler(@NonNull TransactionPool transactionPool, Context context,
+    private final int mCurrentUserId;
+
+    private ScreenRotationAnimation mRotationAnimation;
+
+    DefaultTransitionHandler(@NonNull DisplayController displayController,
+            @NonNull TransactionPool transactionPool, Context context,
             @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
+        mDisplayController = displayController;
         mTransactionPool = transactionPool;
+        mContext = context;
         mMainExecutor = mainExecutor;
         mAnimExecutor = animExecutor;
         mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG);
+        mCurrentUserId = UserHandle.myUserId();
 
         AttributeCache.init(context);
     }
 
+    @VisibleForTesting
+    static boolean isRotationSeamless(@NonNull TransitionInfo info,
+            DisplayController displayController) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                "Display is changing, check if it should be seamless.");
+        boolean checkedDisplayLayout = false;
+        boolean hasTask = false;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+
+            // Only look at changing things. showing/hiding don't need to rotate.
+            if (change.getMode() != TRANSIT_CHANGE) continue;
+
+            // This container isn't rotating, so we can ignore it.
+            if (change.getEndRotation() == change.getStartRotation()) continue;
+
+            if ((change.getFlags() & FLAG_IS_DISPLAY) != 0) {
+                // In the presence of System Alert windows we can not seamlessly rotate.
+                if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                            "  display has system alert windows, so not seamless.");
+                    return false;
+                }
+            } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+                if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                            "  wallpaper is participating but isn't seamless.");
+                    return false;
+                }
+            } else if (change.getTaskInfo() != null) {
+                hasTask = true;
+                // We only enable seamless rotation if all the visible task windows requested it.
+                if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                            "  task %s isn't requesting seamless, so not seamless.",
+                            change.getTaskInfo().taskId);
+                    return false;
+                }
+
+                // This is the only way to get display-id currently, so we will check display
+                // capabilities here
+                if (!checkedDisplayLayout) {
+                    // only need to check display once.
+                    checkedDisplayLayout = true;
+                    final DisplayLayout displayLayout = displayController.getDisplayLayout(
+                            change.getTaskInfo().displayId);
+                    // For the upside down rotation we don't rotate seamlessly as the navigation
+                    // bar moves position. Note most apps (using orientation:sensor or user as
+                    // opposed to fullSensor) will not enter the reverse portrait orientation, so
+                    // actually the orientation won't change at all.
+                    int upsideDownRotation = displayLayout.getUpsideDownRotation();
+                    if (change.getStartRotation() == upsideDownRotation
+                            || change.getEndRotation() == upsideDownRotation) {
+                        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                                "  rotation involves upside-down portrait, so not seamless.");
+                        return false;
+                    }
+
+                    // If the navigation bar can't change sides, then it will jump when we change
+                    // orientations and we don't rotate seamlessly - unless that is allowed, eg.
+                    // with gesture navigation where the navbar is low-profile enough that this
+                    // isn't very noticeable.
+                    if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving()
+                            && (!(displayLayout.navigationBarCanMove()
+                                    && (change.getStartAbsBounds().width()
+                                            != change.getStartAbsBounds().height())))) {
+                        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                                "  nav bar changes sides, so not seamless.");
+                        return false;
+                    }
+                }
+            }
+        }
+
+        // ROTATION_ANIMATION_SEAMLESS can only be requested by task.
+        if (hasTask) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Rotation IS seamless.");
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Gets the rotation animation for the topmost task. Assumes that seamless is checked
+     * elsewhere, so it will default SEAMLESS to ROTATE.
+     */
+    private int getRotationAnimation(@NonNull TransitionInfo info) {
+        // Traverse in top-to-bottom order so that the first task is top-most
+        for (int i = 0; i < info.getChanges().size(); ++i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+
+            // Only look at changing things. showing/hiding don't need to rotate.
+            if (change.getMode() != TRANSIT_CHANGE) continue;
+
+            // This container isn't rotating, so we can ignore it.
+            if (change.getEndRotation() == change.getStartRotation()) continue;
+
+            if (change.getTaskInfo() != null) {
+                final int anim = change.getRotationAnimation();
+                if (anim == ROTATION_ANIMATION_UNSPECIFIED
+                        // Fallback animation for seamless should also be default.
+                        || anim == ROTATION_ANIMATION_SEAMLESS) {
+                    return ROTATION_ANIMATION_ROTATE;
+                }
+                return anim;
+            }
+        }
+        return ROTATION_ANIMATION_ROTATE;
+    }
+
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                 "start default transition animation, info = %s", info);
+        // If keyguard goes away, we should loadKeyguardExitAnimation. Otherwise this just
+        // immediately finishes since there is no animation for screen-wake.
+        if (info.getType() == WindowManager.TRANSIT_WAKE && !info.isKeyguardGoingAway()) {
+            startTransaction.apply();
+            finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+            return true;
+        }
+
         if (mAnimations.containsKey(transition)) {
             throw new IllegalStateException("Got a duplicate startAnimation call for "
                     + transition);
@@ -95,21 +273,78 @@
         final ArrayList<Animator> animations = new ArrayList<>();
         mAnimations.put(transition, animations);
 
+        final ArrayMap<WindowContainerToken, CounterRotator> counterRotators = new ArrayMap<>();
+
         final Runnable onAnimFinish = () -> {
             if (!animations.isEmpty()) return;
+
+            for (int i = 0; i < counterRotators.size(); ++i) {
+                counterRotators.valueAt(i).cleanUp(info.getRootLeash());
+            }
+            counterRotators.clear();
+
+            if (mRotationAnimation != null) {
+                mRotationAnimation.kill();
+                mRotationAnimation = null;
+            }
+
             mAnimations.remove(transition);
             finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
         };
+
+        final int wallpaperTransit = getWallpaperTransitType(info);
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
+
+            if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) {
+                int rotateDelta = change.getEndRotation() - change.getStartRotation();
+                int displayW = change.getEndAbsBounds().width();
+                int displayH = change.getEndAbsBounds().height();
+                if (info.getType() == TRANSIT_CHANGE) {
+                    boolean isSeamless = isRotationSeamless(info, mDisplayController);
+                    final int anim = getRotationAnimation(info);
+                    if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) {
+                        mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession,
+                                mTransactionPool, startTransaction, change, info.getRootLeash());
+                        mRotationAnimation.startAnimation(animations, onAnimFinish,
+                                mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor);
+                        continue;
+                    }
+                } else {
+                    // opening/closing an app into a new orientation. Counter-rotate all
+                    // "going-away" things since they are still in the old orientation.
+                    for (int j = info.getChanges().size() - 1; j >= 0; --j) {
+                        final TransitionInfo.Change innerChange = info.getChanges().get(j);
+                        if (!Transitions.isClosingType(innerChange.getMode())
+                                || !isIndependent(innerChange, info)
+                                || innerChange.getParent() == null) {
+                            continue;
+                        }
+                        CounterRotator crot = counterRotators.get(innerChange.getParent());
+                        if (crot == null) {
+                            crot = new CounterRotator();
+                            crot.setup(startTransaction,
+                                    info.getChange(innerChange.getParent()).getLeash(),
+                                    rotateDelta, displayW, displayH);
+                            if (crot.getSurface() != null) {
+                                int layer = info.getChanges().size() - j;
+                                startTransaction.setLayer(crot.getSurface(), layer);
+                            }
+                            counterRotators.put(innerChange.getParent(), crot);
+                        }
+                        crot.addChild(startTransaction, innerChange.getLeash());
+                    }
+                }
+            }
+
             if (change.getMode() == TRANSIT_CHANGE) {
                 // No default animation for this, so just update bounds/position.
-                t.setPosition(change.getLeash(),
+                startTransaction.setPosition(change.getLeash(),
                         change.getEndAbsBounds().left - change.getEndRelOffset().x,
                         change.getEndAbsBounds().top - change.getEndRelOffset().y);
                 if (change.getTaskInfo() != null) {
                     // Skip non-tasks since those usually have null bounds.
-                    t.setWindowCrop(change.getLeash(),
+                    startTransaction.setWindowCrop(change.getLeash(),
                             change.getEndAbsBounds().width(), change.getEndAbsBounds().height());
                 }
             }
@@ -117,12 +352,18 @@
             // Don't animate anything that isn't independent.
             if (!TransitionInfo.isIndependent(change, info)) continue;
 
-            Animation a = loadAnimation(info.getType(), info.getFlags(), change);
+            Animation a = loadAnimation(info, change, wallpaperTransit);
             if (a != null) {
-                startAnimInternal(animations, a, change.getLeash(), onAnimFinish);
+                startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
+                        mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */);
+
+                if (info.getAnimationOptions() != null) {
+                    attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions());
+                }
             }
         }
-        t.apply();
+        startTransaction.apply();
+        TransitionMetrics.getInstance().reportAnimationStart(transition);
         // run finish now in-case there are no animations
         onAnimFinish.run();
         return true;
@@ -141,87 +382,134 @@
     }
 
     @Nullable
-    private Animation loadAnimation(int type, int flags, TransitionInfo.Change change) {
-        // TODO(b/178678389): It should handle more type animation here
+    private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change,
+            int wallpaperTransit) {
         Animation a = null;
 
-        final boolean isOpening = Transitions.isOpeningType(type);
+        final int type = info.getType();
+        final int flags = info.getFlags();
         final int changeMode = change.getMode();
         final int changeFlags = change.getFlags();
+        final boolean isOpeningType = Transitions.isOpeningType(type);
+        final boolean enter = Transitions.isOpeningType(changeMode);
+        final boolean isTask = change.getTaskInfo() != null;
+        final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+        final int overrideType = options != null ? options.getType() : ANIM_NONE;
+        final boolean canCustomContainer = isTask ? !sDisableCustomTaskAnimationProperty : true;
 
-        if (type == TRANSIT_RELAUNCH) {
-            a = mTransitionAnimation.createRelaunchAnimation(
-                    change.getStartAbsBounds(), mInsets, change.getEndAbsBounds());
-        } else if (type == TRANSIT_KEYGUARD_GOING_AWAY) {
+        if (info.isKeyguardGoingAway()) {
             a = mTransitionAnimation.loadKeyguardExitAnimation(flags,
                     (changeFlags & FLAG_SHOW_WALLPAPER) != 0);
         } else if (type == TRANSIT_KEYGUARD_UNOCCLUDE) {
             a = mTransitionAnimation.loadKeyguardUnoccludeAnimation();
-        } else if (changeMode == TRANSIT_OPEN && isOpening) {
-            if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
-                // This received a transferred starting window, so don't animate
-                return null;
-            }
-
-            if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
-                a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */);
-            } else if (change.getTaskInfo() != null) {
-                a = mTransitionAnimation.loadDefaultAnimationAttr(
-                        R.styleable.WindowAnimation_taskOpenEnterAnimation);
+        } else if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
+            if (isOpeningType) {
+                a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter);
             } else {
-                a = mTransitionAnimation.loadDefaultAnimationRes(
-                        (changeFlags & FLAG_TRANSLUCENT) == 0
-                        ? R.anim.activity_open_enter : R.anim.activity_translucent_open_enter);
-            }
-        } else if (changeMode == TRANSIT_TO_FRONT && isOpening) {
-            if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
-                // This received a transferred starting window, so don't animate
-                return null;
-            }
-
-            if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
-                a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */);
-            } else {
-                a = mTransitionAnimation.loadDefaultAnimationAttr(
-                        R.styleable.WindowAnimation_taskToFrontEnterAnimation);
-            }
-        } else if (changeMode == TRANSIT_CLOSE && !isOpening) {
-            if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
-                a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */);
-            } else if (change.getTaskInfo() != null) {
-                a = mTransitionAnimation.loadDefaultAnimationAttr(
-                        R.styleable.WindowAnimation_taskCloseExitAnimation);
-            } else {
-                a = mTransitionAnimation.loadDefaultAnimationRes(
-                        (changeFlags & FLAG_TRANSLUCENT) == 0
-                        ? R.anim.activity_close_exit : R.anim.activity_translucent_close_exit);
-            }
-        } else if (changeMode == TRANSIT_TO_BACK && !isOpening) {
-            if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) {
-                a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */);
-            } else {
-                a = mTransitionAnimation.loadDefaultAnimationAttr(
-                        R.styleable.WindowAnimation_taskToBackExitAnimation);
+                a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter);
             }
         } else if (changeMode == TRANSIT_CHANGE) {
             // In the absence of a specific adapter, we just want to keep everything stationary.
             a = new AlphaAnimation(1.f, 1.f);
             a.setDuration(TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION);
+        } else if (type == TRANSIT_RELAUNCH) {
+            a = mTransitionAnimation.createRelaunchAnimation(
+                    change.getEndAbsBounds(), mInsets, change.getEndAbsBounds());
+        } else if (overrideType == ANIM_CUSTOM
+                && (canCustomContainer || options.getOverrideTaskTransition())) {
+            a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter
+                    ? options.getEnterResId() : options.getExitResId());
+        } else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) {
+            a = mTransitionAnimation.loadCrossProfileAppEnterAnimation();
+        } else if (overrideType == ANIM_CLIP_REVEAL) {
+            a = mTransitionAnimation.createClipRevealAnimationLocked(type, wallpaperTransit, enter,
+                    change.getEndAbsBounds(), change.getEndAbsBounds(),
+                    options.getTransitionBounds());
+        } else if (overrideType == ANIM_SCALE_UP) {
+            a = mTransitionAnimation.createScaleUpAnimationLocked(type, wallpaperTransit, enter,
+                    change.getEndAbsBounds(), options.getTransitionBounds());
+        } else if (overrideType == ANIM_THUMBNAIL_SCALE_UP
+                || overrideType == ANIM_THUMBNAIL_SCALE_DOWN) {
+            final boolean scaleUp = overrideType == ANIM_THUMBNAIL_SCALE_UP;
+            a = mTransitionAnimation.createThumbnailEnterExitAnimationLocked(enter, scaleUp,
+                    change.getEndAbsBounds(), type, wallpaperTransit, options.getThumbnail(),
+                    options.getTransitionBounds());
+        } else if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0 && isOpeningType) {
+            // This received a transferred starting window, so don't animate
+            return null;
+        } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) {
+            a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                    ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation
+                    : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation);
+        } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) {
+            a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                    ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation
+                    : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation);
+        } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) {
+            a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                    ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation
+                    : R.styleable.WindowAnimation_wallpaperOpenExitAnimation);
+        } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) {
+            a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                    ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation
+                    : R.styleable.WindowAnimation_wallpaperCloseExitAnimation);
+        } else if (type == TRANSIT_OPEN) {
+            if (isTask) {
+                a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                        ? R.styleable.WindowAnimation_taskOpenEnterAnimation
+                        : R.styleable.WindowAnimation_taskOpenExitAnimation);
+            } else {
+                if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) {
+                    a = mTransitionAnimation.loadDefaultAnimationRes(
+                            R.anim.activity_translucent_open_enter);
+                } else {
+                    a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                            ? R.styleable.WindowAnimation_activityOpenEnterAnimation
+                            : R.styleable.WindowAnimation_activityOpenExitAnimation);
+                }
+            }
+        } else if (type == TRANSIT_TO_FRONT) {
+            a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                    ? R.styleable.WindowAnimation_taskToFrontEnterAnimation
+                    : R.styleable.WindowAnimation_taskToFrontExitAnimation);
+        } else if (type == TRANSIT_CLOSE) {
+            if (isTask) {
+                a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                        ? R.styleable.WindowAnimation_taskCloseEnterAnimation
+                        : R.styleable.WindowAnimation_taskCloseExitAnimation);
+            } else {
+                if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
+                    a = mTransitionAnimation.loadDefaultAnimationRes(
+                            R.anim.activity_translucent_close_exit);
+                } else {
+                    a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                            ? R.styleable.WindowAnimation_activityCloseEnterAnimation
+                            : R.styleable.WindowAnimation_activityCloseExitAnimation);
+                }
+            }
+        } else if (type == TRANSIT_TO_BACK) {
+            a = mTransitionAnimation.loadDefaultAnimationAttr(enter
+                    ? R.styleable.WindowAnimation_taskToBackEnterAnimation
+                    : R.styleable.WindowAnimation_taskToBackExitAnimation);
         }
 
         if (a != null) {
-            Rect start = change.getStartAbsBounds();
-            Rect end = change.getEndAbsBounds();
+            if (!a.isInitialized()) {
+                Rect end = change.getEndAbsBounds();
+                a.initialize(end.width(), end.height(), end.width(), end.height());
+            }
             a.restrictDuration(MAX_ANIMATION_DURATION);
-            a.initialize(end.width(), end.height(), start.width(), start.height());
             a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
         }
         return a;
     }
 
-    private void startAnimInternal(@NonNull ArrayList<Animator> animations, @NonNull Animation anim,
-            @NonNull SurfaceControl leash, @NonNull Runnable finishCallback) {
-        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+    static void startSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+            @NonNull Animation anim, @NonNull SurfaceControl leash,
+            @NonNull Runnable finishCallback, @NonNull TransactionPool pool,
+            @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor,
+            @Nullable Point position) {
+        final SurfaceControl.Transaction transaction = pool.acquire();
         final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
         final Transformation transformation = new Transformation();
         final float[] matrix = new float[9];
@@ -231,14 +519,16 @@
         va.addUpdateListener(animation -> {
             final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
 
-            applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix);
+            applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
+                    position);
         });
 
         final Runnable finisher = () -> {
-            applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix);
+            applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
+                    position);
 
-            mTransactionPool.release(transaction);
-            mMainExecutor.execute(() -> {
+            pool.release(transaction);
+            mainExecutor.execute(() -> {
                 animations.remove(va);
                 finishCallback.run();
             });
@@ -255,12 +545,116 @@
             }
         });
         animations.add(va);
-        mAnimExecutor.execute(va::start);
+        animExecutor.execute(va::start);
+    }
+
+    private void attachThumbnail(@NonNull ArrayList<Animator> animations,
+            @NonNull Runnable finishCallback, TransitionInfo.Change change,
+            TransitionInfo.AnimationOptions options) {
+        final boolean isTask = change.getTaskInfo() != null;
+        final boolean isOpen = Transitions.isOpeningType(change.getMode());
+        final boolean isClose = Transitions.isClosingType(change.getMode());
+        if (isOpen) {
+            if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) {
+                attachCrossProfileThunmbnailAnimation(animations, finishCallback, change);
+            } else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) {
+                attachThumbnailAnimation(animations, finishCallback, change, options);
+            }
+        } else if (isClose && options.getType() == ANIM_THUMBNAIL_SCALE_DOWN) {
+            attachThumbnailAnimation(animations, finishCallback, change, options);
+        }
+    }
+
+    private void attachCrossProfileThunmbnailAnimation(@NonNull ArrayList<Animator> animations,
+            @NonNull Runnable finishCallback, TransitionInfo.Change change) {
+        final int thumbnailDrawableRes = change.getTaskInfo().userId == mCurrentUserId
+                ? R.drawable.ic_account_circle : R.drawable.ic_corp_badge;
+        final Rect bounds = change.getEndAbsBounds();
+        final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail(
+                thumbnailDrawableRes, bounds);
+        if (thumbnail == null) {
+            return;
+        }
+
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+                change.getLeash(), thumbnail, transaction);
+        final Animation a =
+                mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds);
+        if (a == null) {
+            return;
+        }
+
+        final Runnable finisher = () -> {
+            wt.destroy(transaction);
+            mTransactionPool.release(transaction);
+
+            finishCallback.run();
+        };
+        a.restrictDuration(MAX_ANIMATION_DURATION);
+        a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
+                mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top));
+    }
+
+    private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations,
+            @NonNull Runnable finishCallback, TransitionInfo.Change change,
+            TransitionInfo.AnimationOptions options) {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+                change.getLeash(), options.getThumbnail(), transaction);
+        final Rect bounds = change.getEndAbsBounds();
+        final int orientation = mContext.getResources().getConfiguration().orientation;
+        final Animation a = mTransitionAnimation.createThumbnailAspectScaleAnimationLocked(bounds,
+                mInsets, options.getThumbnail(), orientation, null /* startRect */,
+                options.getTransitionBounds(), options.getType() == ANIM_THUMBNAIL_SCALE_UP);
+
+        final Runnable finisher = () -> {
+            wt.destroy(transaction);
+            mTransactionPool.release(transaction);
+
+            finishCallback.run();
+        };
+        a.restrictDuration(MAX_ANIMATION_DURATION);
+        a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
+                mMainExecutor, mAnimExecutor, null /* position */);
+    }
+
+    private static int getWallpaperTransitType(TransitionInfo info) {
+        boolean hasOpenWallpaper = false;
+        boolean hasCloseWallpaper = false;
+
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) {
+                if (Transitions.isOpeningType(change.getMode())) {
+                    hasOpenWallpaper = true;
+                } else if (Transitions.isClosingType(change.getMode())) {
+                    hasCloseWallpaper = true;
+                }
+            }
+        }
+
+        if (hasOpenWallpaper && hasCloseWallpaper) {
+            return Transitions.isOpeningType(info.getType())
+                    ? WALLPAPER_TRANSITION_INTRA_OPEN : WALLPAPER_TRANSITION_INTRA_CLOSE;
+        } else if (hasOpenWallpaper) {
+            return WALLPAPER_TRANSITION_OPEN;
+        } else if (hasCloseWallpaper) {
+            return WALLPAPER_TRANSITION_CLOSE;
+        } else {
+            return WALLPAPER_TRANSITION_NONE;
+        }
     }
 
     private static void applyTransformation(long time, SurfaceControl.Transaction t,
-            SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix) {
+            SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix,
+            Point position) {
         anim.getTransformation(time, transformation);
+        if (position != null) {
+            transformation.getMatrix().postTranslate(position.x, position.y);
+        }
         t.setMatrix(leash, transformation.getMatrix(), matrix);
         t.setAlpha(leash, transformation.getAlpha());
         t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
index dffc700..bdcdb63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.transition;
 
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 
 /**
@@ -28,10 +28,10 @@
      * Registers a remote transition handler.
      */
     oneway void registerRemote(in TransitionFilter filter,
-            in IRemoteTransition remoteTransition) = 1;
+            in RemoteTransition remoteTransition) = 1;
 
     /**
      * Unregisters a remote transition handler.
      */
-    oneway void unregisterRemote(in IRemoteTransition remoteTransition) = 2;
+    oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2;
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
new file mode 100644
index 0000000..61e11e8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import android.annotation.NonNull;
+import android.os.RemoteException;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IWindowContainerTransactionCallback;
+
+/**
+ * Utilities and interfaces for transition-like usage on top of the legacy app-transition and
+ * synctransaction tools.
+ */
+public class LegacyTransitions {
+
+    /**
+     * Interface for a "legacy" transition. Effectively wraps a sync callback + remoteAnimation
+     * into one callback.
+     */
+    public interface ILegacyTransition {
+        /**
+         * Called when both the associated sync transaction finishes and the remote animation is
+         * ready.
+         */
+        void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+                RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+                IRemoteAnimationFinishedCallback finishedCallback, SurfaceControl.Transaction t);
+    }
+
+    /**
+     * Makes sure that a remote animation and corresponding sync callback are called together
+     * such that the sync callback is called first. This assumes that both the callback receiver
+     * and the remoteanimation are in the same process so that order is preserved on both ends.
+     */
+    public static class LegacyTransition {
+        private final ILegacyTransition mLegacyTransition;
+        private int mSyncId = -1;
+        private SurfaceControl.Transaction mTransaction;
+        private int mTransit;
+        private RemoteAnimationTarget[] mApps;
+        private RemoteAnimationTarget[] mWallpapers;
+        private RemoteAnimationTarget[] mNonApps;
+        private IRemoteAnimationFinishedCallback mFinishCallback = null;
+        private boolean mCancelled = false;
+        private final SyncCallback mSyncCallback = new SyncCallback();
+        private final RemoteAnimationAdapter mAdapter =
+                new RemoteAnimationAdapter(new RemoteAnimationWrapper(), 0, 0);
+
+        public LegacyTransition(@WindowManager.TransitionType int type,
+                @NonNull ILegacyTransition legacyTransition) {
+            mLegacyTransition = legacyTransition;
+            mTransit = type;
+        }
+
+        public @WindowManager.TransitionType int getType() {
+            return mTransit;
+        }
+
+        public IWindowContainerTransactionCallback getSyncCallback() {
+            return mSyncCallback;
+        }
+
+        public RemoteAnimationAdapter getAdapter() {
+            return mAdapter;
+        }
+
+        private class SyncCallback extends IWindowContainerTransactionCallback.Stub {
+            @Override
+            public void onTransactionReady(int id, SurfaceControl.Transaction t)
+                    throws RemoteException {
+                mSyncId = id;
+                mTransaction = t;
+                checkApply();
+            }
+        }
+
+        private class RemoteAnimationWrapper extends IRemoteAnimationRunner.Stub {
+            @Override
+            public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+                    IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
+                mTransit = transit;
+                mApps = apps;
+                mWallpapers = wallpapers;
+                mNonApps = nonApps;
+                mFinishCallback = finishedCallback;
+                checkApply();
+            }
+
+            @Override
+            public void onAnimationCancelled() throws RemoteException {
+                mCancelled = true;
+                mApps = mWallpapers = mNonApps = null;
+                checkApply();
+            }
+        }
+
+
+        private void checkApply() throws RemoteException {
+            if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) return;
+            mLegacyTransition.onAnimationStart(mTransit, mApps, mWallpapers,
+                    mNonApps, mFinishCallback, mTransaction);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
index 4da6664..3be896e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
@@ -24,6 +24,7 @@
 import android.view.SurfaceControl;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
@@ -43,10 +44,10 @@
     private IBinder mTransition = null;
 
     /** The remote to delegate animation to */
-    private final IRemoteTransition mRemote;
+    private final RemoteTransition mRemote;
 
     public OneShotRemoteHandler(@NonNull ShellExecutor mainExecutor,
-            @NonNull IRemoteTransition remote) {
+            @NonNull RemoteTransition remote) {
         mMainExecutor = mainExecutor;
         mRemote = remote;
     }
@@ -57,7 +58,8 @@
 
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         if (mTransition != transition) return false;
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote"
@@ -70,19 +72,24 @@
         };
         IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
             @Override
-            public void onTransitionFinished(WindowContainerTransaction wct) {
+            public void onTransitionFinished(WindowContainerTransaction wct,
+                    SurfaceControl.Transaction sct) {
                 if (mRemote.asBinder() != null) {
                     mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
                 }
-                mMainExecutor.execute(
-                        () -> finishCallback.onTransitionFinished(wct, null /* wctCB */));
+                mMainExecutor.execute(() -> {
+                    if (sct != null) {
+                        finishTransaction.merge(sct);
+                    }
+                    finishCallback.onTransitionFinished(wct, null /* wctCB */);
+                });
             }
         };
         try {
             if (mRemote.asBinder() != null) {
                 mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */);
             }
-            mRemote.startAnimation(transition, info, t, cb);
+            mRemote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb);
         } catch (RemoteException e) {
             Log.e(Transitions.TAG, "Error running remote transition.", e);
             if (mRemote.asBinder() != null) {
@@ -102,13 +109,14 @@
 
         IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
             @Override
-            public void onTransitionFinished(WindowContainerTransaction wct) {
+            public void onTransitionFinished(WindowContainerTransaction wct,
+                    SurfaceControl.Transaction sct) {
                 mMainExecutor.execute(
                         () -> finishCallback.onTransitionFinished(wct, null /* wctCB */));
             }
         };
         try {
-            mRemote.mergeAnimation(transition, info, t, mergeTarget, cb);
+            mRemote.getRemoteTransition().mergeAnimation(transition, info, t, mergeTarget, cb);
         } catch (RemoteException e) {
             Log.e(Transitions.TAG, "Error merging remote transition.", e);
         }
@@ -118,8 +126,9 @@
     @Nullable
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
             @Nullable TransitionRequestInfo request) {
-        IRemoteTransition remote = request.getRemoteTransition();
-        if (remote != mRemote) return null;
+        RemoteTransition remote = request.getRemoteTransition();
+        IRemoteTransition iRemote = remote != null ? remote.getRemoteTransition() : null;
+        if (iRemote != mRemote.getRemoteTransition()) return null;
         mTransition = transition;
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested"
                 + " for %s: %s", transition, remote);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index 9bfb261..c798ace 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -27,6 +27,7 @@
 import android.view.SurfaceControl;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
@@ -50,45 +51,33 @@
     private final ShellExecutor mMainExecutor;
 
     /** Includes remotes explicitly requested by, eg, ActivityOptions */
-    private final ArrayMap<IBinder, IRemoteTransition> mRequestedRemotes = new ArrayMap<>();
+    private final ArrayMap<IBinder, RemoteTransition> mRequestedRemotes = new ArrayMap<>();
 
     /** Ordered by specificity. Last filters will be checked first */
-    private final ArrayList<Pair<TransitionFilter, IRemoteTransition>> mFilters =
+    private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mFilters =
             new ArrayList<>();
 
-    private final IBinder.DeathRecipient mTransitionDeathRecipient =
-            new IBinder.DeathRecipient() {
-                @Override
-                @BinderThread
-                public void binderDied() {
-                    mMainExecutor.execute(() -> mFilters.clear());
-                }
-            };
+    private final ArrayMap<IBinder, RemoteDeathHandler> mDeathHandlers = new ArrayMap<>();
 
     RemoteTransitionHandler(@NonNull ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
     }
 
-    void addFiltered(TransitionFilter filter, IRemoteTransition remote) {
-        try {
-            remote.asBinder().linkToDeath(mTransitionDeathRecipient, 0 /* flags */);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Failed to link to death");
-            return;
-        }
+    void addFiltered(TransitionFilter filter, RemoteTransition remote) {
+        handleDeath(remote.asBinder(), null /* finishCallback */);
         mFilters.add(new Pair<>(filter, remote));
     }
 
-    void removeFiltered(IRemoteTransition remote) {
+    void removeFiltered(RemoteTransition remote) {
         boolean removed = false;
         for (int i = mFilters.size() - 1; i >= 0; --i) {
-            if (mFilters.get(i).second == remote) {
+            if (mFilters.get(i).second.asBinder().equals(remote.asBinder())) {
                 mFilters.remove(i);
                 removed = true;
             }
         }
         if (removed) {
-            remote.asBinder().unlinkToDeath(mTransitionDeathRecipient, 0 /* flags */);
+            unhandleDeath(remote.asBinder(), null /* finishCallback */);
         }
     }
 
@@ -99,9 +88,10 @@
 
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        IRemoteTransition pendingRemote = mRequestedRemotes.get(transition);
+        RemoteTransition pendingRemote = mRequestedRemotes.get(transition);
         if (pendingRemote == null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have "
                     + "explicit remote, search filters for match for %s", transition, info);
@@ -110,6 +100,7 @@
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s",
                         mFilters.get(i));
                 if (mFilters.get(i).first.matches(info)) {
+                    Slog.d(TAG, "Found filter" + mFilters.get(i));
                     pendingRemote = mFilters.get(i).second;
                     // Add to requested list so that it can be found for merge requests.
                     mRequestedRemotes.put(transition, pendingRemote);
@@ -122,36 +113,27 @@
 
         if (pendingRemote == null) return false;
 
-        final IRemoteTransition remote = pendingRemote;
-        final IBinder.DeathRecipient remoteDied = () -> {
-            Log.e(Transitions.TAG, "Remote transition died, finishing");
-            mMainExecutor.execute(() -> {
-                mRequestedRemotes.remove(transition);
-                finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
-            });
-        };
+        final RemoteTransition remote = pendingRemote;
         IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
             @Override
-            public void onTransitionFinished(WindowContainerTransaction wct) {
-                if (remote.asBinder() != null) {
-                    remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
-                }
+            public void onTransitionFinished(WindowContainerTransaction wct,
+                    SurfaceControl.Transaction sct) {
+                unhandleDeath(remote.asBinder(), finishCallback);
                 mMainExecutor.execute(() -> {
+                    if (sct != null) {
+                        finishTransaction.merge(sct);
+                    }
                     mRequestedRemotes.remove(transition);
                     finishCallback.onTransitionFinished(wct, null /* wctCB */);
                 });
             }
         };
         try {
-            if (remote.asBinder() != null) {
-                remote.asBinder().linkToDeath(remoteDied, 0 /* flags */);
-            }
-            remote.startAnimation(transition, info, t, cb);
+            handleDeath(remote.asBinder(), finishCallback);
+            remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb);
         } catch (RemoteException e) {
             Log.e(Transitions.TAG, "Error running remote transition.", e);
-            if (remote.asBinder() != null) {
-                remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
-            }
+            unhandleDeath(remote.asBinder(), finishCallback);
             mRequestedRemotes.remove(transition);
             mMainExecutor.execute(
                     () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */));
@@ -163,14 +145,15 @@
     public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget);
+        final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget).getRemoteTransition();
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt merge %s into %s",
                 transition, remote);
         if (remote == null) return;
 
         IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
             @Override
-            public void onTransitionFinished(WindowContainerTransaction wct) {
+            public void onTransitionFinished(WindowContainerTransaction wct,
+                    SurfaceControl.Transaction sct) {
                 mMainExecutor.execute(() -> {
                     if (!mRequestedRemotes.containsKey(mergeTarget)) {
                         Log.e(TAG, "Merged transition finished after it's mergeTarget (the "
@@ -193,11 +176,98 @@
     @Nullable
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
             @Nullable TransitionRequestInfo request) {
-        IRemoteTransition remote = request.getRemoteTransition();
+        RemoteTransition remote = request.getRemoteTransition();
         if (remote == null) return null;
         mRequestedRemotes.put(transition, remote);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested"
                 + " for %s: %s", transition, remote);
         return new WindowContainerTransaction();
     }
+
+    private void handleDeath(@NonNull IBinder remote,
+            @Nullable Transitions.TransitionFinishCallback finishCallback) {
+        synchronized (mDeathHandlers) {
+            RemoteDeathHandler deathHandler = mDeathHandlers.get(remote);
+            if (deathHandler == null) {
+                deathHandler = new RemoteDeathHandler(remote);
+                try {
+                    remote.linkToDeath(deathHandler, 0 /* flags */);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to link to death");
+                    return;
+                }
+                mDeathHandlers.put(remote, deathHandler);
+            }
+            deathHandler.addUser(finishCallback);
+        }
+    }
+
+    private void unhandleDeath(@NonNull IBinder remote,
+            @Nullable Transitions.TransitionFinishCallback finishCallback) {
+        synchronized (mDeathHandlers) {
+            RemoteDeathHandler deathHandler = mDeathHandlers.get(remote);
+            if (deathHandler == null) return;
+            deathHandler.removeUser(finishCallback);
+            if (deathHandler.getUserCount() == 0) {
+                if (!deathHandler.mPendingFinishCallbacks.isEmpty()) {
+                    throw new IllegalStateException("Unhandling death for binder that still has"
+                            + " pending finishCallback(s).");
+                }
+                remote.unlinkToDeath(deathHandler, 0 /* flags */);
+                mDeathHandlers.remove(remote);
+            }
+        }
+    }
+
+    /** NOTE: binder deaths can alter the filter order */
+    private class RemoteDeathHandler implements IBinder.DeathRecipient {
+        private final IBinder mRemote;
+        private final ArrayList<Transitions.TransitionFinishCallback> mPendingFinishCallbacks =
+                new ArrayList<>();
+        private int mUsers = 0;
+
+        RemoteDeathHandler(IBinder remote) {
+            mRemote = remote;
+        }
+
+        void addUser(@Nullable Transitions.TransitionFinishCallback finishCallback) {
+            if (finishCallback != null) {
+                mPendingFinishCallbacks.add(finishCallback);
+            }
+            ++mUsers;
+        }
+
+        void removeUser(@Nullable Transitions.TransitionFinishCallback finishCallback) {
+            if (finishCallback != null) {
+                mPendingFinishCallbacks.remove(finishCallback);
+            }
+            --mUsers;
+        }
+
+        int getUserCount() {
+            return mUsers;
+        }
+
+        @Override
+        @BinderThread
+        public void binderDied() {
+            mMainExecutor.execute(() -> {
+                for (int i = mFilters.size() - 1; i >= 0; --i) {
+                    if (mRemote.equals(mFilters.get(i).second.asBinder())) {
+                        mFilters.remove(i);
+                    }
+                }
+                for (int i = mRequestedRemotes.size() - 1; i >= 0; --i) {
+                    if (mRemote.equals(mRequestedRemotes.valueAt(i).asBinder())) {
+                        mRequestedRemotes.removeAt(i);
+                    }
+                }
+                for (int i = mPendingFinishCallbacks.size() - 1; i >= 0; --i) {
+                    mPendingFinishCallbacks.get(i).onTransitionFinished(
+                            null /* wct */, null /* wctCB */);
+                }
+                mPendingFinishCallbacks.clear();
+            });
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
new file mode 100644
index 0000000..13c670a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import static android.hardware.HardwareBuffer.RGBA_8888;
+import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
+import static android.util.RotationUtils.deltaRotation;
+import static android.view.WindowManagerPolicyConstants.SCREEN_FREEZE_LAYER_BASE;
+
+import static com.android.wm.shell.transition.DefaultTransitionHandler.startSurfaceAnimation;
+import static com.android.wm.shell.transition.Transitions.TAG;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.ColorSpace;
+import android.graphics.GraphicBuffer;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
+import android.media.Image;
+import android.media.ImageReader;
+import android.util.Slog;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.SurfaceSession;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.window.TransitionInfo;
+
+import com.android.internal.R;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * This class handles the rotation animation when the device is rotated.
+ *
+ * <p>
+ * The screen rotation animation is composed of 4 different part:
+ * <ul>
+ * <li> The screenshot: <p>
+ *     A screenshot of the whole screen prior the change of orientation is taken to hide the
+ *     element resizing below. The screenshot is then animated to rotate and cross-fade to
+ *     the new orientation with the content in the new orientation.
+ *
+ * <li> The windows on the display: <p>y
+ *      Once the device is rotated, the screen and its content are in the new orientation. The
+ *      animation first rotate the new content into the old orientation to then be able to
+ *      animate to the new orientation
+ *
+ * <li> The Background color frame: <p>
+ *      To have the animation seem more seamless, we add a color transitioning background behind the
+ *      exiting and entering layouts. We compute the brightness of the start and end
+ *      layouts and transition from the two brightness values as grayscale underneath the animation
+ *
+ * <li> The entering Blackframe: <p>
+ *     The enter Blackframe is similar to the exit Blackframe but is only used when a custom
+ *     rotation animation is used and matches the new content size instead of the screenshot.
+ * </ul>
+ */
+class ScreenRotationAnimation {
+    static final int MAX_ANIMATION_DURATION = 10 * 1000;
+
+    private final Context mContext;
+    private final TransactionPool mTransactionPool;
+    private final float[] mTmpFloats = new float[9];
+    // Complete transformations being applied.
+    private final Matrix mSnapshotInitialMatrix = new Matrix();
+    /** The leash of display. */
+    private final SurfaceControl mSurfaceControl;
+    private final Rect mStartBounds = new Rect();
+    private final Rect mEndBounds = new Rect();
+
+    private final int mStartWidth;
+    private final int mStartHeight;
+    private final int mEndWidth;
+    private final int mEndHeight;
+    private final int mStartRotation;
+    private final int mEndRotation;
+
+    /** This layer contains the actual screenshot that is to be faded out. */
+    private SurfaceControl mScreenshotLayer;
+    /**
+     * Only used for screen rotation and not custom animations. Layered behind all other layers
+     * to avoid showing any "empty" spots
+     */
+    private SurfaceControl mBackColorSurface;
+    /** The leash using to animate screenshot layer. */
+    private SurfaceControl mAnimLeash;
+    private Transaction mTransaction;
+
+    // The current active animation to move from the old to the new rotated
+    // state.  Which animation is run here will depend on the old and new
+    // rotations.
+    private Animation mRotateExitAnimation;
+    private Animation mRotateEnterAnimation;
+
+    /** Intensity of light/whiteness of the layout before rotation occurs. */
+    private float mStartLuma;
+    /** Intensity of light/whiteness of the layout after rotation occurs. */
+    private float mEndLuma;
+
+    ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool,
+            Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash) {
+        mContext = context;
+        mTransactionPool = pool;
+
+        mSurfaceControl = change.getLeash();
+        mStartWidth = change.getStartAbsBounds().width();
+        mStartHeight = change.getStartAbsBounds().height();
+        mEndWidth = change.getEndAbsBounds().width();
+        mEndHeight = change.getEndAbsBounds().height();
+        mStartRotation = change.getStartRotation();
+        mEndRotation = change.getEndRotation();
+
+        mStartBounds.set(change.getStartAbsBounds());
+        mEndBounds.set(change.getEndAbsBounds());
+
+        mAnimLeash = new SurfaceControl.Builder(session)
+                .setParent(rootLeash)
+                .setEffectLayer()
+                .setCallsite("ShellRotationAnimation")
+                .setName("Animation leash of screenshot rotation")
+                .build();
+
+        try {
+            SurfaceControl.LayerCaptureArgs args =
+                    new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl)
+                            .setCaptureSecureLayers(true)
+                            .setAllowProtected(true)
+                            .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight))
+                            .build();
+            SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
+                    SurfaceControl.captureLayers(args);
+            if (screenshotBuffer == null) {
+                Slog.w(TAG, "Unable to take screenshot of display");
+                return;
+            }
+
+            mBackColorSurface = new SurfaceControl.Builder(session)
+                    .setParent(rootLeash)
+                    .setColorLayer()
+                    .setCallsite("ShellRotationAnimation")
+                    .setName("BackColorSurface")
+                    .build();
+
+            mScreenshotLayer = new SurfaceControl.Builder(session)
+                    .setParent(mAnimLeash)
+                    .setBLASTLayer()
+                    .setSecure(screenshotBuffer.containsSecureLayers())
+                    .setCallsite("ShellRotationAnimation")
+                    .setName("RotationLayer")
+                    .build();
+
+            HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
+            mStartLuma = getMedianBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace());
+
+            GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer(
+                    screenshotBuffer.getHardwareBuffer());
+
+            t.setLayer(mBackColorSurface, -1);
+            t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma});
+            t.setAlpha(mBackColorSurface, 1);
+            t.show(mBackColorSurface);
+
+            t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
+            t.setPosition(mAnimLeash, 0, 0);
+            t.setAlpha(mAnimLeash, 1);
+            t.show(mAnimLeash);
+
+            t.setBuffer(mScreenshotLayer, buffer);
+            t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace());
+            t.show(mScreenshotLayer);
+
+        } catch (Surface.OutOfResourcesException e) {
+            Slog.w(TAG, "Unable to allocate freeze surface", e);
+        }
+
+        setRotation(t);
+        t.apply();
+    }
+
+    private void setRotation(SurfaceControl.Transaction t) {
+        // Compute the transformation matrix that must be applied
+        // to the snapshot to make it stay in the same original position
+        // with the current screen rotation.
+        int delta = deltaRotation(mEndRotation, mStartRotation);
+        createRotationMatrix(delta, mStartWidth, mStartHeight, mSnapshotInitialMatrix);
+        setRotationTransform(t, mSnapshotInitialMatrix);
+    }
+
+    private void setRotationTransform(SurfaceControl.Transaction t, Matrix matrix) {
+        if (mScreenshotLayer == null) {
+            return;
+        }
+        matrix.getValues(mTmpFloats);
+        float x = mTmpFloats[Matrix.MTRANS_X];
+        float y = mTmpFloats[Matrix.MTRANS_Y];
+        t.setPosition(mScreenshotLayer, x, y);
+        t.setMatrix(mScreenshotLayer,
+                mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y],
+                mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]);
+
+        t.setAlpha(mScreenshotLayer, (float) 1.0);
+        t.show(mScreenshotLayer);
+    }
+
+    /**
+     * Returns true if animating.
+     */
+    public boolean startAnimation(@NonNull ArrayList<Animator> animations,
+            @NonNull Runnable finishCallback, float animationScale,
+            @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
+        if (mScreenshotLayer == null) {
+            // Can't do animation.
+            return false;
+        }
+
+        // TODO : Found a way to get right end luma and re-enable color frame animation.
+        // End luma value is very not stable so it will cause more flicker is we run background
+        // color frame animation.
+        //mEndLuma = getLumaOfSurfaceControl(mEndBounds, mSurfaceControl);
+
+        // Figure out how the screen has moved from the original rotation.
+        int delta = deltaRotation(mEndRotation, mStartRotation);
+        switch (delta) { /* Counter-Clockwise Rotations */
+            case Surface.ROTATION_0:
+                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.screen_rotate_0_exit);
+                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.rotation_animation_enter);
+                break;
+            case Surface.ROTATION_90:
+                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.screen_rotate_plus_90_exit);
+                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.screen_rotate_plus_90_enter);
+                break;
+            case Surface.ROTATION_180:
+                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.screen_rotate_180_exit);
+                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.screen_rotate_180_enter);
+                break;
+            case Surface.ROTATION_270:
+                mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.screen_rotate_minus_90_exit);
+                mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
+                        R.anim.screen_rotate_minus_90_enter);
+                break;
+        }
+
+        mRotateExitAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight);
+        mRotateExitAnimation.restrictDuration(MAX_ANIMATION_DURATION);
+        mRotateExitAnimation.scaleCurrentDuration(animationScale);
+        mRotateEnterAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight);
+        mRotateEnterAnimation.restrictDuration(MAX_ANIMATION_DURATION);
+        mRotateEnterAnimation.scaleCurrentDuration(animationScale);
+
+        mTransaction = mTransactionPool.acquire();
+        startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor);
+        startScreenshotRotationAnimation(animations, finishCallback, mainExecutor, animExecutor);
+        //startColorAnimation(mTransaction, animationScale);
+
+        return true;
+    }
+
+    private void startDisplayRotation(@NonNull ArrayList<Animator> animations,
+            @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor,
+            @NonNull ShellExecutor animExecutor) {
+        startSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback,
+                mTransactionPool, mainExecutor, animExecutor, null /* position */);
+    }
+
+    private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations,
+            @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor,
+            @NonNull ShellExecutor animExecutor) {
+        startSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback,
+                mTransactionPool, mainExecutor, animExecutor, null /* position */);
+    }
+
+    private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) {
+        int colorTransitionMs = mContext.getResources().getInteger(
+                R.integer.config_screen_rotation_color_transition);
+        final float[] rgbTmpFloat = new float[3];
+        final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma);
+        final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma);
+        final long duration = colorTransitionMs * (long) animationScale;
+        final Transaction t = mTransactionPool.acquire();
+
+        final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
+        // Animation length is already expected to be scaled.
+        va.overrideDurationScale(1.0f);
+        va.setDuration(duration);
+        va.addUpdateListener(animation -> {
+            final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
+            final float fraction = currentPlayTime / va.getDuration();
+            applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t);
+        });
+        va.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
+                        t);
+                mTransactionPool.release(t);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
+                        t);
+                mTransactionPool.release(t);
+            }
+        });
+        animExecutor.execute(va::start);
+    }
+
+    public void kill() {
+        Transaction t = mTransaction != null ? mTransaction : mTransactionPool.acquire();
+        if (mAnimLeash.isValid()) {
+            t.remove(mAnimLeash);
+        }
+
+        if (mScreenshotLayer != null) {
+            if (mScreenshotLayer.isValid()) {
+                t.remove(mScreenshotLayer);
+            }
+            mScreenshotLayer = null;
+
+            if (mBackColorSurface != null) {
+                if (mBackColorSurface.isValid()) {
+                    t.remove(mBackColorSurface);
+                }
+                mBackColorSurface = null;
+            }
+        }
+        t.apply();
+        mTransactionPool.release(t);
+    }
+
+    /**
+     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
+     * luminance at the borders of the bitmap
+     * @return the average luminance of all the pixels at the borders of the bitmap
+     */
+    private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
+        // Cannot read content from buffer with protected usage.
+        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
+                || hasProtectedContent(hardwareBuffer)) {
+            return 0;
+        }
+
+        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
+                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
+        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
+        Image image = ir.acquireLatestImage();
+        if (image == null || image.getPlanes().length == 0) {
+            return 0;
+        }
+
+        Image.Plane plane = image.getPlanes()[0];
+        ByteBuffer buffer = plane.getBuffer();
+        int width = image.getWidth();
+        int height = image.getHeight();
+        int pixelStride = plane.getPixelStride();
+        int rowStride = plane.getRowStride();
+        float[] borderLumas = new float[2 * width + 2 * height];
+
+        // Grab the top and bottom borders
+        int l = 0;
+        for (int x = 0; x < width; x++) {
+            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
+            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
+        }
+
+        // Grab the left and right borders
+        for (int y = 0; y < height; y++) {
+            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
+            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
+        }
+
+        // Cleanup
+        ir.close();
+
+        // Oh, is this too simple and inefficient for you?
+        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
+        Arrays.sort(borderLumas);
+        return borderLumas[borderLumas.length / 2];
+    }
+
+    /**
+     * @return whether the hardwareBuffer passed in is marked as protected.
+     */
+    private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
+        return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
+    }
+
+    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
+            int pixelStride, int rowStride) {
+        int offset = y * rowStride + x * pixelStride;
+        int pixel = 0;
+        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
+        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
+        pixel |= (buffer.get(offset + 2) & 0xff);       // B
+        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
+        return Color.valueOf(pixel).luminance();
+    }
+
+    /**
+     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
+     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
+     */
+    private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) {
+        if (surfaceControl ==  null) {
+            return 0;
+        }
+
+        Rect crop = new Rect(0, 0, bounds.width(), bounds.height());
+        SurfaceControl.ScreenshotHardwareBuffer buffer =
+                SurfaceControl.captureLayers(surfaceControl, crop, 1);
+        if (buffer == null) {
+            return 0;
+        }
+
+        return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
+    }
+
+    private static void createRotationMatrix(int rotation, int width, int height,
+            Matrix outMatrix) {
+        switch (rotation) {
+            case Surface.ROTATION_0:
+                outMatrix.reset();
+                break;
+            case Surface.ROTATION_90:
+                outMatrix.setRotate(90, 0, 0);
+                outMatrix.postTranslate(height, 0);
+                break;
+            case Surface.ROTATION_180:
+                outMatrix.setRotate(180, 0, 0);
+                outMatrix.postTranslate(width, height);
+                break;
+            case Surface.ROTATION_270:
+                outMatrix.setRotate(270, 0, 0);
+                outMatrix.postTranslate(0, width);
+                break;
+        }
+    }
+
+    private static void applyColor(int startColor, int endColor, float[] rgbFloat,
+            float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
+        final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
+                endColor);
+        Color middleColor = Color.valueOf(color);
+        rgbFloat[0] = middleColor.red();
+        rgbFloat[1] = middleColor.green();
+        rgbFloat[2] = middleColor.blue();
+        if (surface.isValid()) {
+            t.setColor(surface, rgbFloat);
+        }
+        t.apply();
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
index bc42c6e..802d25f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
@@ -17,7 +17,7 @@
 package com.android.wm.shell.transition;
 
 import android.annotation.NonNull;
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 
 import com.android.wm.shell.common.annotations.ExternalThread;
@@ -39,10 +39,10 @@
      * Registers a remote transition.
      */
     void registerRemote(@NonNull TransitionFilter filter,
-            @NonNull IRemoteTransition remoteTransition);
+            @NonNull RemoteTransition remoteTransition);
 
     /**
      * Unregisters a remote transition.
      */
-    void unregisterRemote(@NonNull IRemoteTransition remoteTransition);
+    void unregisterRemote(@NonNull RemoteTransition remoteTransition);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 60707cc..c369831 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -19,6 +19,7 @@
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -39,10 +40,11 @@
 import android.util.Log;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
-import android.window.IRemoteTransition;
 import android.window.ITransitionPlayer;
+import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 import android.window.TransitionInfo;
+import android.window.TransitionMetrics;
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
 import android.window.WindowContainerTransactionCallback;
@@ -54,6 +56,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
@@ -77,6 +80,15 @@
     /** Transition type for launching 2 tasks simultaneously. */
     public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 2;
 
+    /** Transition type for exiting PIP via the Shell, via pressing the expand button. */
+    public static final int TRANSIT_EXIT_PIP = TRANSIT_FIRST_CUSTOM + 3;
+
+    /** Transition type for removing PIP via the Shell, either via Dismiss bubble or Close. */
+    public static final int TRANSIT_REMOVE_PIP = TRANSIT_FIRST_CUSTOM + 4;
+
+    /** Transition type for entering split by opening an app into side-stage. */
+    public static final int TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE = TRANSIT_FIRST_CUSTOM + 5;
+
     private final WindowOrganizer mOrganizer;
     private final Context mContext;
     private final ShellExecutor mMainExecutor;
@@ -91,27 +103,29 @@
     private float mTransitionAnimationScaleSetting = 1.0f;
 
     private static final class ActiveTransition {
-        IBinder mToken = null;
-        TransitionHandler mHandler = null;
-        boolean mMerged = false;
-        TransitionInfo mInfo = null;
-        SurfaceControl.Transaction mStartT = null;
-        SurfaceControl.Transaction mFinishT = null;
+        IBinder mToken;
+        TransitionHandler mHandler;
+        boolean mMerged;
+        boolean mAborted;
+        TransitionInfo mInfo;
+        SurfaceControl.Transaction mStartT;
+        SurfaceControl.Transaction mFinishT;
     }
 
     /** Keeps track of currently playing transitions in the order of receipt. */
     private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>();
 
     public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool,
-            @NonNull Context context, @NonNull ShellExecutor mainExecutor,
-            @NonNull ShellExecutor animExecutor) {
+            @NonNull DisplayController displayController, @NonNull Context context,
+            @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
         mOrganizer = organizer;
         mContext = context;
         mMainExecutor = mainExecutor;
         mAnimExecutor = animExecutor;
         mPlayerImpl = new TransitionPlayerImpl();
         // The very last handler (0 in the list) should be the default one.
-        mHandlers.add(new DefaultTransitionHandler(pool, context, mainExecutor, animExecutor));
+        mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor,
+                animExecutor));
         // Next lowest priority is remote transitions.
         mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor);
         mHandlers.add(mRemoteTransitionHandler);
@@ -163,13 +177,13 @@
         return new ShellTransitions() {
             @Override
             public void registerRemote(@androidx.annotation.NonNull TransitionFilter filter,
-                    @androidx.annotation.NonNull IRemoteTransition remoteTransition) {
+                    @androidx.annotation.NonNull RemoteTransition remoteTransition) {
                 // Do nothing
             }
 
             @Override
             public void unregisterRemote(
-                    @androidx.annotation.NonNull IRemoteTransition remoteTransition) {
+                    @androidx.annotation.NonNull RemoteTransition remoteTransition) {
                 // Do nothing
             }
         };
@@ -179,6 +193,8 @@
     public void register(ShellTaskOrganizer taskOrganizer) {
         if (mPlayerImpl == null) return;
         taskOrganizer.registerTransitionPlayer(mPlayerImpl);
+        // Pre-load the instance.
+        TransitionMetrics.getInstance();
     }
 
     /**
@@ -205,12 +221,12 @@
 
     /** Register a remote transition to be used when `filter` matches an incoming transition */
     public void registerRemote(@NonNull TransitionFilter filter,
-            @NonNull IRemoteTransition remoteTransition) {
+            @NonNull RemoteTransition remoteTransition) {
         mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
     }
 
     /** Unregisters a remote transition and all associated filters */
-    public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) {
+    public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
         mRemoteTransitionHandler.removeFiltered(remoteTransition);
     }
 
@@ -218,7 +234,7 @@
     public static boolean isOpeningType(@WindowManager.TransitionType int type) {
         return type == TRANSIT_OPEN
                 || type == TRANSIT_TO_FRONT
-                || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+                || type == TRANSIT_KEYGUARD_GOING_AWAY;
     }
 
     /** @return true if the transition was triggered by closing something vs opening something */
@@ -382,7 +398,7 @@
     }
 
     boolean startAnimation(@NonNull ActiveTransition active, TransitionHandler handler) {
-        return handler.startAnimation(active.mToken, active.mInfo, active.mStartT,
+        return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, active.mFinishT,
                 (wct, cb) -> onFinish(active.mToken, wct, cb));
     }
 
@@ -416,17 +432,19 @@
 
     /** Special version of finish just for dealing with no-op/invalid transitions. */
     private void onAbort(IBinder transition) {
-        final int activeIdx = findActiveTransition(transition);
-        if (activeIdx < 0) return;
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
-                "Transition animation aborted due to no-op, notifying core %s", transition);
-        mActiveTransitions.remove(activeIdx);
-        mOrganizer.finishTransition(transition, null /* wct */, null /* wctCB */);
+        onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */);
     }
 
     private void onFinish(IBinder transition,
             @Nullable WindowContainerTransaction wct,
             @Nullable WindowContainerTransactionCallback wctCB) {
+        onFinish(transition, wct, wctCB, false /* abort */);
+    }
+
+    private void onFinish(IBinder transition,
+            @Nullable WindowContainerTransaction wct,
+            @Nullable WindowContainerTransactionCallback wctCB,
+            boolean abort) {
         int activeIdx = findActiveTransition(transition);
         if (activeIdx < 0) {
             Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or "
@@ -434,28 +452,37 @@
             return;
         } else if (activeIdx > 0) {
             // This transition was merged.
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s",
-                    transition);
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged (abort=%b:"
+                    + " %s", abort, transition);
             final ActiveTransition active = mActiveTransitions.get(activeIdx);
             active.mMerged = true;
+            active.mAborted = abort;
             if (active.mHandler != null) {
                 active.mHandler.onTransitionMerged(active.mToken);
             }
             return;
         }
+        mActiveTransitions.get(activeIdx).mAborted = abort;
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
-                "Transition animation finished, notifying core %s", transition);
+                "Transition animation finished (abort=%b), notifying core %s", abort, transition);
         // Merge all relevant transactions together
         SurfaceControl.Transaction fullFinish = mActiveTransitions.get(activeIdx).mFinishT;
         for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) {
             final ActiveTransition toMerge = mActiveTransitions.get(iA);
             if (!toMerge.mMerged) break;
+            // aborted transitions have no start/finish transactions
+            if (mActiveTransitions.get(iA).mStartT == null) break;
+            if (fullFinish == null) {
+                fullFinish = new SurfaceControl.Transaction();
+            }
             // Include start. It will be a no-op if it was already applied. Otherwise, we need it
             // to maintain consistent state.
             fullFinish.merge(mActiveTransitions.get(iA).mStartT);
             fullFinish.merge(mActiveTransitions.get(iA).mFinishT);
         }
-        fullFinish.apply();
+        if (fullFinish != null) {
+            fullFinish.apply();
+        }
         // Now perform all the finishes.
         mActiveTransitions.remove(activeIdx);
         mOrganizer.finishTransition(transition, wct, wctCB);
@@ -464,6 +491,12 @@
             ActiveTransition merged = mActiveTransitions.remove(activeIdx);
             mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */);
         }
+        // sift through aborted transitions
+        while (mActiveTransitions.size() > activeIdx
+                && mActiveTransitions.get(activeIdx).mAborted) {
+            ActiveTransition aborted = mActiveTransitions.remove(activeIdx);
+            mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */);
+        }
         if (mActiveTransitions.size() <= activeIdx) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations "
                     + "finished");
@@ -494,6 +527,12 @@
         int mergeIdx = activeIdx + 1;
         while (mergeIdx < mActiveTransitions.size()) {
             ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx);
+            if (mergeCandidate.mAborted) {
+                // transition was aborted, so we can skip for now (still leave it in the list
+                // so that it gets cleaned-up in the right order).
+                ++mergeIdx;
+                continue;
+            }
             if (mergeCandidate.mMerged) {
                 throw new IllegalStateException("Can't merge a transition after not-merging"
                         + " a preceding one.");
@@ -566,12 +605,19 @@
          * Starts a transition animation. This is always called if handleRequest returned non-null
          * for a particular transition. Otherwise, it is only called if no other handler before
          * it handled the transition.
-         *
+         * @param startTransaction the transaction given to the handler to be applied before the
+         *                         transition animation. Note the handler is expected to call on
+         *                         {@link SurfaceControl.Transaction#apply()} for startTransaction.
+         * @param finishTransaction the transaction given to the handler to be applied after the
+         *                       transition animation. Unlike startTransaction, the handler is NOT
+         *                       expected to apply this transaction. The Transition system will
+         *                       apply it when finishCallback is called.
          * @param finishCallback Call this when finished. This MUST be called on main thread.
          * @return true if transition was handled, false if not (falls-back to default).
          */
         boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction t,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
                 @NonNull TransitionFinishCallback finishCallback);
 
         /**
@@ -661,14 +707,14 @@
 
         @Override
         public void registerRemote(@NonNull TransitionFilter filter,
-                @NonNull IRemoteTransition remoteTransition) {
+                @NonNull RemoteTransition remoteTransition) {
             mMainExecutor.execute(() -> {
                 mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
             });
         }
 
         @Override
-        public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) {
+        public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
             mMainExecutor.execute(() -> {
                 mRemoteTransitionHandler.removeFiltered(remoteTransition);
             });
@@ -695,7 +741,7 @@
 
         @Override
         public void registerRemote(@NonNull TransitionFilter filter,
-                @NonNull IRemoteTransition remoteTransition) {
+                @NonNull RemoteTransition remoteTransition) {
             executeRemoteCallWithTaskPermission(mTransitions, "registerRemote",
                     (transitions) -> {
                         transitions.mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
@@ -703,7 +749,7 @@
         }
 
         @Override
-        public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) {
+        public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
             executeRemoteCallWithTaskPermission(mTransitions, "unregisterRemote",
                     (transitions) -> {
                         transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
new file mode 100644
index 0000000..2c668ed
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import android.graphics.ColorSpace;
+import android.graphics.GraphicBuffer;
+import android.graphics.PixelFormat;
+import android.hardware.HardwareBuffer;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+
+/**
+ * Represents a surface that is displayed over a transition surface.
+ */
+class WindowThumbnail {
+
+    private SurfaceControl mSurfaceControl;
+
+    private WindowThumbnail() {}
+
+    /** Create a thumbnail surface and attach it over a parent surface. */
+    static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent,
+            HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) {
+        WindowThumbnail windowThumbnail = new WindowThumbnail();
+        windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession)
+                .setParent(parent)
+                .setName("WindowThumanil : " + parent.toString())
+                .setCallsite("WindowThumanil")
+                .setFormat(PixelFormat.TRANSLUCENT)
+                .build();
+
+        GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(thumbnailHeader);
+        t.setBuffer(windowThumbnail.mSurfaceControl, graphicBuffer);
+        t.setColorSpace(windowThumbnail.mSurfaceControl, ColorSpace.get(ColorSpace.Named.SRGB));
+        t.setLayer(windowThumbnail.mSurfaceControl, Integer.MAX_VALUE);
+        t.show(windowThumbnail.mSurfaceControl);
+        t.apply();
+
+        return windowThumbnail;
+    }
+
+    SurfaceControl getSurface() {
+        return mSurfaceControl;
+    }
+
+    /** Remove the thumbnail surface and release the surface. */
+    void destroy(SurfaceControl.Transaction t) {
+        if (mSurfaceControl == null) {
+            return;
+        }
+
+        t.remove(mSurfaceControl);
+        t.apply();
+        mSurfaceControl.release();
+        mSurfaceControl = null;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java
new file mode 100644
index 0000000..74e4812
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold;
+
+import android.annotation.FloatRange;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Wrapper interface for unfold transition progress provider for the Shell
+ * @see com.android.systemui.unfold.UnfoldTransitionProgressProvider
+ */
+public interface ShellUnfoldProgressProvider {
+
+    /**
+     * Adds a transition listener
+     */
+    void addListener(Executor executor, UnfoldListener listener);
+
+    /**
+     * Listener for receiving unfold updates
+     */
+    interface UnfoldListener {
+        default void onStateChangeStarted() {}
+
+        default void onStateChangeProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {}
+
+        default void onStateChangeFinished() {}
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
new file mode 100644
index 0000000..9faf454
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold;
+
+import static android.graphics.Color.blue;
+import static android.graphics.Color.green;
+import static android.graphics.Color.red;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+
+/**
+ * Controls background color layer for the unfold animations
+ */
+public class UnfoldBackgroundController {
+
+    private static final int BACKGROUND_LAYER_Z_INDEX = -1;
+
+    private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+    private final float[] mBackgroundColor;
+    private SurfaceControl mBackgroundLayer;
+
+    public UnfoldBackgroundController(
+            @NonNull Context context,
+            @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+        mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+        mBackgroundColor = getBackgroundColor(context);
+    }
+
+    /**
+     * Ensures that unfold animation background color layer is present,
+     * @param transaction where we should add the background if it is not added
+     */
+    public void ensureBackground(@NonNull SurfaceControl.Transaction transaction) {
+        if (mBackgroundLayer != null) return;
+
+        SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
+                .setName("app-unfold-background")
+                .setCallsite("AppUnfoldTransitionController")
+                .setColorLayer();
+        mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder);
+        mBackgroundLayer = colorLayerBuilder.build();
+
+        transaction
+                .setColor(mBackgroundLayer, mBackgroundColor)
+                .show(mBackgroundLayer)
+                .setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX);
+    }
+
+    /**
+     * Ensures that the background is not visible
+     * @param transaction as part of which the removal will happen if needed
+     */
+    public void removeBackground(@NonNull SurfaceControl.Transaction transaction) {
+        if (mBackgroundLayer == null) return;
+        if (mBackgroundLayer.isValid()) {
+            transaction.remove(mBackgroundLayer);
+        }
+        mBackgroundLayer = null;
+    }
+
+    private float[] getBackgroundColor(Context context) {
+        int colorInt = context.getResources().getColor(R.color.unfold_transition_background);
+        return new float[]{
+                (float) red(colorInt) / 255.0F,
+                (float) green(colorInt) / 255.0F,
+                (float) blue(colorInt) / 255.0F
+        };
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
new file mode 100644
index 0000000..b9b6716
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.util;
+
+import android.view.SurfaceControl;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class that takes care of counter-rotating surfaces during a transition animation.
+ */
+public class CounterRotator {
+    SurfaceControl mSurface = null;
+    ArrayList<SurfaceControl> mRotateChildren = null;
+
+    /** Gets the surface with the counter-rotation. */
+    public SurfaceControl getSurface() {
+        return mSurface;
+    }
+
+    /**
+     * Sets up this rotator.
+     *
+     * @param rotateDelta is the forward rotation change (the rotation the display is making).
+     * @param displayW (and H) Is the size of the rotating display.
+     */
+    public void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta,
+            float displayW, float displayH) {
+        if (rotateDelta == 0) return;
+        mRotateChildren = new ArrayList<>();
+        // We want to counter-rotate, so subtract from 4
+        rotateDelta = 4 - (rotateDelta + 4) % 4;
+        mSurface = new SurfaceControl.Builder()
+                .setName("Transition Unrotate")
+                .setContainerLayer()
+                .setParent(parent)
+                .build();
+        // column-major
+        if (rotateDelta == 1) {
+            t.setMatrix(mSurface, 0, 1, -1, 0);
+            t.setPosition(mSurface, displayW, 0);
+        } else if (rotateDelta == 2) {
+            t.setMatrix(mSurface, -1, 0, 0, -1);
+            t.setPosition(mSurface, displayW, displayH);
+        } else if (rotateDelta == 3) {
+            t.setMatrix(mSurface, 0, -1, 1, 0);
+            t.setPosition(mSurface, 0, displayH);
+        }
+        t.show(mSurface);
+    }
+
+    /**
+     * Add a surface that needs to be counter-rotate.
+     */
+    public void addChild(SurfaceControl.Transaction t, SurfaceControl child) {
+        if (mSurface == null) return;
+        t.reparent(child, mSurface);
+        mRotateChildren.add(child);
+    }
+
+    /**
+     * Clean-up. This undoes any reparenting and effectively stops the counter-rotation.
+     */
+    public void cleanUp(SurfaceControl rootLeash) {
+        if (mSurface == null) return;
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        for (int i = mRotateChildren.size() - 1; i >= 0; --i) {
+            t.reparent(mRotateChildren.get(i), rootLeash);
+        }
+        t.remove(mSurface);
+        t.apply();
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 9dd25fe..3ca5b9c 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -25,11 +25,17 @@
 
 android_test {
     name: "WMShellFlickerTests",
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     manifest: "AndroidManifest.xml",
     test_config: "AndroidTest.xml",
     platform_apis: true,
     certificate: "platform",
+    optimize: {
+        enabled: false,
+    },
     test_suites: ["device-tests"],
     libs: ["android.test.runner"],
     static_libs: [
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
index e6d32ff..06df956 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
@@ -42,6 +42,9 @@
     <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
     <!-- ATM.removeRootTasksWithActivityTypes() -->
     <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
+    <!-- Enable bubble notification-->
+    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
+
     <!-- Allow the test to write directly to /sdcard/ -->
     <application android:requestLegacyExternalStorage="true">
         <uses-library android:name="android.test.runner"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index c5b5b91..c07f0eb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -14,99 +14,103 @@
  * limitations under the License.
  */
 
+@file:JvmName("CommonAssertions")
 package com.android.wm.shell.flicker
 
 import android.graphics.Region
 import android.view.Surface
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.traces.layers.getVisibleBounds
+import com.android.server.wm.traces.common.FlickerComponentName
 
-fun FlickerTestParameter.appPairsDividerIsVisible() {
+fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() {
     assertLayersEnd {
-        this.isVisible(APP_PAIR_SPLIT_DIVIDER)
+        this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT)
     }
 }
 
-fun FlickerTestParameter.appPairsDividerIsInvisible() {
+fun FlickerTestParameter.appPairsDividerIsInvisibleAtEnd() {
     assertLayersEnd {
-        this.notContains(APP_PAIR_SPLIT_DIVIDER)
+        this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT)
     }
 }
 
 fun FlickerTestParameter.appPairsDividerBecomesVisible() {
     assertLayers {
-        this.isInvisible(DOCKED_STACK_DIVIDER)
+        this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
-            .isVisible(DOCKED_STACK_DIVIDER)
+            .isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
     }
 }
 
-fun FlickerTestParameter.dockedStackDividerIsVisible() {
+fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() {
     assertLayersEnd {
-        this.isVisible(DOCKED_STACK_DIVIDER)
+        this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
     }
 }
 
 fun FlickerTestParameter.dockedStackDividerBecomesVisible() {
     assertLayers {
-        this.isInvisible(DOCKED_STACK_DIVIDER)
+        this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
-            .isVisible(DOCKED_STACK_DIVIDER)
+            .isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
     }
 }
 
 fun FlickerTestParameter.dockedStackDividerBecomesInvisible() {
     assertLayers {
-        this.isVisible(DOCKED_STACK_DIVIDER)
+        this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
-            .isInvisible(DOCKED_STACK_DIVIDER)
+            .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
     }
 }
 
-fun FlickerTestParameter.dockedStackDividerIsInvisible() {
+fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() {
     assertLayersEnd {
-        this.notContains(DOCKED_STACK_DIVIDER)
+        this.notContains(DOCKED_STACK_DIVIDER_COMPONENT)
     }
 }
 
-fun FlickerTestParameter.appPairsPrimaryBoundsIsVisible(rotation: Int, primaryLayerName: String) {
+fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd(
+    rotation: Int,
+    primaryComponent: FlickerComponentName
+) {
     assertLayersEnd {
-        val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
-        visibleRegion(primaryLayerName)
+        val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+        visibleRegion(primaryComponent)
             .coversExactly(getPrimaryRegion(dividerRegion, rotation))
     }
 }
 
-fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisible(
+fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd(
     rotation: Int,
-    primaryLayerName: String
+    primaryComponent: FlickerComponentName
 ) {
     assertLayersEnd {
-        val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER)
-        visibleRegion(primaryLayerName)
+        val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region
+        visibleRegion(primaryComponent)
             .coversExactly(getPrimaryRegion(dividerRegion, rotation))
     }
 }
 
-fun FlickerTestParameter.appPairsSecondaryBoundsIsVisible(
+fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd(
     rotation: Int,
-    secondaryLayerName: String
+    secondaryComponent: FlickerComponentName
 ) {
     assertLayersEnd {
-        val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
-        visibleRegion(secondaryLayerName)
+        val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+        visibleRegion(secondaryComponent)
             .coversExactly(getSecondaryRegion(dividerRegion, rotation))
     }
 }
 
-fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisible(
+fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd(
     rotation: Int,
-    secondaryLayerName: String
+    secondaryComponent: FlickerComponentName
 ) {
     assertLayersEnd {
-        val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER)
-        visibleRegion(secondaryLayerName)
+        val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region
+        visibleRegion(secondaryComponent)
             .coversExactly(getSecondaryRegion(dividerRegion, rotation))
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
index 03b93c7..40891f3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
+@file:JvmName("CommonConstants")
 package com.android.wm.shell.flicker
 
-const val IME_WINDOW_NAME = "InputMethod"
+import com.android.server.wm.traces.common.FlickerComponentName
+
 const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
-const val APP_PAIR_SPLIT_DIVIDER = "AppPairSplitDivider"
-const val DOCKED_STACK_DIVIDER = "DockedStackDivider"
\ No newline at end of file
+val APP_PAIR_SPLIT_DIVIDER_COMPONENT = FlickerComponentName("", "AppPairSplitDivider#")
+val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#")
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
index a6d6735..b63d9ff 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+@file:JvmName("WaitUtils")
 package com.android.wm.shell.flicker
 
 import android.os.SystemClock
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt
index ef9f742..038be9c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.apppairs
 
-import android.os.SystemClock
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -25,7 +24,7 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.wm.shell.flicker.appPairsDividerIsInvisible
+import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd
 import com.android.wm.shell.flicker.helpers.AppPairsHelper
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
@@ -61,7 +60,7 @@
                 // TODO pair apps through normal UX flow
                 executeShellCommand(
                     composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true))
-                SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+                nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) }
             }
         }
 
@@ -85,15 +84,13 @@
     @Test
     override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
 
-    @FlakyTest
+    @Presubmit
     @Test
-    override fun navBarLayerIsAlwaysVisible() {
-        super.navBarLayerIsAlwaysVisible()
-    }
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible()
+    fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd()
 
     @Presubmit
     @Test
@@ -103,8 +100,8 @@
             "Non resizeable app not initialized"
         }
         testSpec.assertWmEnd {
-            isVisible(nonResizeableApp.defaultWindowName)
-            isInvisible(primaryApp.defaultWindowName)
+            isAppWindowVisible(nonResizeableApp.component)
+            isAppWindowInvisible(primaryApp.component)
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
index db63c4c..bbc6b2d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.apppairs
 
-import android.os.SystemClock
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -25,10 +24,10 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.traces.layers.getVisibleBounds
-import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER
-import com.android.wm.shell.flicker.appPairsDividerIsVisible
+import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.AppPairsHelper
+import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,10 +53,14 @@
                 // TODO pair apps through normal UX flow
                 executeShellCommand(
                     composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
-                SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+                waitAppsShown(primaryApp, secondaryApp)
             }
         }
 
+    @Presubmit
+    @Test
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
     @FlakyTest
     @Test
     override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
@@ -68,14 +71,14 @@
 
     @Presubmit
     @Test
-    fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+    fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
     fun bothAppWindowsVisible() {
         testSpec.assertWmEnd {
-            isVisible(primaryApp.defaultWindowName)
-            isVisible(secondaryApp.defaultWindowName)
+            isAppWindowVisible(primaryApp.component)
+            isAppWindowVisible(secondaryApp.component)
         }
     }
 
@@ -83,10 +86,10 @@
     @Test
     fun appsEndingBounds() {
         testSpec.assertLayersEnd {
-            val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
-            visibleRegion(primaryApp.defaultWindowName)
+            val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+            visibleRegion(primaryApp.component)
                 .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion))
-            visibleRegion(secondaryApp.defaultWindowName)
+            visibleRegion(secondaryApp.component)
                 .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion))
         }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt
index c8d3423..bb784a8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.apppairs
 
-import android.os.SystemClock
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -25,7 +24,7 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.wm.shell.flicker.appPairsDividerIsVisible
+import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.AppPairsHelper
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
@@ -61,7 +60,7 @@
                 // TODO pair apps through normal UX flow
                 executeShellCommand(
                         composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true))
-                SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+                nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) }
             }
         }
 
@@ -77,6 +76,10 @@
         resetMultiWindowConfig(instrumentation)
     }
 
+    @Presubmit
+    @Test
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
     @FlakyTest
     @Test
     override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
@@ -87,7 +90,7 @@
 
     @Presubmit
     @Test
-    fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+    fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
@@ -97,8 +100,8 @@
             "Non resizeable app not initialized"
         }
         testSpec.assertWmEnd {
-            isVisible(nonResizeableApp.defaultWindowName)
-            isVisible(primaryApp.defaultWindowName)
+            isAppWindowVisible(nonResizeableApp.component)
+            isAppWindowVisible(primaryApp.component)
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt
index 83df836..a1a4db1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt
@@ -25,10 +25,10 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.traces.layers.getVisibleBounds
-import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER
-import com.android.wm.shell.flicker.appPairsDividerIsInvisible
+import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd
 import com.android.wm.shell.flicker.helpers.AppPairsHelper
+import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -51,9 +51,11 @@
         get() = {
             super.transition(this, it)
             setup {
-                executeShellCommand(
-                    composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
-                SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+                eachRun {
+                    executeShellCommand(
+                            composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
+                    waitAppsShown(primaryApp, secondaryApp)
+                }
             }
             transitions {
                 // TODO pair apps through normal UX flow
@@ -73,14 +75,14 @@
 
     @Presubmit
     @Test
-    fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible()
+    fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd()
 
     @Presubmit
     @Test
     fun bothAppWindowsInvisible() {
         testSpec.assertWmEnd {
-            isInvisible(primaryApp.defaultWindowName)
-            isInvisible(secondaryApp.defaultWindowName)
+            isAppWindowInvisible(primaryApp.component)
+            isAppWindowInvisible(secondaryApp.component)
         }
     }
 
@@ -88,10 +90,10 @@
     @Test
     fun appsStartingBounds() {
         testSpec.assertLayersStart {
-            val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
-            visibleRegion(primaryApp.defaultWindowName)
+            val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
+            visibleRegion(primaryApp.component)
                 .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion))
-            visibleRegion(secondaryApp.defaultWindowName)
+            visibleRegion(secondaryApp.component)
                 .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion))
         }
     }
@@ -100,16 +102,14 @@
     @Test
     fun appsEndingBounds() {
         testSpec.assertLayersEnd {
-            notContains(primaryApp.defaultWindowName)
-            notContains(secondaryApp.defaultWindowName)
+            notContains(primaryApp.component)
+            notContains(secondaryApp.component)
         }
     }
 
-    @FlakyTest
+    @Presubmit
     @Test
-    override fun navBarLayerIsAlwaysVisible() {
-        super.navBarLayerIsAlwaysVisible()
-    }
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt
index 1935bb9..9e20bbb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt
@@ -20,24 +20,23 @@
 import android.content.Context
 import android.platform.test.annotations.Presubmit
 import android.system.helpers.ActivityHelper
-import android.view.Surface
 import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.endRotation
 import com.android.server.wm.flicker.helpers.isRotated
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.repetitions
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.wm.shell.flicker.helpers.AppPairsHelper
 import com.android.wm.shell.flicker.helpers.BaseAppHelper
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow
@@ -55,7 +54,7 @@
     protected val activityHelper = ActivityHelper.getInstance()
     protected val appPairsHelper = AppPairsHelper(instrumentation,
         Components.SplitScreenActivity.LABEL,
-        Components.SplitScreenActivity.COMPONENT)
+        Components.SplitScreenActivity.COMPONENT.toFlickerComponent())
 
     protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation)
     protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation)
@@ -154,39 +153,33 @@
 
     @FlakyTest(bugId = 186510496)
     @Test
-    open fun navBarLayerIsAlwaysVisible() {
-        testSpec.navBarLayerIsAlwaysVisible()
+    open fun navBarLayerIsVisible() {
+        testSpec.navBarLayerIsVisible()
     }
 
     @Presubmit
     @Test
-    open fun statusBarLayerIsAlwaysVisible() {
-        testSpec.statusBarLayerIsAlwaysVisible()
+    open fun statusBarLayerIsVisible() {
+        testSpec.statusBarLayerIsVisible()
     }
 
     @Presubmit
     @Test
-    open fun navBarWindowIsAlwaysVisible() {
-        testSpec.navBarWindowIsAlwaysVisible()
+    open fun navBarWindowIsVisible() {
+        testSpec.navBarWindowIsVisible()
     }
 
     @Presubmit
     @Test
-    open fun statusBarWindowIsAlwaysVisible() {
-        testSpec.statusBarWindowIsAlwaysVisible()
+    open fun statusBarWindowIsVisible() {
+        testSpec.statusBarWindowIsVisible()
     }
 
     @Presubmit
     @Test
-    open fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(Surface.ROTATION_0,
-            testSpec.config.endRotation)
-    }
+    open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Presubmit
     @Test
-    open fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(Surface.ROTATION_0,
-            testSpec.config.endRotation)
-    }
+    open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt
index c875c00..56a2531 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.apppairs
 
-import android.os.SystemClock
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
@@ -28,10 +27,10 @@
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.endRotation
 import com.android.server.wm.flicker.helpers.setRotation
-import com.android.wm.shell.flicker.appPairsDividerIsVisible
-import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
+import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -57,41 +56,43 @@
             transitions {
                 executeShellCommand(composePairsCommand(
                     primaryTaskId, secondaryTaskId, true /* pair */))
-                SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+                waitAppsShown(primaryApp, secondaryApp)
                 setRotation(testSpec.config.endRotation)
             }
         }
 
-    @FlakyTest
+    @Presubmit
     @Test
-    override fun statusBarLayerIsAlwaysVisible() {
-        super.statusBarLayerIsAlwaysVisible()
-    }
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
+    @Presubmit
+    @Test
+    override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
 
     @Presubmit
     @Test
     fun bothAppWindowsVisible() {
         testSpec.assertWmEnd {
-            isVisible(primaryApp.defaultWindowName)
-                .isVisible(secondaryApp.defaultWindowName)
+            isAppWindowVisible(primaryApp.component)
+            isAppWindowVisible(secondaryApp.component)
         }
     }
 
     @Presubmit
     @Test
-    fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+    fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
 
-    @FlakyTest(bugId = 172776659)
+    @Presubmit
     @Test
-    fun appPairsPrimaryBoundsIsVisible() =
-        testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation,
-            primaryApp.defaultWindowName)
+    fun appPairsPrimaryBoundsIsVisibleAtEnd() =
+        testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+            primaryApp.component)
 
-    @FlakyTest(bugId = 172776659)
+    @FlakyTest
     @Test
-    fun appPairsSecondaryBoundsIsVisible() =
-        testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation,
-            secondaryApp.defaultWindowName)
+    fun appPairsSecondaryBoundsIsVisibleAtEnd() =
+        testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+            secondaryApp.component)
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt
index c3360ca..0699a4f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.apppairs
 
-import android.os.SystemClock
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
@@ -28,12 +27,10 @@
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.endRotation
 import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.appPairsDividerIsVisible
-import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible
-import com.android.wm.shell.flicker.helpers.AppPairsHelper
+import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -60,48 +57,50 @@
                 this.setRotation(testSpec.config.endRotation)
                 executeShellCommand(
                     composePairsCommand(primaryTaskId, secondaryTaskId, pair = true))
-                SystemClock.sleep(AppPairsHelper.TIMEOUT_MS)
+                waitAppsShown(primaryApp, secondaryApp)
             }
         }
 
     @Presubmit
     @Test
-    fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible()
+    fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
 
-    @FlakyTest
+    @Presubmit
     @Test
-    override fun statusBarLayerIsAlwaysVisible() {
-        super.statusBarLayerIsAlwaysVisible()
-    }
+    override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
 
     @Presubmit
     @Test
     fun bothAppWindowsVisible() {
         testSpec.assertWmEnd {
-            isVisible(primaryApp.defaultWindowName)
-            isVisible(secondaryApp.defaultWindowName)
+            isAppWindowVisible(primaryApp.component)
+            isAppWindowVisible(secondaryApp.component)
         }
     }
 
     @FlakyTest(bugId = 172776659)
     @Test
-    fun appPairsPrimaryBoundsIsVisible() =
-        testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation,
-            primaryApp.defaultWindowName)
+    fun appPairsPrimaryBoundsIsVisibleAtEnd() =
+        testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+            primaryApp.component)
 
     @FlakyTest(bugId = 172776659)
     @Test
-    fun appPairsSecondaryBoundsIsVisible() =
-        testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation,
-            secondaryApp.defaultWindowName)
+    fun appPairsSecondaryBoundsIsVisibleAtEnd() =
+        testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation,
+            secondaryApp.component)
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt
index 512fd9a..b95193a1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt
@@ -22,7 +22,10 @@
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import org.junit.Assume.assumeFalse
+import org.junit.Before
 import org.junit.Test
 
 abstract class RotateTwoLaunchedAppsTransition(
@@ -37,8 +40,8 @@
                 test {
                     device.wakeUpAndGoToHomeScreen()
                     this.setRotation(Surface.ROTATION_0)
-                    primaryApp.launchViaIntent()
-                    secondaryApp.launchViaIntent()
+                    primaryApp.launchViaIntent(wmHelper)
+                    secondaryApp.launchViaIntent(wmHelper)
                     updateTasksId()
                 }
             }
@@ -52,10 +55,17 @@
             }
         }
 
+    @Before
+    override fun setup() {
+        // AppPairs hasn't been updated to Shell Transition. There will be conflict on rotation.
+        assumeFalse(isShellTransitionsEnabled())
+        super.setup()
+    }
+
     @FlakyTest
     @Test
-    override fun navBarLayerIsAlwaysVisible() {
-        super.navBarLayerIsAlwaysVisible()
+    override fun navBarLayerIsVisible() {
+        super.navBarLayerIsVisible()
     }
 
     @FlakyTest
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
new file mode 100644
index 0000000..322d8b5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.bubble
+
+import android.app.INotificationManager
+import android.app.Instrumentation
+import android.app.NotificationManager
+import android.content.Context
+import android.os.ServiceManager
+import android.view.Surface
+import androidx.test.filters.FlakyTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE
+import com.android.server.wm.flicker.repetitions
+import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper
+import org.junit.Test
+import org.junit.runners.Parameterized
+
+/**
+ * Base configurations for Bubble flicker tests
+ */
+abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) {
+
+    protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    protected val context: Context = instrumentation.context
+    protected val testApp = LaunchBubbleHelper(instrumentation)
+
+    protected val notifyManager = INotificationManager.Stub.asInterface(
+            ServiceManager.getService(Context.NOTIFICATION_SERVICE))
+
+    protected val packageManager = context.getPackageManager()
+    protected val uid = packageManager.getApplicationInfo(
+            testApp.component.packageName, 0).uid
+
+    protected lateinit var addBubbleBtn: UiObject2
+    protected lateinit var cancelAllBtn: UiObject2
+
+    protected abstract val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+
+    @JvmOverloads
+    protected open fun buildTransition(
+        extraSpec: FlickerBuilder.(Map<String, Any?>) -> Unit = {}
+    ): FlickerBuilder.(Map<String, Any?>) -> Unit {
+        return { configuration ->
+
+            setup {
+                test {
+                    notifyManager.setBubblesAllowed(testApp.component.packageName,
+                            uid, NotificationManager.BUBBLE_PREFERENCE_ALL)
+                    testApp.launchViaIntent(wmHelper)
+                    addBubbleBtn = device.wait(Until.findObject(
+                            By.text("Add Bubble")), FIND_OBJECT_TIMEOUT)
+                    cancelAllBtn = device.wait(Until.findObject(
+                            By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT)
+                }
+            }
+
+            teardown {
+                notifyManager.setBubblesAllowed(testApp.component.packageName,
+                        uid, NotificationManager.BUBBLE_PREFERENCE_NONE)
+                testApp.exit()
+            }
+
+            extraSpec(this, configuration)
+        }
+    }
+
+    @FlakyTest
+    @Test
+    fun testAppIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
+    }
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            repeat { testSpec.config.repetitions }
+            transition(this, testSpec.config)
+        }
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
+                            repetitions = 5)
+        }
+
+        const val FIND_OBJECT_TIMEOUT = 2000L
+        const val SYSTEM_UI_PACKAGE = SYSTEMUI_PACKAGE
+        const val BUBBLE_RES_NAME = "bubble_view"
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
new file mode 100644
index 0000000..bfdcb36
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.bubble
+
+import android.content.Context
+import android.graphics.Point
+import android.util.DisplayMetrics
+import android.view.WindowManager
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a new activity from bubble.
+ *
+ * To run this test: `atest WMShellFlickerTests:DismissBubbleScreen`
+ *
+ * Actions:
+ *     Dismiss a bubble notification
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
+class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+
+    val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+    val displaySize = DisplayMetrics()
+
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition() {
+            setup {
+                eachRun {
+                    addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found")
+                }
+            }
+            transitions {
+                wm?.run { wm.getDefaultDisplay().getMetrics(displaySize) } ?: error("WM not found")
+                val dist = Point((displaySize.widthPixels / 2), displaySize.heightPixels)
+                val showBubble = device.wait(Until.findObject(
+                        By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT)
+                showBubble?.run { drag(dist, 1000) } ?: error("Show bubble not found")
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
new file mode 100644
index 0000000..42eeadf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.bubble
+
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a new activity from bubble.
+ *
+ * To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen`
+ *
+ * Actions:
+ *     Launch an app and enable app's bubble notification
+ *     Send a bubble notification
+ *     The activity for the bubble is launched
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
+class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition() {
+            setup {
+                test {
+                    addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found")
+                }
+            }
+            transitions {
+                val showBubble = device.wait(Until.findObject(
+                        By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT)
+                showBubble?.run { showBubble.click() } ?: error("Bubble notify not found")
+                device.pressBack()
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
new file mode 100644
index 0000000..47e8c0c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.bubble
+
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test creating a bubble notification
+ *
+ * To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen`
+ *
+ * Actions:
+ *     Launch an app and enable app's bubble notification
+ *     Send a bubble notification
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
+class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition() {
+            transitions {
+                addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found")
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
new file mode 100644
index 0000000..194e28f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.bubble
+
+import android.os.SystemClock
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a new activity from bubble.
+ *
+ * To run this test: `atest WMShellFlickerTests:MultiBubblesScreen`
+ *
+ * Actions:
+ *     Switch in different bubble notifications
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
+class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition() {
+            setup {
+                test {
+                    for (i in 1..3) {
+                        addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found")
+                    }
+                    val showBubble = device.wait(Until.findObject(
+                            By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT)
+                    showBubble?.run { showBubble.click() } ?: error("Show bubble not found")
+                    SystemClock.sleep(1000)
+                }
+            }
+            transitions {
+                val bubbles = device.wait(Until.findObjects(
+                        By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT)
+                for (entry in bubbles) {
+                    entry?.run { entry.click() } ?: error("Bubble not found")
+                    SystemClock.sleep(1000)
+                }
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
index 5b8cfb8..623055f6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
@@ -17,14 +17,15 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
 import android.graphics.Region
+import com.android.server.wm.flicker.Flicker
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.traces.common.FlickerComponentName
 
 class AppPairsHelper(
     instrumentation: Instrumentation,
     activityLabel: String,
-    component: ComponentName
+    component: FlickerComponentName
 ) : BaseAppHelper(instrumentation, activityLabel, component) {
     fun getPrimaryBounds(dividerBounds: Region): android.graphics.Region {
         val primaryAppBounds = Region(0, 0, dividerBounds.bounds.right,
@@ -43,5 +44,17 @@
     companion object {
         const val TEST_REPETITIONS = 1
         const val TIMEOUT_MS = 3_000L
+
+        fun Flicker.waitAppsShown(app1: SplitScreenHelper?, app2: SplitScreenHelper?) {
+            wmHelper.waitFor("primaryAndSecondaryAppsVisible") { dump ->
+                val primaryAppVisible = app1?.let {
+                    dump.wmState.isWindowSurfaceShown(app1.defaultWindowName)
+                } ?: false
+                val secondaryAppVisible = app2?.let {
+                    dump.wmState.isWindowSurfaceShown(app2.defaultWindowName)
+                } ?: false
+                primaryAppVisible && secondaryAppVisible
+            }
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
index 4fe69ad..57bc0d5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
@@ -17,9 +17,9 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
 import android.content.pm.PackageManager.FEATURE_LEANBACK
 import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY
+import android.os.SystemProperties
 import android.support.test.launcherhelper.LauncherStrategyFactory
 import android.util.Log
 import androidx.test.uiautomator.By
@@ -27,13 +27,13 @@
 import androidx.test.uiautomator.Until
 import com.android.compatibility.common.util.SystemUtil
 import com.android.server.wm.flicker.helpers.StandardAppHelper
-import com.android.server.wm.traces.parser.toWindowName
+import com.android.server.wm.traces.common.FlickerComponentName
 import java.io.IOException
 
 abstract class BaseAppHelper(
     instrumentation: Instrumentation,
     launcherName: String,
-    component: ComponentName
+    component: FlickerComponentName
 ) : StandardAppHelper(
     instrumentation,
     launcherName,
@@ -60,6 +60,9 @@
     companion object {
         private const val APP_CLOSE_WAIT_TIME_MS = 3_000L
 
+        fun isShellTransitionsEnabled() =
+                SystemProperties.getBoolean("persist.debug.shell_transit", false)
+
         fun executeShellCommand(instrumentation: Instrumentation, cmd: String) {
             try {
                 SystemUtil.runShellCommand(instrumentation, cmd)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt
index b4ae187..471e010 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt
@@ -17,10 +17,11 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.wm.shell.flicker.testapp.Components
 
 class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
     instrumentation,
     Components.FixedActivity.LABEL,
-    Components.FixedActivity.COMPONENT
+    Components.FixedActivity.COMPONENT.toFlickerComponent()
 )
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
index cac46fe..0f00ede 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
@@ -21,13 +21,14 @@
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
 import com.android.server.wm.flicker.helpers.FIND_TIMEOUT
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import com.android.wm.shell.flicker.testapp.Components
 
 open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
     instrumentation,
     Components.ImeActivity.LABEL,
-    Components.ImeActivity.COMPONENT
+    Components.ImeActivity.COMPONENT.toFlickerComponent()
 ) {
     /**
      * Opens the IME and wait for it to be displayed
@@ -61,7 +62,7 @@
         if (wmHelper == null) {
             device.waitForIdle()
         } else {
-            require(wmHelper.waitImeWindowShown()) { "IME did not appear" }
+            require(wmHelper.waitImeShown()) { "IME did not appear" }
         }
     }
 
@@ -78,7 +79,7 @@
             if (wmHelper == null) {
                 uiDevice.waitForIdle()
             } else {
-                require(wmHelper.waitImeWindowGone()) { "IME did did not close" }
+                require(wmHelper.waitImeGone()) { "IME did did not close" }
             }
         } else {
             // While pressing the back button should close the IME on TV as well, it may also lead
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt
new file mode 100644
index 0000000..6695c17
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.helpers
+
+import android.app.Instrumentation
+import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.wm.shell.flicker.testapp.Components
+
+class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper(
+    instrumentation,
+    Components.LaunchBubbleActivity.LABEL,
+    Components.LaunchBubbleActivity.COMPONENT.toFlickerComponent()
+) {
+
+    companion object {
+        const val TEST_REPETITIONS = 1
+        const val TIMEOUT_MS = 3_000L
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt
index 7f99e62..12ccbaf 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt
@@ -17,14 +17,14 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
 import android.content.Context
 import android.provider.Settings
+import com.android.server.wm.traces.common.FlickerComponentName
 
 class MultiWindowHelper(
     instrumentation: Instrumentation,
     activityLabel: String,
-    componentsInfo: ComponentName
+    componentsInfo: FlickerComponentName
 ) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) {
 
     companion object {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
index f4dd7de..2357b0d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
@@ -17,12 +17,16 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
+import android.graphics.Rect
 import android.media.session.MediaController
 import android.media.session.MediaSessionManager
 import android.os.SystemClock
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.FIND_TIMEOUT
 import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow
 import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild
@@ -31,7 +35,7 @@
 class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
     instrumentation,
     Components.PipActivity.LABEL,
-    Components.PipActivity.COMPONENT
+    Components.PipActivity.COMPONENT.toFlickerComponent()
 ) {
     private val mediaSessionManager: MediaSessionManager
         get() = context.getSystemService(MediaSessionManager::class.java)
@@ -62,7 +66,7 @@
         stringExtras: Map<String, String>
     ) {
         super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras)
-        wmHelper.waitFor { it.wmState.hasPipWindow() }
+        wmHelper.waitFor("hasPipWindow") { it.wmState.hasPipWindow() }
     }
 
     private fun focusOnObject(selector: BySelector): Boolean {
@@ -84,7 +88,11 @@
         clickObject(ENTER_PIP_BUTTON_ID)
 
         // Wait on WMHelper or simply wait for 3 seconds
-        wmHelper?.waitFor { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000)
+        wmHelper?.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000)
+        // when entering pip, the dismiss button is visible at the start. to ensure the pip
+        // animation is complete, wait until the pip dismiss button is no longer visible. 
+        // b/176822698: dismiss-only state will be removed in the future
+        uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT)
     }
 
     fun clickStartMediaSessionButton() {
@@ -113,61 +121,61 @@
         }
     }
 
+    private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect {
+        val windowRegion = wmHelper.getWindowRegion(component)
+        require(!windowRegion.isEmpty) {
+            "Unable to find a PIP window in the current state"
+        }
+        return windowRegion.bounds
+    }
+
     /**
-     * Expands the pip window and dismisses it by clicking on the X button.
-     *
-     * Note, currently the View coordinates reported by the accessibility are relative to
-     * the window, so the correct coordinates need to be calculated
-     *
-     * For example, in a PIP window located at Rect(508, 1444 - 1036, 1741), the
-     * dismiss button coordinates are shown as Rect(650, 0 - 782, 132), with center in
-     * Point(716, 66), instead of Point(970, 1403)
-     *
-     * See b/179337864
+     * Taps the pip window and dismisses it by clicking on the X button.
      */
     fun closePipWindow(wmHelper: WindowManagerStateHelper) {
         if (isTelevision) {
             uiDevice.closeTvPipWindow()
         } else {
-            expandPipWindow(wmHelper)
-            val exitPipObject = uiDevice.findObject(By.res(SYSTEMUI_PACKAGE, "dismiss"))
-            requireNotNull(exitPipObject) { "PIP window dismiss button not found" }
-            val dismissButtonBounds = exitPipObject.visibleBounds
+            val windowRect = getWindowRect(wmHelper)
+            uiDevice.click(windowRect.centerX(), windowRect.centerY())
+            // search and interact with the dismiss button
+            val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
+            uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
+            val dismissPipObject = uiDevice.findObject(dismissSelector)
+                    ?: error("PIP window dismiss button not found")
+            val dismissButtonBounds = dismissPipObject.visibleBounds
             uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY())
         }
 
         // Wait for animation to complete.
-        wmHelper.waitFor { !it.wmState.hasPipWindow() }
+        wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() }
         wmHelper.waitForHomeActivityVisible()
     }
 
     /**
-     * Click once on the PIP window to expand it
+     * Close the pip window by pressing the expand button
      */
-    fun expandPipWindow(wmHelper: WindowManagerStateHelper) {
-        val windowRegion = wmHelper.getWindowRegion(component)
-        require(!windowRegion.isEmpty) {
-            "Unable to find a PIP window in the current state"
-        }
-        val windowRect = windowRegion.bounds
+    fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) {
+        val windowRect = getWindowRect(wmHelper)
         uiDevice.click(windowRect.centerX(), windowRect.centerY())
-        // Ensure WindowManagerService wait until all animations have completed
+        // search and interact with the expand button
+        val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button")
+        uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT)
+        val expandPipObject = uiDevice.findObject(expandSelector)
+                ?: error("PIP window expand button not found")
+        val expandButtonBounds = expandPipObject.visibleBounds
+        uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY())
+        wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() }
         wmHelper.waitForAppTransitionIdle()
-        mInstrumentation.uiAutomation.syncInputTransactions()
     }
 
     /**
-     * Double click on the PIP window to reopen to app
+     * Double click on the PIP window to expand it
      */
-    fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) {
-        val windowRegion = wmHelper.getWindowRegion(component)
-        require(!windowRegion.isEmpty) {
-            "Unable to find a PIP window in the current state"
-        }
-        val windowRect = windowRegion.bounds
+    fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) {
+        val windowRect = getWindowRect(wmHelper)
         uiDevice.click(windowRect.centerX(), windowRect.centerY())
         uiDevice.click(windowRect.centerX(), windowRect.centerY())
-        wmHelper.waitFor { !it.wmState.hasPipWindow() }
         wmHelper.waitForAppTransitionIdle()
     }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt
index ba13e38..4d0fbc4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt
@@ -17,10 +17,11 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.wm.shell.flicker.testapp.Components
 
 class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
     instrumentation,
     Components.SimpleActivity.LABEL,
-    Components.SimpleActivity.COMPONENT
+    Components.SimpleActivity.COMPONENT.toFlickerComponent()
 )
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
index 901b7a3..0ec9b2d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
@@ -17,32 +17,39 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
+import android.content.res.Resources
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.wm.shell.flicker.testapp.Components
 
 class SplitScreenHelper(
     instrumentation: Instrumentation,
     activityLabel: String,
-    componentsInfo: ComponentName
+    componentsInfo: FlickerComponentName
 ) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) {
 
     companion object {
         const val TEST_REPETITIONS = 1
         const val TIMEOUT_MS = 3_000L
 
+        // TODO: remove all legacy split screen flicker tests when legacy split screen is fully
+        //  deprecated.
+        fun isUsingLegacySplit(): Boolean =
+                Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useLegacySplit)
+
         fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper =
             SplitScreenHelper(instrumentation,
                 Components.SplitScreenActivity.LABEL,
-                Components.SplitScreenActivity.COMPONENT)
+                Components.SplitScreenActivity.COMPONENT.toFlickerComponent())
 
         fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper =
             SplitScreenHelper(instrumentation,
                 Components.SplitScreenSecondaryActivity.LABEL,
-                Components.SplitScreenSecondaryActivity.COMPONENT)
+                Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent())
 
         fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper =
             SplitScreenHelper(instrumentation,
                 Components.NonResizeableActivity.LABEL,
-                Components.NonResizeableActivity.COMPONENT)
+                Components.NonResizeableActivity.COMPONENT.toFlickerComponent())
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt
index 4f12f2b..bd44d08 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt
@@ -18,20 +18,21 @@
 
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.HOME_WINDOW_TITLE
-import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
+import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -48,7 +49,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
+@Group4
 class EnterSplitScreenDockActivity(
     testSpec: FlickerTestParameter
 ) : LegacySplitScreenTransition(testSpec) {
@@ -60,16 +61,16 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME, LIVE_WALLPAPER_PACKAGE_NAME,
-            splitScreenApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, *HOME_WINDOW_TITLE)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT, LIVE_WALLPAPER_COMPONENT,
+            splitScreenApp.component, FlickerComponentName.SPLASH_SCREEN,
+                FlickerComponentName.SNAPSHOT, LAUNCHER_COMPONENT)
 
     @Presubmit
     @Test
-    fun dockedStackPrimaryBoundsIsVisible() =
-        testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
-            splitScreenApp.defaultWindowName)
+    fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            splitScreenApp.component)
 
     @Presubmit
     @Test
@@ -77,27 +78,39 @@
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
     fun appWindowIsVisible() {
         testSpec.assertWmEnd {
-            isVisible(splitScreenApp.defaultWindowName)
+            isAppWindowVisible(splitScreenApp.component)
         }
     }
 
+    @FlakyTest
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
             return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
                 repetitions = SplitScreenHelper.TEST_REPETITIONS,
-                supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910
+                supportedRotations = listOf(Surface.ROTATION_0), // bugId = 179116910
+                supportedNavigationModes = listOf(
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
             )
         }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt
index f91f634..625d48b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt
@@ -22,10 +22,11 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -42,6 +43,7 @@
 @RunWith(Parameterized::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
 class EnterSplitScreenFromDetachedRecentTask(
     testSpec: FlickerTestParameter
 ) : LegacySplitScreenTransition(testSpec) {
@@ -61,24 +63,34 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME,
-                splitScreenApp.defaultWindowName)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT,
+                FlickerComponentName.SPLASH_SCREEN,
+                FlickerComponentName.SNAPSHOT,
+                splitScreenApp.component)
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
     fun appWindowIsVisible() {
         testSpec.assertWmEnd {
-            isVisible(splitScreenApp.defaultWindowName)
+            isAppWindowVisible(splitScreenApp.component)
         }
     }
 
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt
index 85ded8a..2ed2806 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt
@@ -22,18 +22,17 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.appWindowBecomesVisible
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible
+import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -49,7 +48,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
+@Group4
 class EnterSplitScreenLaunchToSide(
     testSpec: FlickerTestParameter
 ) : LegacySplitScreenTransition(testSpec) {
@@ -62,22 +61,22 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName,
-            secondaryApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component,
+            secondaryApp.component, FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
 
     @Presubmit
     @Test
-    fun dockedStackPrimaryBoundsIsVisible() =
-        testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
-            splitScreenApp.defaultWindowName)
+    fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            splitScreenApp.component)
 
     @Presubmit
     @Test
-    fun dockedStackSecondaryBoundsIsVisible() =
-        testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation,
-            secondaryApp.defaultWindowName)
+    fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            secondaryApp.component)
 
     @Presubmit
     @Test
@@ -85,15 +84,35 @@
 
     @Presubmit
     @Test
-    fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName)
+    fun appWindowBecomesVisible() {
+        testSpec.assertWm {
+            // when the app is launched, first the activity becomes visible, then the
+            // SnapshotStartingWindow appears and then the app window becomes visible.
+            // Because we log WM once per frame, sometimes the activity and the window
+            // become visible in the same entry, sometimes not, thus it is not possible to
+            // assert the visibility of the activity here
+            this.isAppWindowInvisible(secondaryApp.component)
+                    .then()
+                    // during re-parenting, the window may disappear and reappear from the
+                    // trace, this occurs because we log only 1x per frame
+                    .notContains(secondaryApp.component, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(secondaryApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt
index e958bf3..ee6cf34 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt
@@ -22,11 +22,11 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.canSplitScreen
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
@@ -50,7 +50,7 @@
 @RunWith(Parameterized::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group1
+@Group4
 class EnterSplitScreenNotSupportNonResizable(
     testSpec: FlickerTestParameter
 ) : LegacySplitScreenTransition(testSpec) {
@@ -70,12 +70,12 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME,
-            WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME,
-            nonResizeableApp.defaultWindowName,
-            splitScreenApp.defaultWindowName)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT,
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT,
+            nonResizeableApp.component,
+            splitScreenApp.component)
 
     @Before
     override fun setup() {
@@ -91,7 +91,12 @@
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible()
+    fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt
index d3acc82..163b6ffda 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt
@@ -25,8 +25,8 @@
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
@@ -67,12 +67,12 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME,
-                nonResizeableApp.defaultWindowName,
-                splitScreenApp.defaultWindowName)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT,
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT,
+            nonResizeableApp.component,
+            splitScreenApp.component)
 
     @Before
     override fun setup() {
@@ -88,16 +88,21 @@
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
     fun appWindowIsVisible() {
         testSpec.assertWmEnd {
-            isVisible(nonResizeableApp.defaultWindowName)
+            isAppWindowVisible(nonResizeableApp.component)
         }
     }
 
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt
index bad4683..2b629b0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.flicker.legacysplitscreen
 
-import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.Postsubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -24,15 +24,13 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesInVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom
 import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.layerBecomesInvisible
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -67,31 +65,52 @@
                 }
             }
             transitions {
-                device.exitSplitScreenFromBottom()
+                device.exitSplitScreenFromBottom(wmHelper)
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN,
+            splitScreenApp.component, secondaryApp.component,
+            FlickerComponentName.SNAPSHOT)
 
-    @Presubmit
+    @Postsubmit
     @Test
-    fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(DOCKED_STACK_DIVIDER)
+    fun layerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
+                    .then()
+                    .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
+        }
+    }
 
     @FlakyTest
     @Test
-    fun appWindowBecomesInVisible() =
-        testSpec.appWindowBecomesInVisible(secondaryApp.defaultWindowName)
+    fun appWindowBecomesInVisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(secondaryApp.component)
+                    .then()
+                    .isAppWindowInvisible(secondaryApp.component)
+        }
+    }
 
-    @Presubmit
+    @Postsubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
-    @Presubmit
+    @Postsubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
+
+    @FlakyTest
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @FlakyTest
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt
index 76dcd8b..95fe3be 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt
@@ -24,15 +24,13 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesInVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.layerBecomesInvisible
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -71,31 +69,52 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
-
-    @FlakyTest(bugId = 175687842)
-    @Test
-    fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible()
-
-    @FlakyTest
-    @Test
-    fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName)
-
-    @FlakyTest
-    @Test
-    fun appWindowBecomesInVisible() =
-        testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN,
+            splitScreenApp.component, secondaryApp.component,
+            FlickerComponentName.SNAPSHOT)
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
+
+    @FlakyTest
+    @Test
+    fun layerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(splitScreenApp.component)
+                    .then()
+                    .isInvisible(splitScreenApp.component)
+        }
+    }
+
+    @FlakyTest
+    @Test
+    fun appWindowBecomesInVisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(splitScreenApp.component)
+                    .then()
+                    .isAppWindowInvisible(splitScreenApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt
index d0a64b3..f7d628d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt
@@ -23,15 +23,11 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesInVisible
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.layerBecomesInvisible
-import com.android.server.wm.flicker.layerBecomesVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
@@ -72,11 +68,11 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME,
-            nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName,
-            WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+            nonResizeableApp.component, splitScreenApp.component,
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
 
     @Before
     override fun setup() {
@@ -92,44 +88,109 @@
 
     @Presubmit
     @Test
-    fun resizableAppLayerBecomesInvisible() =
-            testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName)
+    fun resizableAppLayerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(splitScreenApp.component)
+                    .then()
+                    .isInvisible(splitScreenApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun nonResizableAppLayerBecomesVisible() =
-            testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+    fun nonResizableAppLayerBecomesVisible() {
+        testSpec.assertLayers {
+            this.notContains(nonResizeableApp.component)
+                    .then()
+                    .isInvisible(nonResizeableApp.component)
+                    .then()
+                    .isVisible(nonResizeableApp.component)
+        }
+    }
+
+    /**
+     * Assets that [splitScreenApp] exists at the start of the trace and, once it becomes
+     * invisible, it remains invisible until the end of the trace.
+     */
+    @Presubmit
+    @Test
+    fun resizableAppWindowBecomesInvisible() {
+        testSpec.assertWm {
+            // when the activity gets PAUSED the window may still be marked as visible
+            // it will be updated in the next log entry. This occurs because we record 1x
+            // per frame, thus ignore activity check here
+            this.isAppWindowVisible(splitScreenApp.component)
+                    .then()
+                    // immediately after the window (after onResume and before perform relayout)
+                    // the activity is invisible. This may or not be logged, since we record 1x
+                    // per frame, thus ignore activity check here
+                    .isAppWindowInvisible(splitScreenApp.component)
+        }
+    }
+
+    /**
+     * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then
+     * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes
+     * visible, it remains visible until the end of the trace.
+     */
+    @Presubmit
+    @Test
+    fun nonResizableAppWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.notContains(nonResizeableApp.component)
+                    .then()
+                    // we log once per frame, upon logging, window may be visible or not depending
+                    // on what was processed until that moment. Both behaviors are correct
+                    .isAppWindowInvisible(nonResizeableApp.component, isOptional = true)
+                    .then()
+                    // immediately after the window (after onResume and before perform relayout)
+                    // the activity is invisible. This may or not be logged, since we record 1x
+                    // per frame, thus ignore activity check here
+                    .isAppWindowVisible(nonResizeableApp.component)
+        }
+    }
+
+    /**
+     * Asserts that both the app window and the activity are visible at the end of the trace
+     */
+    @Presubmit
+    @Test
+    fun nonResizableAppWindowBecomesVisibleAtEnd() {
+        testSpec.assertWmEnd {
+            isAppWindowVisible(nonResizeableApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun resizableAppWindowBecomesInvisible() =
-            testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName)
-
-    @Presubmit
-    @Test
-    fun nonResizableAppWindowBecomesVisible() =
-            testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
-
-    @Presubmit
-    @Test
-    fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible()
+    fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
 
     @Presubmit
     @Test
     fun onlyNonResizableAppWindowIsVisibleAtEnd() {
         testSpec.assertWmEnd {
-            isInvisible(splitScreenApp.defaultWindowName)
-            isVisible(nonResizeableApp.defaultWindowName)
+            isAppWindowInvisible(splitScreenApp.component)
+            isAppWindowVisible(nonResizeableApp.component)
         }
     }
 
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
             return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
-                repetitions = SplitScreenHelper.TEST_REPETITIONS,
-                supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
+                    repetitions = SplitScreenHelper.TEST_REPETITIONS,
+                    supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt
index c26c05f..a5c6571 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt
@@ -23,13 +23,11 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.layerBecomesVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
@@ -70,11 +68,11 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME,
-            nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName,
-            WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+            nonResizeableApp.component, splitScreenApp.component,
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
 
     @Before
     override fun setup() {
@@ -90,27 +88,59 @@
 
     @Presubmit
     @Test
-    fun nonResizableAppLayerBecomesVisible() =
-            testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+    fun nonResizableAppLayerBecomesVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(nonResizeableApp.component)
+                    .then()
+                    .isVisible(nonResizeableApp.component)
+        }
+    }
+
+    /**
+     * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then
+     * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes
+     * visible, it remains visible until the end of the trace.
+     */
+    @Presubmit
+    @Test
+    fun nonResizableAppWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.notContains(nonResizeableApp.component)
+                    .then()
+                    // we log once per frame, upon logging, window may be visible or not depending
+                    // on what was processed until that moment. Both behaviors are correct
+                    .isAppWindowInvisible(nonResizeableApp.component, isOptional = true)
+                    .then()
+                    // immediately after the window (after onResume and before perform relayout)
+                    // the activity is invisible. This may or not be logged, since we record 1x
+                    // per frame, thus ignore activity check here
+                    .isAppWindowVisible(nonResizeableApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun nonResizableAppWindowBecomesVisible() =
-            testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
-
-    @Presubmit
-    @Test
-    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
     fun bothAppsWindowsAreVisibleAtEnd() {
         testSpec.assertWmEnd {
-            isVisible(splitScreenApp.defaultWindowName)
-            isVisible(nonResizeableApp.defaultWindowName)
+            isAppWindowVisible(splitScreenApp.component)
+            isAppWindowVisible(nonResizeableApp.component)
         }
     }
 
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt
index fb17589..6f486b0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.flicker.legacysplitscreen
 
+import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.RequiresDevice
@@ -23,16 +24,12 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesInVisible
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.layerBecomesInvisible
-import com.android.server.wm.flicker.layerBecomesVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsInvisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
@@ -73,11 +70,11 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME,
-                splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+            TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component,
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
 
     @Before
     override fun setup() {
@@ -93,37 +90,73 @@
 
     @Presubmit
     @Test
-    fun resizableAppLayerBecomesInvisible() =
-            testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName)
+    fun resizableAppLayerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(splitScreenApp.component)
+                    .then()
+                    .isInvisible(splitScreenApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun nonResizableAppLayerBecomesVisible() =
-            testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+    fun nonResizableAppLayerBecomesVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(nonResizeableApp.component)
+                    .then()
+                    .isVisible(nonResizeableApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun resizableAppWindowBecomesInvisible() =
-        testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName)
+    fun resizableAppWindowBecomesInvisible() {
+        testSpec.assertWm {
+            // when the activity gets PAUSED the window may still be marked as visible
+            // it will be updated in the next log entry. This occurs because we record 1x
+            // per frame, thus ignore activity check here
+            this.isAppWindowVisible(splitScreenApp.component)
+                    .then()
+                    // immediately after the window (after onResume and before perform relayout)
+                    // the activity is invisible. This may or not be logged, since we record 1x
+                    // per frame, thus ignore activity check here
+                    .isAppWindowInvisible(splitScreenApp.component)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun nonResizableAppWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(nonResizeableApp.component)
+                    .then()
+                    .isAppWindowVisible(nonResizeableApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun nonResizableAppWindowBecomesVisible() =
-        testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
-
-    @Presubmit
-    @Test
-    fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible()
+    fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd()
 
     @Presubmit
     @Test
     fun onlyNonResizableAppWindowIsVisibleAtEnd() {
         testSpec.assertWmEnd {
-            isInvisible(splitScreenApp.defaultWindowName)
-            isVisible(nonResizeableApp.defaultWindowName)
+            isAppWindowInvisible(splitScreenApp.component)
+            isAppWindowVisible(nonResizeableApp.component)
         }
     }
 
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt
index a9c28ef..f03c927 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt
@@ -23,14 +23,12 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
-import com.android.server.wm.flicker.layerBecomesVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
@@ -71,11 +69,11 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME,
-                splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT,
+            TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component,
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
 
     @Before
     override fun setup() {
@@ -91,27 +89,60 @@
 
     @Presubmit
     @Test
-    fun nonResizableAppLayerBecomesVisible() =
-            testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName)
+    fun nonResizableAppLayerBecomesVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(nonResizeableApp.component)
+                    .then()
+                    .isVisible(nonResizeableApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun nonResizableAppWindowBecomesVisible() =
-        testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName)
+    fun nonResizableAppWindowBecomesVisible() {
+        testSpec.assertWm {
+            // when the app is launched, first the activity becomes visible, then the
+            // SnapshotStartingWindow appears and then the app window becomes visible.
+            // Because we log WM once per frame, sometimes the activity and the window
+            // become visible in the same entry, sometimes not, thus it is not possible to
+            // assert the visibility of the activity here
+            this.isAppWindowInvisible(nonResizeableApp.component)
+                    .then()
+                    // during re-parenting, the window may disappear and reappear from the
+                    // trace, this occurs because we log only 1x per frame
+                    .notContains(nonResizeableApp.component, isOptional = true)
+                    .then()
+                    // if the window reappears after re-parenting it will most likely not
+                    // be visible in the first log entry (because we log only 1x per frame)
+                    .isAppWindowInvisible(nonResizeableApp.component, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(nonResizeableApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
     fun bothAppsWindowsAreVisibleAtEnd() {
         testSpec.assertWmEnd {
-            isVisible(splitScreenApp.defaultWindowName)
-            isVisible(nonResizeableApp.defaultWindowName)
+            isAppWindowVisible(splitScreenApp.component)
+            isAppWindowVisible(nonResizeableApp.component)
         }
     }
 
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt
index a4d2ab5..2ccd03b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt
@@ -16,10 +16,9 @@
 
 package com.android.wm.shell.flicker.legacysplitscreen
 
+import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import android.view.Surface
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
@@ -27,21 +26,19 @@
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.endRotation
-import com.android.server.wm.flicker.focusDoesNotChange
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.exitSplitScreen
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.layerBecomesInvisible
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible
 import com.android.wm.shell.flicker.helpers.SimpleAppHelper
 import org.junit.FixMethodOrder
@@ -62,8 +59,6 @@
 class LegacySplitScreenToLauncher(
     testSpec: FlickerTestParameter
 ) : LegacySplitScreenTransition(testSpec) {
-    private val launcherPackageName = LauncherStrategyFactory.getInstance(instrumentation)
-        .launcherStrategy.supportedLauncherPackage
     private val testApp = SimpleAppHelper(instrumentation)
 
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
@@ -90,51 +85,69 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(launcherPackageName, WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
 
     @Presubmit
     @Test
-    fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.endRotation)
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Presubmit
     @Test
-    fun statusBarLayerRotatesScales() =
-        testSpec.statusBarLayerRotatesScales(testSpec.config.endRotation)
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
     @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
-    @Presubmit
+    @Postsubmit
     @Test
     fun dockedStackDividerBecomesInvisible() = testSpec.dockedStackDividerBecomesInvisible()
 
+    @Postsubmit
+    @Test
+    fun layerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+                    .then()
+                    .isInvisible(testApp.component)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun focusDoesNotChange() {
+        testSpec.assertEventLog {
+            this.focusDoesNotChange()
+        }
+    }
+
     @Presubmit
     @Test
-    fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(testApp.getPackage())
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
-    @FlakyTest(bugId = 151179149)
+    @Presubmit
     @Test
-    fun focusDoesNotChange() = testSpec.focusDoesNotChange()
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt
index e8d4d1e..661c8b6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt
@@ -31,11 +31,14 @@
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
 import com.android.server.wm.flicker.repetitions
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setDevEnableNonResizableMultiWindow
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 
@@ -46,12 +49,17 @@
     protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation)
     protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation)
     protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation)
-    protected val LAUNCHER_PACKAGE_NAME = LauncherStrategyFactory.getInstance(instrumentation)
-        .launcherStrategy.supportedLauncherPackage
+    protected val LAUNCHER_COMPONENT = FlickerComponentName("",
+            LauncherStrategyFactory.getInstance(instrumentation)
+                    .launcherStrategy.supportedLauncherPackage)
     private var prevDevEnableNonResizableMultiWindow = 0
 
     @Before
     open fun setup() {
+        // Only run legacy split tests when the system is using legacy split screen.
+        assumeTrue(SplitScreenHelper.isUsingLegacySplit())
+        // Legacy split is having some issue with Shell transition, and will be deprecated soon.
+        assumeFalse(isShellTransitionsEnabled())
         prevDevEnableNonResizableMultiWindow = getDevEnableNonResizableMultiWindow(context)
         if (prevDevEnableNonResizableMultiWindow != 0) {
             // Turn off the development option
@@ -70,8 +78,9 @@
      *
      * b/182720234
      */
-    open val ignoredWindows: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-        WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    open val ignoredWindows: List<FlickerComponentName> = listOf(
+        FlickerComponentName.SPLASH_SCREEN,
+        FlickerComponentName.SNAPSHOT)
 
     protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = { configuration ->
@@ -138,9 +147,9 @@
     }
 
     companion object {
-        internal const val LIVE_WALLPAPER_PACKAGE_NAME =
-            "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2"
-        internal const val LETTERBOX_NAME = "Letterbox"
-        internal const val TOAST_NAME = "Toast"
+        internal val LIVE_WALLPAPER_COMPONENT = FlickerComponentName("",
+            "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2")
+        internal val LETTERBOX_COMPONENT = FlickerComponentName("", "Letterbox")
+        internal val TOAST_COMPONENT = FlickerComponentName("", "Toast")
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt
index 05eb5f4..34eff80 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt
@@ -24,15 +24,11 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.focusChanges
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.launchSplitScreen
-import com.android.server.wm.flicker.layerBecomesVisible
-import com.android.server.wm.flicker.noUncoveredRegions
-import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.flicker.statusBarLayerIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.wm.shell.flicker.appPairsDividerBecomesVisible
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
@@ -62,22 +58,28 @@
             }
         }
 
-    override val ignoredWindows: List<String>
-        get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName,
-            WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    override val ignoredWindows: List<FlickerComponentName>
+        get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component,
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
 
     @FlakyTest
     @Test
-    fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(splitScreenApp.getPackage())
-
-    @FlakyTest
-    @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation)
+    fun appWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(splitScreenApp.component)
+                    .then()
+                    .isAppWindowVisible(splitScreenApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
+
+    @Presubmit
+    @Test
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
     @Presubmit
     @Test
@@ -85,12 +87,27 @@
 
     @FlakyTest
     @Test
-    fun layerBecomesVisible() = testSpec.layerBecomesVisible(splitScreenApp.getPackage())
+    fun layerBecomesVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(splitScreenApp.component)
+                    .then()
+                    .isVisible(splitScreenApp.component)
+        }
+    }
 
-    @FlakyTest(bugId = 151179149)
+    @Presubmit
     @Test
-    fun focusChanges() = testSpec.focusChanges(splitScreenApp.`package`,
-        "recents_animation_input_consumer", "NexusLauncherActivity")
+    fun focusChanges() {
+        testSpec.assertEventLog {
+            this.focusChanges(splitScreenApp.`package`,
+                    "recents_animation_input_consumer", "NexusLauncherActivity")
+        }
+    }
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt
index 3e83b63..58e1def 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt
@@ -27,24 +27,24 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.endRotation
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.ImeAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.resizeSplitScreen
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.traces.layers.getVisibleBounds
-import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.helpers.SimpleAppHelper
+import com.android.wm.shell.flicker.testapp.Components
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -101,16 +101,16 @@
         }
 
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @FlakyTest(bugId = 156223549)
     @Test
     fun topAppWindowIsAlwaysVisible() {
         testSpec.assertWm {
-            this.showsAppWindow(sSimpleActivity)
+            this.isAppWindowVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent())
         }
     }
 
@@ -118,45 +118,43 @@
     @Test
     fun bottomAppWindowIsAlwaysVisible() {
         testSpec.assertWm {
-            this.showsAppWindow(sImeActivity)
+            this.isAppWindowVisible(Components.ImeActivity.COMPONENT.toFlickerComponent())
         }
     }
 
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
     @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
 
     @Test
-    fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.endRotation)
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Test
-    fun statusBarLayerRotatesScales() =
-        testSpec.statusBarLayerRotatesScales(testSpec.config.endRotation)
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
     @Test
     fun topAppLayerIsAlwaysVisible() {
         testSpec.assertLayers {
-            this.isVisible(sSimpleActivity)
+            this.isVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent())
         }
     }
 
     @Test
     fun bottomAppLayerIsAlwaysVisible() {
         testSpec.assertLayers {
-            this.isVisible(sImeActivity)
+            this.isVisible(Components.ImeActivity.COMPONENT.toFlickerComponent())
         }
     }
 
     @Test
     fun dividerLayerIsAlwaysVisible() {
         testSpec.assertLayers {
-            this.isVisible(DOCKED_STACK_DIVIDER)
+            this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
         }
     }
 
@@ -166,7 +164,7 @@
         testSpec.assertLayersStart {
             val displayBounds = WindowUtils.displayBounds
             val dividerBounds =
-                entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds
+                layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds
 
             val topAppBounds = Region(0, 0, dividerBounds.right,
                 dividerBounds.top + WindowUtils.dockedStackDividerInset)
@@ -174,8 +172,10 @@
                 dividerBounds.bottom - WindowUtils.dockedStackDividerInset,
                 displayBounds.right,
                 displayBounds.bottom - WindowUtils.navigationBarHeight)
-            visibleRegion("SimpleActivity").coversExactly(topAppBounds)
-            visibleRegion("ImeActivity").coversExactly(bottomAppBounds)
+            visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent())
+                .coversExactly(topAppBounds)
+            visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent())
+                .coversExactly(bottomAppBounds)
         }
     }
 
@@ -185,7 +185,7 @@
         testSpec.assertLayersStart {
             val displayBounds = WindowUtils.displayBounds
             val dividerBounds =
-                entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds
+                layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds
 
             val topAppBounds = Region(0, 0, dividerBounds.right,
                 dividerBounds.top + WindowUtils.dockedStackDividerInset)
@@ -194,8 +194,10 @@
                 displayBounds.right,
                 displayBounds.bottom - WindowUtils.navigationBarHeight)
 
-            visibleRegion(sSimpleActivity).coversExactly(topAppBounds)
-            visibleRegion(sImeActivity).coversExactly(bottomAppBounds)
+            visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent())
+                .coversExactly(topAppBounds)
+            visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent())
+                .coversExactly(bottomAppBounds)
         }
     }
 
@@ -207,8 +209,6 @@
     }
 
     companion object {
-        private const val sSimpleActivity = "SimpleActivity"
-        private const val sImeActivity = "ImeActivity"
         private val startRatio = Rational(1, 3)
         private val stopRatio = Rational(2, 3)
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt
index 58482ea..8a50bc0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt
@@ -24,18 +24,16 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.endRotation
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -66,38 +64,44 @@
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
-    fun dockedStackPrimaryBoundsIsVisible() =
-        testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
-            splitScreenApp.defaultWindowName)
-
-    @FlakyTest(bugId = 169271943)
-    @Test
-    fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation,
-            testSpec.config.endRotation)
-
-    @FlakyTest(bugId = 169271943)
-    @Test
-    fun statusBarLayerRotatesScales() =
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation,
-            testSpec.config.endRotation)
+    fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            splitScreenApp.component)
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    @Presubmit
+    @Test
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @FlakyTest
     @Test
-    fun appWindowBecomesVisible() =
-        testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName)
+    fun appWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(splitScreenApp.component)
+                    .then()
+                    .isAppWindowVisible(splitScreenApp.component)
+        }
+    }
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt
index 06828d6..84676a9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt
@@ -24,18 +24,16 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.endRotation
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -66,35 +64,43 @@
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
-    fun dockedStackPrimaryBoundsIsVisible() = testSpec.dockedStackPrimaryBoundsIsVisible(
-        testSpec.config.startRotation, splitScreenApp.defaultWindowName)
-
-    @FlakyTest(bugId = 169271943)
-    @Test
-    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales(
-        testSpec.config.startRotation, testSpec.config.endRotation)
-
-    @FlakyTest(bugId = 169271943)
-    @Test
-    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales(
-        testSpec.config.startRotation, testSpec.config.endRotation)
+    fun dockedStackPrimaryBoundsIsVisibleAtEnd() = testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(
+        testSpec.config.startRotation, splitScreenApp.component)
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    @Presubmit
+    @Test
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @FlakyTest
     @Test
-    fun appWindowBecomesVisible() =
-        testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName)
+    fun appWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(splitScreenApp.component)
+                    .then()
+                    .isAppWindowVisible(splitScreenApp.component)
+        }
+    }
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt
index f8e32bf..2abdca9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt
@@ -18,26 +18,23 @@
 
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.endRotation
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -69,42 +66,63 @@
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
-    fun dockedStackPrimaryBoundsIsVisible() =
-        testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
-            splitScreenApp.defaultWindowName)
+    fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            splitScreenApp.component)
 
     @Presubmit
     @Test
-    fun dockedStackSecondaryBoundsIsVisible() =
-        testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation,
-            secondaryApp.defaultWindowName)
-
-    @FlakyTest(bugId = 169271943)
-    @Test
-    fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation,
-            testSpec.config.endRotation)
-
-    @FlakyTest(bugId = 169271943)
-    @Test
-    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales(
-        testSpec.config.startRotation, testSpec.config.endRotation)
+    fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            secondaryApp.component)
 
     @Presubmit
     @Test
-    fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName)
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun appWindowBecomesVisible() {
+        testSpec.assertWm {
+            // when the app is launched, first the activity becomes visible, then the
+            // SnapshotStartingWindow appears and then the app window becomes visible.
+            // Because we log WM once per frame, sometimes the activity and the window
+            // become visible in the same entry, sometimes not, thus it is not possible to
+            // assert the visibility of the activity here
+            this.isAppWindowInvisible(secondaryApp.component)
+                    .then()
+                    // during re-parenting, the window may disappear and reappear from the
+                    // trace, this occurs because we log only 1x per frame
+                    .notContains(secondaryApp.component, isOptional = true)
+                    .then()
+                    // if the window reappears after re-parenting it will most likely not
+                    // be visible in the first log entry (because we log only 1x per frame)
+                    .isAppWindowInvisible(secondaryApp.component, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(secondaryApp.component)
+        }
+    }
+
+    @Presubmit
+    @Test
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt
index cb246ca..fe9b9f5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt
@@ -24,20 +24,18 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.appWindowBecomesVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.endRotation
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.wm.shell.flicker.dockedStackDividerIsVisible
-import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible
-import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -74,44 +72,55 @@
 
     @Presubmit
     @Test
-    fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible()
+    fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd()
 
     @Presubmit
     @Test
-    fun dockedStackPrimaryBoundsIsVisible() =
-        testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation,
-            splitScreenApp.defaultWindowName)
+    fun dockedStackPrimaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            splitScreenApp.component)
 
     @Presubmit
     @Test
-    fun dockedStackSecondaryBoundsIsVisible() =
-        testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation,
-            secondaryApp.defaultWindowName)
+    fun dockedStackSecondaryBoundsIsVisibleAtEnd() =
+        testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation,
+            secondaryApp.component)
 
-    @FlakyTest(bugId = 169271943)
+    @Presubmit
     @Test
-    fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation,
-            testSpec.config.endRotation)
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
-    @FlakyTest(bugId = 169271943)
+    @Presubmit
     @Test
-    fun statusBarLayerRotatesScales() =
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation,
-            testSpec.config.endRotation)
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
     @FlakyTest
     @Test
-    fun appWindowBecomesVisible() =
-        testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName)
+    fun appWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(secondaryApp.component)
+                    .then()
+                    .isAppWindowVisible(secondaryApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt
index 2a66074..f9b0800 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+@file:JvmName("CommonAssertions")
 package com.android.wm.shell.flicker.pip
 
-internal const val PIP_WINDOW_TITLE = "PipMenuActivity"
+internal const val PIP_WINDOW_COMPONENT = "PipMenuActivity"
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt
deleted file mode 100644
index 00e50e7..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip launch and exit.
- * To run this test: `atest WMShellFlickerTests:EnterExitPipTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class EnterExitPipTest(
-    testSpec: FlickerTestParameter
-) : PipTransition(testSpec) {
-    private val testApp = FixedAppHelper(instrumentation)
-
-    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
-        get() = buildTransition(eachRun = true) {
-            setup {
-                eachRun {
-                    testApp.launchViaIntent(wmHelper)
-                }
-            }
-            transitions {
-                // This will bring PipApp to fullscreen
-                pipApp.launchViaIntent(wmHelper)
-            }
-        }
-
-    @Presubmit
-    @Test
-    fun pipAppRemainInsideVisibleBounds() {
-        testSpec.assertWm {
-            coversAtMost(displayBounds, pipApp.defaultWindowName)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun showBothAppWindowsThenHidePip() {
-        testSpec.assertWm {
-            showsAppWindow(testApp.defaultWindowName)
-                .showsAppWindowOnTop(pipApp.defaultWindowName)
-                .then()
-                .hidesAppWindow(testApp.defaultWindowName)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun showBothAppLayersThenHidePip() {
-        testSpec.assertLayers {
-            isVisible(testApp.defaultWindowName)
-                .isVisible(pipApp.defaultWindowName)
-                .then()
-                .isInvisible(testApp.defaultWindowName)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun testAppCoversFullScreenWithPipOnDisplay() {
-        testSpec.assertLayersStart {
-            visibleRegion(testApp.defaultWindowName).coversExactly(displayBounds)
-            visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun pipAppCoversFullScreen() {
-        testSpec.assertLayersEnd {
-            visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds)
-        }
-    }
-
-    companion object {
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
-                supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5)
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
index b6af260..52a744f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
@@ -23,6 +23,7 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
 import com.android.server.wm.flicker.annotation.Group3
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.FixMethodOrder
@@ -32,8 +33,21 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test Pip launch.
+ * Test entering pip from an app by interacting with the app UI
+ *
  * To run this test: `atest WMShellFlickerTests:EnterPipTest`
+ *
+ * Actions:
+ *     Launch an app in full screen
+ *     Press an "enter pip" button to put [pipApp] in pip mode
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
@@ -41,49 +55,121 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group3
 class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+    /**
+     * Defines the transition used to run the test
+     */
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = buildTransition(eachRun = true, stringExtras = emptyMap()) {
             transitions {
-                pipApp.clickEnterPipButton()
-                pipApp.expandPipWindow(wmHelper)
+                pipApp.clickEnterPipButton(wmHelper)
             }
         }
 
-    @FlakyTest
-    @Test
-    override fun noUncoveredRegions() {
-        super.noUncoveredRegions()
-    }
-
+    /**
+     * Checks [pipApp] window remains visible throughout the animation
+     */
     @Presubmit
     @Test
     fun pipAppWindowAlwaysVisible() {
         testSpec.assertWm {
-            this.showsAppWindow(pipApp.defaultWindowName)
+            this.isAppWindowVisible(pipApp.component)
         }
     }
 
-    @FlakyTest
+    /**
+     * Checks [pipApp] layer remains visible throughout the animation
+     */
+    @Presubmit
     @Test
-    fun pipLayerBecomesVisible() {
+    fun pipAppLayerAlwaysVisible() {
         testSpec.assertLayers {
-            this.isVisible(pipApp.windowName)
+            this.isVisible(pipApp.component)
         }
     }
 
-    @FlakyTest
+    /**
+     * Checks that the pip app window remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
     @Test
-    fun pipWindowBecomesVisible() {
+    fun pipWindowRemainInsideVisibleBounds() {
         testSpec.assertWm {
-            invoke("pipWindowIsNotVisible") {
-                verify("Has no pip window").that(it.wmState.hasPipWindow()).isTrue()
-            }.then().invoke("pipWindowIsVisible") {
-                verify("Has pip window").that(it.wmState.hasPipWindow()).isTrue()
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the pip app layer remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
+    @Test
+    fun pipLayerRemainInsideVisibleBounds() {
+        testSpec.assertLayers {
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [pipApp] always reduces during the animation
+     */
+    @Presubmit
+    @Test
+    fun pipLayerReduces() {
+        val layerName = pipApp.component.toLayerName()
+        testSpec.assertLayers {
+            val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+            pipLayerList.zipWithNext { previous, current ->
+                current.visibleRegion.coversAtMost(previous.visibleRegion.region)
             }
         }
     }
 
+    /**
+     * Checks that [pipApp] window becomes pinned
+     */
+    @Presubmit
+    @Test
+    fun pipWindowBecomesPinned() {
+        testSpec.assertWm {
+            invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp.component) }
+                .then()
+                .invoke("pipWindowIsPinned") { it.isPinned(pipApp.component) }
+        }
+    }
+
+    /**
+     * Checks [LAUNCHER_COMPONENT] layer remains visible throughout the animation
+     */
+    @Presubmit
+    @Test
+    fun launcherLayerBecomesVisible() {
+        testSpec.assertLayers {
+            isInvisible(LAUNCHER_COMPONENT)
+                .then()
+                .isVisible(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks the focus doesn't change during the animation
+     */
+    @FlakyTest
+    @Test
+    fun focusDoesNotChange() {
+        testSpec.assertEventLog {
+            this.focusDoesNotChange()
+        }
+    }
+
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): List<FlickerTestParameter> {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
index 3a1456e..c8c3f4d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
@@ -25,7 +25,11 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group3
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.navBarLayerRotatesAndScales
+import com.android.server.wm.flicker.statusBarLayerRotatesScales
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.wm.shell.flicker.helpers.FixedAppHelper
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT
@@ -38,8 +42,22 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test Pip with orientation changes.
- * To run this test: `atest WMShellFlickerTests:PipOrientationTest`
+ * Test entering pip while changing orientation (from app in landscape to pip window in portrait)
+ *
+ * To run this test: `atest EnterPipToOtherOrientationTest:EnterPipToOtherOrientationTest`
+ *
+ * Actions:
+ *     Launch [testApp] on a fixed portrait orientation
+ *     Launch [pipApp] on a fixed landscape orientation
+ *     Broadcast action [ACTION_ENTER_PIP] to enter pip mode
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
@@ -53,6 +71,9 @@
     private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90)
     private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0)
 
+    /**
+     * Defines the transition used to run the test
+     */
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = { configuration ->
             setupAndTeardown(this, configuration)
@@ -79,65 +100,125 @@
                 broadcastActionTrigger.doAction(ACTION_ENTER_PIP)
                 wmHelper.waitFor { it.wmState.hasPipWindow() }
                 wmHelper.waitForAppTransitionIdle()
+                // during rotation the status bar becomes invisible and reappears at the end
+                wmHelper.waitForNavBarStatusBarVisible()
             }
         }
 
+    /**
+     * Checks that the [FlickerComponentName.NAV_BAR] has the correct position at
+     * the start and end of the transition
+     */
     @FlakyTest
     @Test
-    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
+    override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
-    @FlakyTest
+    /**
+     * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at
+     * the start and end of the transition
+     */
+    @Presubmit
     @Test
-    override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
+    override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
-    @FlakyTest
+    /**
+     * Checks that all parts of the screen are covered at the start and end of the transition
+     *
+     * TODO b/197726599 Prevents all states from being checked
+     */
+    @Presubmit
     @Test
-    override fun noUncoveredRegions() {
-        super.noUncoveredRegions()
-    }
+    override fun entireScreenCovered() = testSpec.entireScreenCovered(allStates = false)
 
+    /**
+     * Checks [pipApp] window remains visible and on top throughout the transition
+     */
     @Presubmit
     @Test
     fun pipAppWindowIsAlwaysOnTop() {
         testSpec.assertWm {
-            showsAppWindowOnTop(pipApp.defaultWindowName)
+            isAppWindowOnTop(pipApp.component)
         }
     }
 
+    /**
+     * Checks that [testApp] window is not visible at the start
+     */
     @Presubmit
     @Test
-    fun pipAppHidesTestApp() {
+    fun testAppWindowInvisibleOnStart() {
         testSpec.assertWmStart {
-            isInvisible(testApp.defaultWindowName)
+            isAppWindowInvisible(testApp.component)
         }
     }
 
+    /**
+     * Checks that [testApp] window is visible at the end
+     */
     @Presubmit
     @Test
-    fun testAppWindowIsVisible() {
+    fun testAppWindowVisibleOnEnd() {
         testSpec.assertWmEnd {
-            isVisible(testApp.defaultWindowName)
+            isAppWindowVisible(testApp.component)
         }
     }
 
+    /**
+     * Checks that [testApp] layer is not visible at the start
+     */
     @Presubmit
     @Test
-    fun pipAppLayerHidesTestApp() {
+    fun testAppLayerInvisibleOnStart() {
         testSpec.assertLayersStart {
-            visibleRegion(pipApp.defaultWindowName).coversExactly(startingBounds)
-            isInvisible(testApp.defaultWindowName)
+            isInvisible(testApp.component)
         }
     }
 
+    /**
+     * Checks that [testApp] layer is visible at the end
+     */
     @Presubmit
     @Test
-    fun testAppLayerCoversFullScreen() {
+    fun testAppLayerVisibleOnEnd() {
         testSpec.assertLayersEnd {
-            visibleRegion(testApp.defaultWindowName).coversExactly(endingBounds)
+            isVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [pipApp] covers the full display area at the start of
+     * the transition
+     */
+    @Presubmit
+    @Test
+    fun pipAppLayerCoversFullScreenOnStart() {
+        testSpec.assertLayersStart {
+            visibleRegion(pipApp.component).coversExactly(startingBounds)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [testApp] plus the visible region of [pipApp]
+     * cover the full display area at the end of the transition
+     */
+    @Presubmit
+    @Test
+    fun testAppPlusPipLayerCoversFullScreenOnEnd() {
+        testSpec.assertLayersEnd {
+            val pipRegion = visibleRegion(pipApp.component).region
+            visibleRegion(testApp.component)
+                .plus(pipRegion)
+                .coversExactly(endingBounds)
         }
     }
 
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
new file mode 100644
index 0000000..64b7eb5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.wm.shell.flicker.helpers.FixedAppHelper
+import org.junit.Test
+
+/**
+ * Base class for pip expand tests
+ */
+abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+    protected val testApp = FixedAppHelper(instrumentation)
+
+    /**
+     * Checks that the pip app window remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
+    @Test
+    open fun pipAppWindowRemainInsideVisibleBounds() {
+        testSpec.assertWm {
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the pip app layer remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
+    @Test
+    open fun pipAppLayerRemainInsideVisibleBounds() {
+        testSpec.assertLayers {
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks both app windows are visible at the start of the transition (with [pipApp] on top).
+     * Then, during the transition, [testApp] becomes invisible and [pipApp] remains visible
+     */
+    @Presubmit
+    @Test
+    open fun showBothAppWindowsThenHidePip() {
+        testSpec.assertWm {
+            // when the activity is STOPPING, sometimes it becomes invisible in an entry before
+            // the window, sometimes in the same entry. This occurs because we log 1x per frame
+            // thus we ignore activity here
+            isAppWindowVisible(testApp.component)
+                    .isAppWindowOnTop(pipApp.component)
+                    .then()
+                    .isAppWindowInvisible(testApp.component)
+                    .isAppWindowVisible(pipApp.component)
+        }
+    }
+
+    /**
+     * Checks both app layers are visible at the start of the transition. Then, during the
+     * transition, [testApp] becomes invisible and [pipApp] remains visible
+     */
+    @Presubmit
+    @Test
+    open fun showBothAppLayersThenHidePip() {
+        testSpec.assertLayers {
+            isVisible(testApp.component)
+                    .isVisible(pipApp.component)
+                    .then()
+                    .isInvisible(testApp.component)
+                    .isVisible(pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [testApp] plus the visible region of [pipApp]
+     * cover the full display area at the start of the transition
+     */
+    @Presubmit
+    @Test
+    open fun testPlusPipAppsCoverFullScreenAtStart() {
+        testSpec.assertLayersStart {
+            val pipRegion = visibleRegion(pipApp.component).region
+            visibleRegion(testApp.component)
+                    .plus(pipRegion)
+                    .coversExactly(displayBounds)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [pipApp] covers the full display area at the end of
+     * the transition
+     */
+    @Presubmit
+    @Test
+    open fun pipAppCoversFullScreenAtEnd() {
+        testSpec.assertLayersEnd {
+            visibleRegion(pipApp.component).coversExactly(displayBounds)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [pipApp] always expands during the animation
+     */
+    @Presubmit
+    @Test
+    open fun pipLayerExpands() {
+        val layerName = pipApp.component.toLayerName()
+        testSpec.assertLayers {
+            val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+            pipLayerList.zipWithNext { previous, current ->
+                current.visibleRegion.coversAtLeast(previous.visibleRegion.region)
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
new file mode 100644
index 0000000..5207fed
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.view.Surface
+import androidx.test.filters.FlakyTest
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.startRotation
+import org.junit.Test
+
+/**
+ * Base class for exiting pip (closing pip window) without returning to the app
+ */
+abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition(eachRun = true) { configuration ->
+            setup {
+                eachRun {
+                    this.setRotation(configuration.startRotation)
+                }
+            }
+            teardown {
+                eachRun {
+                    this.setRotation(Surface.ROTATION_0)
+                }
+            }
+        }
+
+    /**
+     * Checks that [pipApp] window is pinned and visible at the start and then becomes
+     * unpinned and invisible at the same moment, and remains unpinned and invisible
+     * until the end of the transition
+     */
+    @Presubmit
+    @Test
+    open fun pipWindowBecomesInvisible() {
+        testSpec.assertWm {
+            this.invoke("hasPipWindow") {
+                it.isPinned(pipApp.component).isAppWindowVisible(pipApp.component)
+            }.then().invoke("!hasPipWindow") {
+                it.isNotPinned(pipApp.component).isAppWindowInvisible(pipApp.component)
+            }
+        }
+    }
+
+    /**
+     * Checks that [pipApp] and [LAUNCHER_COMPONENT] layers are visible at the start
+     * of the transition. Then [pipApp] layer becomes invisible, and remains invisible
+     * until the end of the transition
+     */
+    @Presubmit
+    @Test
+    open fun pipLayerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(pipApp.component)
+                .isVisible(LAUNCHER_COMPONENT)
+                .then()
+                .isInvisible(pipApp.component)
+                .isVisible(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that the focus changes between the [pipApp] window and the launcher when
+     * closing the pip window
+     */
+    @FlakyTest(bugId = 151179149)
+    @Test
+    open fun focusChanges() {
+        testSpec.assertEventLog {
+            this.focusChanges(pipApp.launcherName, "NexusLauncherActivity")
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
new file mode 100644
index 0000000..b53342d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window back to full screen via the expand button
+ *
+ * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest`
+ *
+ * Actions:
+ *     Launch an app in pip mode [pipApp],
+ *     Launch another full screen mode [testApp]
+ *     Expand [pipApp] app to full screen by clicking on the pip window and
+ *     then on the expand button
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group3
+class ExitPipViaExpandButtonClickTest(
+    testSpec: FlickerTestParameter
+) : ExitPipToAppTransition(testSpec) {
+
+    /**
+     * Defines the transition used to run the test
+     */
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition(eachRun = true) {
+            setup {
+                eachRun {
+                    // launch an app behind the pip one
+                    testApp.launchViaIntent(wmHelper)
+                }
+            }
+            transitions {
+                // This will bring PipApp to fullscreen
+                pipApp.expandPipWindowToApp(wmHelper)
+                // Wait until the other app is no longer visible
+                wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName())
+            }
+        }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+                    supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
new file mode 100644
index 0000000..1fec3cf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window back to full screen via an intent
+ *
+ * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest`
+ *
+ * Actions:
+ *     Launch an app in pip mode [pipApp],
+ *     Launch another full screen mode [testApp]
+ *     Expand [pipApp] app to full screen via an intent
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited from [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group3
+class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransition(testSpec) {
+
+    /**
+     * Defines the transition used to run the test
+     */
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition(eachRun = true) {
+            setup {
+                eachRun {
+                    // launch an app behind the pip one
+                    testApp.launchViaIntent(wmHelper)
+                }
+            }
+            transitions {
+                // This will bring PipApp to fullscreen
+                pipApp.launchViaIntent(wmHelper)
+                // Wait until the other app is no longer visible
+                wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName())
+            }
+        }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+                supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5)
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
new file mode 100644
index 0000000..73626c2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test closing a pip window via the dismiss button
+ *
+ * To run this test: `atest WMShellFlickerTests:ExitPipWithDismissButtonTest`
+ *
+ * Actions:
+ *     Launch an app in pip mode [pipApp],
+ *     Click on the pip window
+ *     Click on dismiss button and wait window disappear
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group3
+class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) {
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = {
+            super.transition(this, it)
+            transitions {
+                pipApp.closePipWindow(wmHelper)
+            }
+        }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
+                            repetitions = 5)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
new file mode 100644
index 0000000..9e43dee
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.view.Surface
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.statusBarLayerRotatesScales
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test closing a pip window by swiping it to the bottom-center of the screen
+ *
+ * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest`
+ *
+ * Actions:
+ *     Launch an app in pip mode [pipApp],
+ *     Swipe the pip window to the bottom-center of the screen and wait it disappear
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group3
+class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) {
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = { args ->
+            super.transition(this, args)
+            transitions {
+                val pipRegion = wmHelper.getWindowRegion(pipApp.component).bounds
+                val pipCenterX = pipRegion.centerX()
+                val pipCenterY = pipRegion.centerY()
+                val displayCenterX = device.displayWidth / 2
+                device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 10)
+                wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() }
+                wmHelper.waitForWindowSurfaceDisappeared(pipApp.component)
+                wmHelper.waitForAppTransitionIdle()
+            }
+        }
+
+    @Presubmit
+    @Test
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
+    @Presubmit
+    @Test
+    override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
+
+    @Presubmit
+    @Test
+    override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
+
+    @Presubmit
+    @Test
+    override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
+
+    @FlakyTest
+    @Test
+    override fun pipWindowBecomesInvisible() = super.pipWindowBecomesInvisible()
+
+    @FlakyTest
+    @Test
+    override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible()
+
+    @Presubmit
+    @Test
+    override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    @Presubmit
+    @Test
+    override fun entireScreenCovered() = super.entireScreenCovered()
+
+    @Presubmit
+    @Test
+    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
+                            repetitions = 20)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
new file mode 100644
index 0000000..d0fee9a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.view.Surface
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window by double clicking it
+ *
+ * To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest`
+ *
+ * Actions:
+ *     Launch an app in pip mode [pipApp],
+ *     Expand [pipApp] app to its maximum pip size by double clicking on it
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group3
+class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition(eachRun = true) {
+            transitions {
+                pipApp.doubleClickPipWindow(wmHelper)
+            }
+        }
+
+    /**
+     * Checks that the pip app window remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
+    @Test
+    fun pipWindowRemainInsideVisibleBounds() {
+        testSpec.assertWm {
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the pip app layer remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
+    @Test
+    fun pipLayerRemainInsideVisibleBounds() {
+        testSpec.assertLayers {
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks [pipApp] window remains visible throughout the animation
+     */
+    @Presubmit
+    @Test
+    fun pipWindowIsAlwaysVisible() {
+        testSpec.assertWm {
+            isAppWindowVisible(pipApp.component)
+        }
+    }
+
+    /**
+     * Checks [pipApp] layer remains visible throughout the animation
+     */
+    @Presubmit
+    @Test
+    fun pipLayerIsAlwaysVisible() {
+        testSpec.assertLayers {
+            isVisible(pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [pipApp] always expands during the animation
+     */
+    @Presubmit
+    @Test
+    fun pipLayerExpands() {
+        val layerName = pipApp.component.toLayerName()
+        testSpec.assertLayers {
+            val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+            pipLayerList.zipWithNext { previous, current ->
+                current.visibleRegion.coversAtLeast(previous.visibleRegion.region)
+            }
+        }
+    }
+
+    /**
+     * Checks [pipApp] window remains pinned throughout the animation
+     */
+    @Presubmit
+    @Test
+    fun windowIsAlwaysPinned() {
+        testSpec.assertWm {
+            this.invoke("hasPipWindow") { it.isPinned(pipApp.component) }
+        }
+    }
+
+    /**
+     * Checks [pipApp] layer remains visible throughout the animation
+     */
+    @Presubmit
+    @Test
+    fun launcherIsAlwaysVisible() {
+        testSpec.assertLayers {
+            isVisible(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that the focus doesn't change between windows during the transition
+     */
+    @FlakyTest
+    @Test
+    fun focusDoesNotChange() {
+        testSpec.assertEventLog {
+            this.focusDoesNotChange()
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
+                            repetitions = 5)
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt
deleted file mode 100644
index 0037059..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.content.ComponentName
-import com.android.server.wm.traces.common.windowmanager.WindowManagerState
-import com.android.server.wm.traces.parser.toWindowName
-
-/**
- * Checks that an activity [activity] is in PIP mode
- */
-fun WindowManagerState.isInPipMode(activity: ComponentName): Boolean {
-    val windowName = activity.toWindowName()
-    return isInPipMode(windowName)
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt
new file mode 100644
index 0000000..0ab857d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.traces.RegionSubject
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test Pip movement with Launcher shelf height change (decrease).
+ *
+ * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest`
+ *
+ * Actions:
+ *     Launch [pipApp] in pip mode
+ *     Launch [testApp]
+ *     Press home
+ *     Check if pip window moves down (visually)
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group3
+class MovePipDownShelfHeightChangeTest(
+    testSpec: FlickerTestParameter
+) : MovePipShelfHeightTransition(testSpec) {
+    /**
+     * Defines the transition used to run the test
+     */
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition(eachRun = false) {
+            teardown {
+                eachRun {
+                    testApp.launchViaIntent(wmHelper)
+                }
+                test {
+                    testApp.exit(wmHelper)
+                }
+            }
+            transitions {
+                taplInstrumentation.pressHome()
+            }
+        }
+
+    override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) {
+        current.isHigherOrEqual(previous.region)
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+                    supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5)
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
new file mode 100644
index 0000000..6e0324c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.wm.shell.flicker.helpers.FixedAppHelper
+import org.junit.Test
+
+/**
+ * Base class for pip tests with Launcher shelf height change
+ */
+abstract class MovePipShelfHeightTransition(
+    testSpec: FlickerTestParameter
+) : PipTransition(testSpec) {
+    protected val taplInstrumentation = LauncherInstrumentation()
+    protected val testApp = FixedAppHelper(instrumentation)
+
+    /**
+     * Checks if the window movement direction is valid
+     */
+    protected abstract fun assertRegionMovement(previous: RegionSubject, current: RegionSubject)
+
+    /**
+     * Checks [pipApp] window remains visible throughout the animation
+     */
+    @Presubmit
+    @Test
+    open fun pipWindowIsAlwaysVisible() {
+        testSpec.assertWm {
+            isAppWindowVisible(pipApp.component)
+        }
+    }
+
+    /**
+     * Checks [pipApp] layer remains visible throughout the animation
+     */
+    @Presubmit
+    @Test
+    open fun pipLayerIsAlwaysVisible() {
+        testSpec.assertLayers {
+            isVisible(pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the pip app window remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
+    @Test
+    open fun pipWindowRemainInsideVisibleBounds() {
+        testSpec.assertWm {
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the pip app layer remains inside the display bounds throughout the whole
+     * animation
+     */
+    @Presubmit
+    @Test
+    open fun pipLayerRemainInsideVisibleBounds() {
+        testSpec.assertLayers {
+            coversAtMost(displayBounds, pipApp.component)
+        }
+    }
+
+    /**
+     * Checks that the visible region of [pipApp] always moves in the correct direction
+     * during the animation.
+     */
+    @Presubmit
+    @Test
+    open fun pipWindowMoves() {
+        val windowName = pipApp.component.toWindowName()
+        testSpec.assertWm {
+            val pipWindowList = this.windowStates { it.name.contains(windowName) && it.isVisible }
+            pipWindowList.zipWithNext { previous, current ->
+                assertRegionMovement(previous.frame, current.frame)
+            }
+        }
+    }
+
+    /**
+     * Checks that the visible region of [pipApp] always moves up during the animation
+     */
+    @Presubmit
+    @Test
+    open fun pipLayerMoves() {
+        val layerName = pipApp.component.toLayerName()
+        testSpec.assertLayers {
+            val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+            pipLayerList.zipWithNext { previous, current ->
+                assertRegionMovement(previous.visibleRegion, current.visibleRegion)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt
new file mode 100644
index 0000000..e507edf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.traces.RegionSubject
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test Pip movement with Launcher shelf height change (increase).
+ *
+ * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest`
+ *
+ * Actions:
+ *     Launch [pipApp] in pip mode
+ *     Press home
+ *     Launch [testApp]
+ *     Check if pip window moves up (visually)
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group3
+class MovePipUpShelfHeightChangeTest(
+    testSpec: FlickerTestParameter
+) : MovePipShelfHeightTransition(testSpec) {
+    /**
+     * Defines the transition used to run the test
+     */
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition(eachRun = false) {
+            teardown {
+                eachRun {
+                    taplInstrumentation.pressHome()
+                }
+                test {
+                    testApp.exit(wmHelper)
+                }
+            }
+            transitions {
+                testApp.launchViaIntent(wmHelper)
+            }
+        }
+
+    override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) {
+        current.isLowerOrEqual(previous.region)
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+                supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5)
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt
deleted file mode 100644
index eae7e97..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.focusChanges
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.startRotation
-import org.junit.Test
-import org.junit.runners.Parameterized
-
-abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
-    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
-        get() = buildTransition(eachRun = true) { configuration ->
-            setup {
-                eachRun {
-                    this.setRotation(configuration.startRotation)
-                }
-            }
-            teardown {
-                eachRun {
-                    this.setRotation(Surface.ROTATION_0)
-                }
-            }
-        }
-
-    @Presubmit
-    @Test
-    open fun pipWindowBecomesInvisible() {
-        testSpec.assertWm {
-            this.showsAppWindow(PIP_WINDOW_TITLE)
-                .then()
-                .hidesAppWindow(PIP_WINDOW_TITLE)
-        }
-    }
-
-    @Presubmit
-    @Test
-    open fun pipLayerBecomesInvisible() {
-        testSpec.assertLayers {
-            this.isVisible(PIP_WINDOW_TITLE)
-                .then()
-                .isInvisible(PIP_WINDOW_TITLE)
-        }
-    }
-
-    @FlakyTest(bugId = 151179149)
-    @Test
-    open fun focusChanges() = testSpec.focusChanges(pipApp.launcherName, "NexusLauncherActivity")
-
-    companion object {
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
-                    repetitions = 5)
-        }
-    }
-}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt
deleted file mode 100644
index cf84a2c..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip launch.
- * To run this test: `atest WMShellFlickerTests:PipCloseWithDismissButton`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class PipCloseWithDismissButtonTest(testSpec: FlickerTestParameter) : PipCloseTransition(testSpec) {
-    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
-        get() = {
-            super.transition(this, it)
-            transitions {
-                pipApp.closePipWindow(wmHelper)
-            }
-        }
-
-    @FlakyTest
-    @Test
-    override fun pipLayerBecomesInvisible() {
-        super.pipLayerBecomesInvisible()
-    }
-
-    @FlakyTest
-    @Test
-    override fun pipWindowBecomesInvisible() {
-        super.pipWindowBecomesInvisible()
-    }
-}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt
deleted file mode 100644
index 524a1b4..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip launch.
- * To run this test: `atest WMShellFlickerTests:PipCloseWithSwipe`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition(testSpec) {
-    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
-        get() = {
-            super.transition(this, it)
-            transitions {
-                val pipRegion = wmHelper.getWindowRegion(pipApp.component).bounds
-                val pipCenterX = pipRegion.centerX()
-                val pipCenterY = pipRegion.centerY()
-                val displayCenterX = device.displayWidth / 2
-                device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 5)
-            }
-        }
-
-    @Presubmit
-    @Test
-    override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible()
-
-    @Presubmit
-    @Test
-    override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible()
-
-    @Presubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    @Presubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    @FlakyTest
-    @Test
-    override fun pipWindowBecomesInvisible() = super.pipWindowBecomesInvisible()
-
-    @FlakyTest
-    @Test
-    override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible()
-
-    @Presubmit
-    @Test
-    override fun statusBarLayerRotatesScales() =
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0)
-
-    @Presubmit
-    @Test
-    override fun noUncoveredRegions() = super.noUncoveredRegions()
-
-    @Presubmit
-    @Test
-    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
-}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
index d88f94d..aba8ace 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
@@ -22,12 +22,12 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.startRotation
-import com.android.wm.shell.flicker.IME_WINDOW_NAME
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.wm.shell.flicker.helpers.ImeAppHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -43,7 +43,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
+@Group4
 class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
     private val imeApp = ImeAppHelper(instrumentation)
 
@@ -79,7 +79,7 @@
     fun pipInVisibleBounds() {
         testSpec.assertWm {
             val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
-            coversAtMost(displayBounds, pipApp.defaultWindowName)
+            coversAtMost(displayBounds, pipApp.component)
         }
     }
 
@@ -90,7 +90,7 @@
     @Test
     fun pipIsAboveAppWindow() {
         testSpec.assertWmTag(TAG_IME_VISIBLE) {
-            isAboveWindow(IME_WINDOW_NAME, pipApp.defaultWindowName)
+            isAboveWindow(FlickerComponentName.IME, pipApp.component)
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt
index 6833b96..9bea5c0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt
@@ -23,15 +23,20 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.launchSplitScreen
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.wm.shell.flicker.helpers.ImeAppHelper
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
 import com.android.server.wm.flicker.repetitions
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
+import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled
+import com.android.wm.shell.flicker.helpers.FixedAppHelper
+import com.android.wm.shell.flicker.helpers.ImeAppHelper
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
 import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -46,12 +51,19 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@FlakyTest(bugId = 161435597)
-@Group3
+@Group4
 class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
     private val imeApp = ImeAppHelper(instrumentation)
     private val testApp = FixedAppHelper(instrumentation)
 
+    @Before
+    open fun setup() {
+        // Only run legacy split tests when the system is using legacy split screen.
+        assumeTrue(SplitScreenHelper.isUsingLegacySplit())
+        // Legacy split is having some issue with Shell transition, and will be deprecated soon.
+        assumeFalse(isShellTransitionsEnabled())
+    }
+
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = {
             withTestName { testSpec.name }
@@ -80,11 +92,11 @@
             }
         }
 
-    @Presubmit
+    @FlakyTest(bugId = 161435597)
     @Test
     fun pipWindowInsideDisplayBounds() {
         testSpec.assertWm {
-            coversAtMost(displayBounds, pipApp.defaultWindowName)
+            coversAtMost(displayBounds, pipApp.component)
         }
     }
 
@@ -92,25 +104,17 @@
     @Test
     fun bothAppWindowsVisible() {
         testSpec.assertWmEnd {
-            isVisible(testApp.defaultWindowName)
-            isVisible(imeApp.defaultWindowName)
-            noWindowsOverlap(testApp.defaultWindowName, imeApp.defaultWindowName)
+            isAppWindowVisible(testApp.component)
+            isAppWindowVisible(imeApp.component)
+            doNotOverlap(testApp.component, imeApp.component)
         }
     }
 
-    @Presubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    @Presubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    @Presubmit
+    @FlakyTest(bugId = 161435597)
     @Test
     fun pipLayerInsideDisplayBounds() {
         testSpec.assertLayers {
-            coversAtMost(displayBounds, pipApp.defaultWindowName)
+            coversAtMost(displayBounds, pipApp.component)
         }
     }
 
@@ -118,18 +122,14 @@
     @Test
     fun bothAppLayersVisible() {
         testSpec.assertLayersEnd {
-            visibleRegion(testApp.defaultWindowName).coversAtMost(displayBounds)
-            visibleRegion(imeApp.defaultWindowName).coversAtMost(displayBounds)
+            visibleRegion(testApp.component).coversAtMost(displayBounds)
+            visibleRegion(imeApp.component).coversAtMost(displayBounds)
         }
     }
 
-    @Presubmit
+    @FlakyTest(bugId = 161435597)
     @Test
-    override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible()
-
-    @Presubmit
-    @Test
-    override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible()
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
     companion object {
         const val TEST_REPETITIONS = 2
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
index d531af2..669f37a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
@@ -23,13 +23,13 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.endRotation
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.noUncoveredRegions
 import com.android.server.wm.flicker.startRotation
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
 import com.android.wm.shell.flicker.helpers.FixedAppHelper
@@ -41,17 +41,32 @@
 
 /**
  * Test Pip Stack in bounds after rotations.
+ *
  * To run this test: `atest WMShellFlickerTests:PipRotationTest`
+ *
+ * Actions:
+ *     Launch a [pipApp] in pip mode
+ *     Launch another app [fixedApp] (appears below pip)
+ *     Rotate the screen from [testSpec.config.startRotation] to [testSpec.config.endRotation]
+ *     (usually, 0->90 and 90->0)
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited from [PipTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
+@Group4
 class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
     private val fixedApp = FixedAppHelper(instrumentation)
-    private val startingBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
-    private val endingBounds = WindowUtils.getDisplayBounds(testSpec.config.endRotation)
+    private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
+    private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.config.endRotation)
 
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = buildTransition(eachRun = false) { configuration ->
@@ -66,49 +81,104 @@
             transitions {
                 setRotation(configuration.endRotation)
             }
-            teardown {
-                eachRun {
-                    setRotation(Surface.ROTATION_0)
-                }
-            }
         }
 
-    @FlakyTest(bugId = 185400889)
-    @Test
-    override fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation,
-        testSpec.config.endRotation, allStates = false)
-
-    @FlakyTest
-    @Test
-    override fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation,
-            testSpec.config.endRotation)
-
+    /**
+     * Checks that all parts of the screen are covered at the start and end of the transition
+     */
     @Presubmit
     @Test
-    override fun statusBarLayerRotatesScales() =
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation,
-            testSpec.config.endRotation)
+    override fun entireScreenCovered() = testSpec.entireScreenCovered()
 
-    @FlakyTest(bugId = 185400889)
+    /**
+     * Checks the position of the navigation bar at the start and end of the transition
+     */
+    @FlakyTest
+    @Test
+    override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
+
+    /**
+     * Checks the position of the status bar at the start and end of the transition
+     */
+    @Presubmit
+    @Test
+    override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    /**
+     * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition
+     */
+    @Presubmit
     @Test
     fun appLayerRotates_StartingBounds() {
         testSpec.assertLayersStart {
-            visibleRegion(fixedApp.defaultWindowName).coversExactly(startingBounds)
-            visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds)
+            visibleRegion(fixedApp.component).coversExactly(screenBoundsStart)
         }
     }
 
-    @FlakyTest(bugId = 185400889)
+    /**
+     * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition
+     */
+    @Presubmit
     @Test
     fun appLayerRotates_EndingBounds() {
         testSpec.assertLayersEnd {
-            visibleRegion(fixedApp.defaultWindowName).coversExactly(endingBounds)
-            visibleRegion(pipApp.defaultWindowName).coversAtMost(endingBounds)
+            visibleRegion(fixedApp.component).coversExactly(screenBoundsEnd)
+        }
+    }
+
+    /**
+     * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition
+     */
+    @Presubmit
+    @Test
+    fun pipLayerRotates_StartingBounds() {
+        testSpec.assertLayersStart {
+            visibleRegion(pipApp.component).coversAtMost(screenBoundsStart)
+        }
+    }
+
+    /**
+     * Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition
+     */
+    @Presubmit
+    @Test
+    fun pipLayerRotates_EndingBounds() {
+        testSpec.assertLayersEnd {
+            visibleRegion(pipApp.component).coversAtMost(screenBoundsEnd)
+        }
+    }
+
+    /**
+     * Ensure that the [pipApp] window does not obscure the [fixedApp] at the start of the
+     * transition
+     */
+    @Presubmit
+    @Test
+    fun pipIsAboveFixedAppWindow_Start() {
+        testSpec.assertWmStart {
+            isAboveWindow(pipApp.component, fixedApp.component)
+        }
+    }
+
+    /**
+     * Ensure that the [pipApp] window does not obscure the [fixedApp] at the end of the
+     * transition
+     */
+    @Presubmit
+    @Test
+    fun pipIsAboveFixedAppWindow_End() {
+        testSpec.assertWmEnd {
+            isAboveWindow(pipApp.component, fixedApp.component)
         }
     }
 
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt
deleted file mode 100644
index 1294ac9..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
-import com.google.common.truth.Truth
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip movement with Launcher shelf height change.
- * To run this test: `atest WMShellFlickerTests:PipShelfHeightTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class PipShelfHeightTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
-    private val taplInstrumentation = LauncherInstrumentation()
-    private val testApp = FixedAppHelper(instrumentation)
-
-    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
-        get() = buildTransition(eachRun = false) {
-            teardown {
-                eachRun {
-                    taplInstrumentation.pressHome()
-                }
-                test {
-                    testApp.exit(wmHelper)
-                }
-            }
-            transitions {
-                testApp.launchViaIntent(wmHelper)
-            }
-        }
-
-    @Presubmit
-    @Test
-    fun pipAlwaysVisible() = testSpec.assertWm { this.showsAppWindow(pipApp.windowName) }
-
-    @Presubmit
-    @Test
-    fun pipLayerInsideDisplay() {
-        testSpec.assertLayersStart {
-            visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun pipWindowMovesUp() = testSpec.assertWmEnd {
-        val initialState = this.trace?.first()?.wmState
-            ?: error("Trace should not be empty")
-        val startPos = initialState.pinnedWindows.first().frame
-        val currPos = this.wmState.pinnedWindows.first().frame
-        val subject = Truth.assertWithMessage("Pip should have moved up")
-        subject.that(currPos.top).isGreaterThan(startPos.top)
-        subject.that(currPos.bottom).isGreaterThan(startPos.bottom)
-        subject.that(currPos.left).isEqualTo(startPos.left)
-        subject.that(currPos.right).isEqualTo(startPos.right)
-    }
-
-    companion object {
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
-                supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5)
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt
deleted file mode 100644
index 55e5c41..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.focusChanges
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.startRotation
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip launch.
- * To run this test: `atest WMShellFlickerTests:PipToAppTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
-    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
-        get() = buildTransition(eachRun = true) { configuration ->
-            setup {
-                eachRun {
-                    this.setRotation(configuration.startRotation)
-                }
-            }
-            teardown {
-                eachRun {
-                    this.setRotation(Surface.ROTATION_0)
-                }
-            }
-            transitions {
-                pipApp.expandPipWindowToApp(wmHelper)
-            }
-        }
-
-    @FlakyTest
-    @Test
-    fun appReplacesPipWindow() {
-        testSpec.assertWm {
-            this.showsAppWindow(PIP_WINDOW_TITLE)
-                .then()
-                .showsAppWindowOnTop(pipApp.launcherName)
-        }
-    }
-
-    @FlakyTest
-    @Test
-    fun appReplacesPipLayer() {
-        testSpec.assertLayers {
-            this.isVisible(PIP_WINDOW_TITLE)
-                .then()
-                .isVisible(pipApp.launcherName)
-        }
-    }
-
-    @FlakyTest
-    @Test
-    fun testAppCoversFullScreen() {
-        testSpec.assertLayersStart {
-            visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds)
-        }
-    }
-
-    @FlakyTest(bugId = 151179149)
-    @Test
-    fun focusChanges() = testSpec.focusChanges("NexusLauncherActivity",
-        pipApp.launcherName, "NexusLauncherActivity")
-
-    companion object {
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
-                    repetitions = 5)
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
index b4c75a6..e8a61e8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
@@ -20,25 +20,24 @@
 import android.content.Intent
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
-import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.isRotated
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.repetitions
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
 import com.android.wm.shell.flicker.helpers.PipAppHelper
 import com.android.wm.shell.flicker.testapp.Components
 import org.junit.Test
@@ -162,32 +161,29 @@
 
     @Presubmit
     @Test
-    open fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    open fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    open fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
-
-    @FlakyTest
-    @Test
-    open fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
-
-    @FlakyTest
-    @Test
-    open fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    open fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
-    open fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, Surface.ROTATION_0)
+    open fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    open fun statusBarLayerRotatesScales() =
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0)
+    open fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
     @Presubmit
     @Test
-    open fun noUncoveredRegions() =
-        testSpec.noUncoveredRegions(testSpec.config.startRotation, Surface.ROTATION_0)
+    open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
+
+    @Presubmit
+    @Test
+    open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    @Presubmit
+    @Test
+    open fun entireScreenCovered() = testSpec.entireScreenCovered()
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
index 1f58bb2..d6dbc36 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
@@ -16,14 +16,13 @@
 
 package com.android.wm.shell.flicker.pip
 
-import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
@@ -44,7 +43,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
+@Group4
 class SetRequestedOrientationWhilePinnedTest(
     testSpec: FlickerTestParameter
 ) : PipTransition(testSpec) {
@@ -83,56 +82,70 @@
 
     @FlakyTest
     @Test
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
+    @FlakyTest
+    @Test
+    override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
+
+    @FlakyTest
+    @Test
+    override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
+
+    @FlakyTest
+    @Test
+    override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
+
+    @FlakyTest
+    @Test
     override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
 
     @FlakyTest
     @Test
     override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
 
-    @Presubmit
+    @FlakyTest
     @Test
     fun pipWindowInsideDisplay() {
         testSpec.assertWmStart {
-            frameRegion(pipApp.defaultWindowName).coversAtMost(startingBounds)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun pipAppShowsOnTop() {
-        testSpec.assertWmEnd {
-            showsAppWindowOnTop(pipApp.defaultWindowName)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun pipLayerInsideDisplay() {
-        testSpec.assertLayersStart {
-            visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds)
-        }
-    }
-
-    @Presubmit
-    @Test
-    fun pipAlwaysVisible() = testSpec.assertWm {
-        this.showsAppWindow(pipApp.windowName)
-    }
-
-    @Presubmit
-    @Test
-    fun pipAppLayerCoversFullScreen() {
-        testSpec.assertLayersEnd {
-            visibleRegion(pipApp.defaultWindowName).coversExactly(endingBounds)
+            frameRegion(pipApp.component).coversAtMost(startingBounds)
         }
     }
 
     @FlakyTest
     @Test
-    override fun noUncoveredRegions() {
-        super.noUncoveredRegions()
+    fun pipAppShowsOnTop() {
+        testSpec.assertWmEnd {
+            isAppWindowOnTop(pipApp.component)
+        }
     }
 
+    @FlakyTest
+    @Test
+    fun pipLayerInsideDisplay() {
+        testSpec.assertLayersStart {
+            visibleRegion(pipApp.component).coversAtMost(startingBounds)
+        }
+    }
+
+    @FlakyTest
+    @Test
+    fun pipAlwaysVisible() = testSpec.assertWm {
+        this.isAppWindowVisible(pipApp.component)
+    }
+
+    @FlakyTest
+    @Test
+    fun pipAppLayerCoversFullScreen() {
+        testSpec.assertLayersEnd {
+            visibleRegion(pipApp.component).coversExactly(endingBounds)
+        }
+    }
+
+    @FlakyTest
+    @Test
+    override fun entireScreenCovered() = super.entireScreenCovered()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
index 0110ba3..061218a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
@@ -37,14 +37,17 @@
     private val systemUiResources =
             packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
     private val pipBoundsWhileInMenu: Rect = systemUiResources.run {
-        val bounds = getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME))
+        val bounds = getString(getIdentifier("pip_menu_bounds", "string",
+                SYSTEM_UI_PACKAGE_NAME))
         Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds")
     }
     private val playButtonDescription = systemUiResources.run {
-        getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME))
+        getString(getIdentifier("pip_play", "string",
+                SYSTEM_UI_PACKAGE_NAME))
     }
     private val pauseButtonDescription = systemUiResources.run {
-        getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME))
+        getString(getIdentifier("pip_pause", "string",
+                SYSTEM_UI_PACKAGE_NAME))
     }
 
     @Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
index 1b73920..1c66340 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
@@ -70,7 +70,8 @@
     // descendant and then retrieve the element from the menu and return to the caller of this
     // method.
     val elementSelector = By.desc(desc)
-    val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector)
+    val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR)
+            .hasDescendant(elementSelector)
 
     return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS)
             ?.findObject(elementSelector)
@@ -94,7 +95,8 @@
 }
 
 fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) {
-    focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) ||
+    focusOnAndClickTvPipMenuElement(By.desc(desc)
+            .pkg(SYSTEM_UI_PACKAGE_NAME)) ||
             error("Could not focus on the Pip menu object with \"$desc\" description")
     // So apparently Accessibility framework on TV is not very reliable and sometimes the state of
     // the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
index 5549330..2cdbffa 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
@@ -107,5 +107,20 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <activity
+            android:name=".LaunchBubbleActivity"
+            android:label="LaunchBubbleApp"
+            android:exported="true"
+            android:launchMode="singleTop">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.VIEW" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".BubbleActivity"
+            android:label="BubbleApp"
+            android:exported="false"
+            android:resizeableActivity="true" />
     </application>
 </manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png
new file mode 100644
index 0000000..d424a17
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png
Binary files differ
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml
new file mode 100644
index 0000000..b43f31d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/>
+</vector>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml
new file mode 100644
index 0000000..0e8c7a0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M12,4c-4.97,0 -9,3.58 -9,8c0,1.53 0.49,2.97 1.33,4.18c0.12,0.18 0.2,0.46 0.1,0.66c-0.33,0.68 -0.79,1.52 -1.38,2.39c-0.12,0.17 0.01,0.41 0.21,0.39c0.63,-0.05 1.86,-0.26 3.38,-0.91c0.17,-0.07 0.36,-0.06 0.52,0.03C8.55,19.54 10.21,20 12,20c4.97,0 9,-3.58 9,-8S16.97,4 12,4zM16.94,11.63l-3.29,3.29c-0.13,0.13 -0.34,0.04 -0.34,-0.14v-1.57c0,-0.11 -0.1,-0.21 -0.21,-0.2c-2.19,0.06 -3.65,0.65 -5.14,1.95c-0.15,0.13 -0.38,0 -0.33,-0.19c0.7,-2.57 2.9,-4.57 5.5,-4.75c0.1,-0.01 0.18,-0.09 0.18,-0.19V8.2c0,-0.18 0.22,-0.27 0.34,-0.14l3.29,3.29C17.02,11.43 17.02,11.55 16.94,11.63z"
+      android:fillColor="#000000"
+      android:fillType="evenOdd"/>
+</vector>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml
new file mode 100644
index 0000000..f8b0ca3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <Button
+        android:id="@+id/button_finish"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:layout_marginStart="8dp"
+        android:text="Finish" />
+    <Button
+        android:id="@+id/button_new_task"
+        android:layout_width="wrap_content"
+        android:layout_height="46dp"
+        android:layout_marginStart="8dp"
+        android:text="New Task" />
+    <Button
+        android:id="@+id/button_new_bubble"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:layout_marginEnd="8dp"
+        android:text="New Bubble" />
+
+    <Button
+        android:id="@+id/button_activity_for_result"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:layout_marginStart="8dp"
+        android:text="Activity For Result" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml
new file mode 100644
index 0000000..f23c464
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@android:color/black">
+
+        <Button
+            android:id="@+id/button_create"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:layout_centerVertical="true"
+            android:text="Add Bubble" />
+
+        <Button
+            android:id="@+id/button_cancel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/button_create"
+            android:layout_centerHorizontal="true"
+            android:layout_marginTop="20dp"
+            android:text="Cancel Bubble" />
+
+        <Button
+            android:id="@+id/button_cancel_all"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/button_cancel"
+            android:layout_centerHorizontal="true"
+            android:layout_marginTop="20dp"
+            android:text="Cancel All Bubble" />
+</RelativeLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java
new file mode 100644
index 0000000..bc3bc75
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.testapp;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Toast;
+
+public class BubbleActivity extends Activity {
+    private int mNotifId = 0;
+
+    public BubbleActivity() {
+        super();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent intent = getIntent();
+        if (intent != null) {
+            mNotifId = intent.getIntExtra(BubbleHelper.EXTRA_BUBBLE_NOTIF_ID, -1);
+        } else {
+            mNotifId = -1;
+        }
+
+        setContentView(R.layout.activity_bubble);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        String result = resultCode == Activity.RESULT_OK ? "OK" : "CANCELLED";
+        Toast.makeText(this, "Activity result: " + result, Toast.LENGTH_SHORT).show();
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
new file mode 100644
index 0000000..d743dff
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.testapp;
+
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.drawable.Icon;
+import android.os.SystemClock;
+import android.service.notification.StatusBarNotification;
+import android.view.WindowManager;
+
+import java.util.HashMap;
+
+public class BubbleHelper {
+
+    static final String EXTRA_BUBBLE_NOTIF_ID = "EXTRA_BUBBLE_NOTIF_ID";
+    static final String CHANNEL_ID = "bubbles";
+    static final String CHANNEL_NAME = "Bubbles";
+    static final int DEFAULT_HEIGHT_DP = 300;
+
+    private static BubbleHelper sInstance;
+
+    private final Context mContext;
+    private NotificationManager mNotificationManager;
+    private float mDisplayHeight;
+
+    private HashMap<Integer, BubbleInfo> mBubbleMap = new HashMap<>();
+
+    private int mNextNotifyId = 0;
+    private int mColourIndex = 0;
+
+    public static class BubbleInfo {
+        public int id;
+        public int height;
+        public Icon icon;
+
+        public BubbleInfo(int id, int height, Icon icon) {
+            this.id = id;
+            this.height = height;
+            this.icon = icon;
+        }
+    }
+
+    public static BubbleHelper getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new BubbleHelper(context);
+        }
+        return sInstance;
+    }
+
+    private BubbleHelper(Context context) {
+        mContext = context;
+        mNotificationManager = context.getSystemService(NotificationManager.class);
+
+        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME,
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setDescription("Channel that posts bubbles");
+        channel.setAllowBubbles(true);
+        mNotificationManager.createNotificationChannel(channel);
+
+        Point p = new Point();
+        WindowManager wm = context.getSystemService(WindowManager.class);
+        wm.getDefaultDisplay().getRealSize(p);
+        mDisplayHeight = p.y;
+
+    }
+
+      private int getNextNotifyId() {
+        int id = mNextNotifyId;
+        mNextNotifyId++;
+        return id;
+    }
+
+    private Icon getIcon() {
+        return Icon.createWithResource(mContext, R.drawable.bg);
+    }
+
+    public int addNewBubble(boolean autoExpand, boolean suppressNotif) {
+        int id = getNextNotifyId();
+        BubbleInfo info = new BubbleInfo(id, DEFAULT_HEIGHT_DP, getIcon());
+        mBubbleMap.put(info.id, info);
+
+        Notification.BubbleMetadata data = getBubbleBuilder(info)
+                .setSuppressNotification(suppressNotif)
+                .setAutoExpandBubble(false)
+                .build();
+        Notification notification = getNotificationBuilder(info.id)
+                .setBubbleMetadata(data).build();
+
+        mNotificationManager.notify(info.id, notification);
+        return info.id;
+    }
+
+    private Notification.Builder getNotificationBuilder(int id) {
+        Person chatBot = new Person.Builder()
+                .setBot(true)
+                .setName("BubbleBot")
+                .setImportant(true)
+                .build();
+
+        RemoteInput remoteInput = new RemoteInput.Builder("key")
+                .setLabel("Reply")
+                .build();
+
+        String shortcutId = "BubbleChat";
+        return new Notification.Builder(mContext, CHANNEL_ID)
+                .setChannelId(CHANNEL_ID)
+                .setShortcutId(shortcutId)
+                .setContentIntent(PendingIntent.getActivity(mContext, 0,
+                        new Intent(mContext, LaunchBubbleActivity.class),
+                        PendingIntent.FLAG_UPDATE_CURRENT))
+                .setStyle(new Notification.MessagingStyle(chatBot)
+                        .setConversationTitle("Bubble Chat")
+                        .addMessage("Hello? This is bubble: " + id,
+                                SystemClock.currentThreadTimeMillis() - 300000, chatBot)
+                        .addMessage("Is it me, " + id + ", you're looking for?",
+                                SystemClock.currentThreadTimeMillis(), chatBot)
+                )
+                .setSmallIcon(R.drawable.ic_bubble);
+    }
+
+    private Notification.BubbleMetadata.Builder getBubbleBuilder(BubbleInfo info) {
+        Intent target = new Intent(mContext, BubbleActivity.class);
+        target.putExtra(EXTRA_BUBBLE_NOTIF_ID, info.id);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, info.id, target,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+
+        return new Notification.BubbleMetadata.Builder()
+                .setIntent(bubbleIntent)
+                .setIcon(info.icon)
+                .setDesiredHeight(info.height);
+    }
+
+    public void cancel(int id) {
+        mNotificationManager.cancel(id);
+    }
+
+    public void cancelAll() {
+        mNotificationManager.cancelAll();
+    }
+
+    public void cancelLast() {
+        StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications();
+        if (activeNotifications.length > 0) {
+            mNotificationManager.cancel(
+                    activeNotifications[activeNotifications.length - 1].getId());
+        }
+    }
+
+    public void cancelFirst() {
+        StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications();
+        if (activeNotifications.length > 0) {
+            mNotificationManager.cancel(activeNotifications[0].getId());
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
index 0ead91b..0ed59bd 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
@@ -87,4 +87,16 @@
         public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
                 PACKAGE_NAME + ".SplitScreenSecondaryActivity");
     }
+
+    public static class LaunchBubbleActivity {
+        public static final String LABEL = "LaunchBubbleApp";
+        public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
+                PACKAGE_NAME + ".LaunchBubbleActivity");
+    }
+
+    public static class BubbleActivity {
+        public static final String LABEL = "BubbleApp";
+        public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
+                PACKAGE_NAME + ".BubbleActivity");
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java
new file mode 100644
index 0000000..71fa66d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.testapp;
+
+
+import android.app.Activity;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.view.View;
+
+import java.util.Arrays;
+
+public class LaunchBubbleActivity extends Activity {
+
+    private BubbleHelper mBubbleHelper;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        addInboxShortcut(getApplicationContext());
+        mBubbleHelper = BubbleHelper.getInstance(this);
+        setContentView(R.layout.activity_main);
+        findViewById(R.id.button_create).setOnClickListener(this::add);
+        findViewById(R.id.button_cancel).setOnClickListener(this::cancel);
+        findViewById(R.id.button_cancel_all).setOnClickListener(this::cancelAll);
+    }
+
+    private void add(View v) {
+        mBubbleHelper.addNewBubble(false /* autoExpand */, false /* suppressNotif */);
+    }
+
+    private void cancel(View v) {
+        mBubbleHelper.cancelLast();
+    }
+
+    private void cancelAll(View v) {
+        mBubbleHelper.cancelAll();
+    }
+
+    private void addInboxShortcut(Context context) {
+        Icon icon = Icon.createWithResource(this, R.drawable.bg);
+        Person[] persons = new Person[4];
+        for (int i = 0; i < persons.length; i++) {
+            persons[i] = new Person.Builder()
+                    .setBot(false)
+                    .setIcon(icon)
+                    .setName("google" + i)
+                    .setImportant(true)
+                    .build();
+        }
+
+        ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "BubbleChat")
+                .setShortLabel("BubbleChat")
+                .setLongLived(true)
+                .setIntent(new Intent(Intent.ACTION_VIEW))
+                .setIcon(Icon.createWithResource(context, R.drawable.ic_message))
+                .setPersons(persons)
+                .build();
+        ShortcutManager scmanager = context.getSystemService(ShortcutManager.class);
+        scmanager.addDynamicShortcuts(Arrays.asList(shortcut));
+    }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 6b74b62..d5acbbc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -197,6 +197,43 @@
     }
 
     @Test
+    public void testAddListenerForMultipleTypes() {
+        RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+        mOrganizer.onTaskAppeared(taskInfo1, null);
+        RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+        mOrganizer.onTaskAppeared(taskInfo2, null);
+
+        TrackingTaskListener listener = new TrackingTaskListener();
+        mOrganizer.addListenerForType(listener,
+                TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_FULLSCREEN);
+
+        // onTaskAppeared event should be delivered once for each taskInfo.
+        assertTrue(listener.appeared.contains(taskInfo1));
+        assertTrue(listener.appeared.contains(taskInfo2));
+        assertEquals(2, listener.appeared.size());
+    }
+
+    @Test
+    public void testRemoveListenerForMultipleTypes() {
+        RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+        mOrganizer.onTaskAppeared(taskInfo1, null);
+        RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+        mOrganizer.onTaskAppeared(taskInfo2, null);
+
+        TrackingTaskListener listener = new TrackingTaskListener();
+        mOrganizer.addListenerForType(listener,
+                TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_FULLSCREEN);
+
+        mOrganizer.removeListener(listener);
+
+        // If listener is removed properly, onTaskInfoChanged event shouldn't be delivered.
+        mOrganizer.onTaskInfoChanged(taskInfo1);
+        assertTrue(listener.infoChanged.isEmpty());
+        mOrganizer.onTaskInfoChanged(taskInfo2);
+        assertTrue(listener.infoChanged.isEmpty());
+    }
+
+    @Test
     public void testWindowingModeChange() {
         RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
         TrackingTaskListener mwListener = new TrackingTaskListener();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
index 20ac5bf..1cbad15 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
@@ -47,6 +47,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.common.HandlerExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable;
 
 import org.junit.After;
 import org.junit.Before;
@@ -71,6 +73,8 @@
     ShellTaskOrganizer mOrganizer;
     @Mock
     HandlerExecutor mExecutor;
+    @Mock
+    SyncTransactionQueue mSyncQueue;
 
     SurfaceSession mSession;
     SurfaceControl mLeash;
@@ -99,7 +103,14 @@
         }).when(mExecutor).execute(any());
 
         when(mOrganizer.getExecutor()).thenReturn(mExecutor);
-        mTaskView = new TaskView(mContext, mOrganizer);
+
+        doAnswer((InvocationOnMock invocationOnMock) -> {
+            final TransactionRunnable r = invocationOnMock.getArgument(0);
+            r.runWithTransaction(new SurfaceControl.Transaction());
+            return null;
+        }).when(mSyncQueue).runInSync(any());
+
+        mTaskView = new TaskView(mContext, mOrganizer, mSyncQueue);
         mTaskView.setListener(mExecutor, mViewListener);
     }
 
@@ -112,7 +123,7 @@
 
     @Test
     public void testSetPendingListener_throwsException() {
-        TaskView taskView = new TaskView(mContext, mOrganizer);
+        TaskView taskView = new TaskView(mContext, mOrganizer, mSyncQueue);
         taskView.setListener(mExecutor, mViewListener);
         try {
             taskView.setListener(mExecutor, mViewListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java
index 27c6261..294bc12 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java
@@ -21,6 +21,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 
@@ -30,7 +31,7 @@
     public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
             DisplayController displayController) {
         super(organizer, syncQueue, displayController, mock(ShellExecutor.class),
-                mock(DisplayImeController.class));
+                mock(DisplayImeController.class), mock(DisplayInsetsController.class));
         mPool = new TestAppPairsPool(this);
         setPairsPool(mPool);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 3e3195f..bc701d0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -131,7 +131,7 @@
 
         NotificationListenerService.Ranking ranking =
                 mock(NotificationListenerService.Ranking.class);
-        when(ranking.visuallyInterruptive()).thenReturn(true);
+        when(ranking.isTextChanged()).thenReturn(true);
         mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking);
         mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null,
                 mMainExecutor);
@@ -793,7 +793,7 @@
     }
 
     @Test
-    public void test_expanded_removeLastBubble_collapsesStack() {
+    public void test_expanded_removeLastBubble_showsOverflowIfNotEmpty() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         changeExpandedStateAtTime(true, 2000);
@@ -802,6 +802,21 @@
         // Test
         mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
+        assertThat(mBubbleData.getOverflowBubbles().size()).isGreaterThan(0);
+        assertSelectionChangedTo(mBubbleData.getOverflow());
+    }
+
+    @Test
+    public void test_expanded_removeLastBubble_collapsesIfOverflowEmpty() {
+        // Setup
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        changeExpandedStateAtTime(true, 2000);
+        mBubbleData.setListener(mListener);
+
+        // Test
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NO_BUBBLE_UP);
+        verifyUpdateReceived();
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
         assertExpandedChangedTo(false);
     }
 
@@ -869,6 +884,35 @@
         assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA2.getKey()));
     }
 
+    /**
+     * Verifies that after the stack is collapsed with the overflow selected, it will select
+     * the top bubble upon next expansion.
+     */
+    @Test
+    public void test_collapseWithOverflowSelected_nextExpansion() {
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        sendUpdatedEntryAtTime(mEntryA2, 2000);
+        mBubbleData.setExpanded(true);
+
+        mBubbleData.setListener(mListener);
+
+        // Select the overflow
+        mBubbleData.setShowingOverflow(true);
+        mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
+        verifyUpdateReceived();
+        assertSelectionChangedTo(mBubbleData.getOverflow());
+
+        // Collapse
+        mBubbleData.setExpanded(false);
+        verifyUpdateReceived();
+        assertSelectionNotChanged();
+
+        // Expand (here we should select the new bubble)
+        mBubbleData.setExpanded(true);
+        verifyUpdateReceived();
+        assertSelectionChangedTo(mBubbleA2);
+    }
+
     private void verifyUpdateReceived() {
         verify(mListener).applyUpdate(mUpdateCaptor.capture());
         reset(mListener);
@@ -902,7 +946,7 @@
         assertWithMessage("selectionChanged").that(update.selectionChanged).isFalse();
     }
 
-    private void assertSelectionChangedTo(Bubble bubble) {
+    private void assertSelectionChangedTo(BubbleViewProvider bubble) {
         BubbleData.Update update = mUpdateCaptor.getValue();
         assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue();
         assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble);
@@ -925,7 +969,6 @@
         assertThat(update.overflowBubbles).isEqualTo(bubbles);
     }
 
-
     private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName,
             NotificationListenerService.Ranking ranking) {
         return createBubbleEntry(userId, notifKey, packageName, ranking, 1000);
@@ -971,15 +1014,15 @@
     }
 
     private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime) {
-        sendUpdatedEntryAtTime(entry, postTime, true /* visuallyInterruptive */);
+        sendUpdatedEntryAtTime(entry, postTime, true /* isTextChanged */);
     }
 
     private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime,
-            boolean visuallyInterruptive) {
+            boolean textChanged) {
         setPostTime(entry, postTime);
         // BubbleController calls this:
         Bubble b = mBubbleData.getOrCreateBubble(entry, null /* persistedBubble */);
-        b.setVisuallyInterruptiveForTest(visuallyInterruptive);
+        b.setTextChangedForTest(textChanged);
         // And then this
         mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/,
                 true /* showInShade */);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java
index 6644eaf..5c1bcb9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java
@@ -63,7 +63,7 @@
         mFlyoutMessage.senderName = "Josh";
         mFlyoutMessage.message = "Hello";
 
-        mFlyout = new BubbleFlyoutView(getContext());
+        mFlyout = new BubbleFlyoutView(getContext(), mPositioner);
 
         mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text);
         mSenderName = mFlyout.findViewById(R.id.bubble_flyout_name);
@@ -75,9 +75,8 @@
     public void testShowFlyout_isVisible() {
         mFlyout.setupFlyoutStartingAsDot(
                 mFlyoutMessage,
-                new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
-                false,
-                mPositioner);
+                new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter,
+                false);
         mFlyout.setVisibility(View.VISIBLE);
 
         assertEquals("Hello", mFlyoutText.getText());
@@ -89,9 +88,8 @@
     public void testFlyoutHide_runsCallback() {
         Runnable after = mock(Runnable.class);
         mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
-                new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
-                false,
-                mPositioner);
+                new PointF(100, 100), true, Color.WHITE, null, after, mDotCenter,
+                false);
         mFlyout.hideFlyout();
 
         verify(after).run();
@@ -100,9 +98,8 @@
     @Test
     public void testSetCollapsePercent() {
         mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
-                new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
-                false,
-                mPositioner);
+                new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter,
+                false);
         mFlyout.setVisibility(View.VISIBLE);
 
         mFlyout.setCollapsePercent(1f);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
index 1eba3c2..2b9bdce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -16,9 +16,12 @@
 
 package com.android.wm.shell.bubbles.animation;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.annotation.SuppressLint;
 import android.content.res.Configuration;
@@ -36,12 +39,12 @@
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.BubbleStackView;
 
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Spy;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -49,26 +52,30 @@
 
     private int mDisplayWidth = 500;
     private int mDisplayHeight = 1000;
-    private int mExpandedViewPadding = 10;
 
     private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class);
-    @Spy
     ExpandedAnimationController mExpandedController;
 
     private int mStackOffset;
     private PointF mExpansionPoint;
+    private BubblePositioner mPositioner;
+    private BubbleStackView.StackViewState mStackViewState;
 
     @SuppressLint("VisibleForTests")
     @Before
     public void setUp() throws Exception {
         super.setUp();
 
-        BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class));
-        positioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
+        BubbleStackView stackView = mock(BubbleStackView.class);
+        when(stackView.getState()).thenReturn(getStackViewState());
+        mPositioner = new BubblePositioner(getContext(), mock(WindowManager.class));
+        mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
                 Insets.of(0, 0, 0, 0),
                 new Rect(0, 0, mDisplayWidth, mDisplayHeight));
-        mExpandedController = new ExpandedAnimationController(positioner, mExpandedViewPadding,
-                mOnBubbleAnimatedOutAction);
+        mExpandedController = new ExpandedAnimationController(mPositioner,
+                mOnBubbleAnimatedOutAction,
+                stackView);
+        spyOn(mExpandedController);
 
         addOneMoreThanBubbleLimitBubbles();
         mLayout.setActiveController(mExpandedController);
@@ -78,6 +85,13 @@
         mExpansionPoint = new PointF(100, 100);
     }
 
+    public BubbleStackView.StackViewState getStackViewState() {
+        mStackViewState.numberOfBubbles = mLayout.getChildCount();
+        mStackViewState.selectedIndex = 0;
+        mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint);
+        return mStackViewState;
+    }
+
     @Test
     @Ignore
     public void testExpansionAndCollapse() throws InterruptedException {
@@ -143,11 +157,12 @@
     private void testBubblesInCorrectExpandedPositions() {
         // Check all the visible bubbles to see if they're in the right place.
         for (int i = 0; i < mLayout.getChildCount(); i++) {
-            float expectedPosition = mExpandedController.getBubbleXOrYForOrientation(i);
-            assertEquals(expectedPosition,
+            PointF expectedPosition = mPositioner.getExpandedBubbleXY(i,
+                    getStackViewState());
+            assertEquals(expectedPosition.x,
                     mLayout.getChildAt(i).getTranslationX(),
                     2f);
-            assertEquals(expectedPosition,
+            assertEquals(expectedPosition.y,
                     mLayout.getChildAt(i).getTranslationY(), 2f);
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index ef046d4..b888450 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -58,7 +58,7 @@
         mT = mock(SurfaceControl.Transaction.class);
         mMock = mock(IInputMethodManager.class);
         mExecutor = spy(Runnable::run);
-        mPerDisplay = new DisplayImeController(null, null, mExecutor, new TransactionPool() {
+        mPerDisplay = new DisplayImeController(null, null, null, mExecutor, new TransactionPool() {
             @Override
             public SurfaceControl.Transaction acquire() {
                 return mT;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
new file mode 100644
index 0000000..b66c2b4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.util.SparseArray;
+import android.view.IDisplayWindowInsetsController;
+import android.view.IWindowManager;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.TestShellExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+@SmallTest
+public class DisplayInsetsControllerTest {
+
+    private static final int SECOND_DISPLAY = DEFAULT_DISPLAY + 10;
+
+    @Mock
+    private IWindowManager mWm;
+    @Mock
+    private DisplayController mDisplayController;
+    private DisplayInsetsController mController;
+    private SparseArray<IDisplayWindowInsetsController> mInsetsControllersByDisplayId;
+    private TestShellExecutor mExecutor;
+
+    private ArgumentCaptor<Integer> mDisplayIdCaptor;
+    private ArgumentCaptor<IDisplayWindowInsetsController> mInsetsControllerCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mExecutor = new TestShellExecutor();
+        mInsetsControllersByDisplayId = new SparseArray<>();
+        mDisplayIdCaptor =  ArgumentCaptor.forClass(Integer.class);
+        mInsetsControllerCaptor = ArgumentCaptor.forClass(IDisplayWindowInsetsController.class);
+        mController = new DisplayInsetsController(mWm, mDisplayController, mExecutor);
+        addDisplay(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testOnDisplayAdded_setsDisplayWindowInsetsControllerOnWMService()
+            throws RemoteException {
+        addDisplay(SECOND_DISPLAY);
+
+        verify(mWm).setDisplayWindowInsetsController(eq(SECOND_DISPLAY), notNull());
+    }
+
+    @Test
+    public void testOnDisplayRemoved_unsetsDisplayWindowInsetsControllerInWMService()
+            throws RemoteException {
+        addDisplay(SECOND_DISPLAY);
+        removeDisplay(SECOND_DISPLAY);
+
+        verify(mWm).setDisplayWindowInsetsController(SECOND_DISPLAY, null);
+    }
+
+    @Test
+    public void testPerDisplayListenerCallback() throws RemoteException {
+        TrackedListener defaultListener = new TrackedListener();
+        TrackedListener secondListener = new TrackedListener();
+        addDisplay(SECOND_DISPLAY);
+        mController.addInsetsChangedListener(DEFAULT_DISPLAY, defaultListener);
+        mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener);
+
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false);
+        mExecutor.flushAll();
+
+        assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
+        assertTrue(defaultListener.insetsChangedCount == 1);
+        assertTrue(defaultListener.insetsControlChangedCount == 1);
+        assertTrue(defaultListener.showInsetsCount == 1);
+        assertTrue(defaultListener.hideInsetsCount == 1);
+
+        assertTrue(secondListener.topFocusedWindowChangedCount == 0);
+        assertTrue(secondListener.insetsChangedCount == 0);
+        assertTrue(secondListener.insetsControlChangedCount == 0);
+        assertTrue(secondListener.showInsetsCount == 0);
+        assertTrue(secondListener.hideInsetsCount == 0);
+
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false);
+        mExecutor.flushAll();
+
+        assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
+        assertTrue(defaultListener.insetsChangedCount == 1);
+        assertTrue(defaultListener.insetsControlChangedCount == 1);
+        assertTrue(defaultListener.showInsetsCount == 1);
+        assertTrue(defaultListener.hideInsetsCount == 1);
+
+        assertTrue(secondListener.topFocusedWindowChangedCount == 1);
+        assertTrue(secondListener.insetsChangedCount == 1);
+        assertTrue(secondListener.insetsControlChangedCount == 1);
+        assertTrue(secondListener.showInsetsCount == 1);
+        assertTrue(secondListener.hideInsetsCount == 1);
+    }
+
+    private void addDisplay(int displayId) throws RemoteException {
+        mController.onDisplayAdded(displayId);
+        verify(mWm, times(mInsetsControllersByDisplayId.size() + 1))
+                .setDisplayWindowInsetsController(mDisplayIdCaptor.capture(),
+                        mInsetsControllerCaptor.capture());
+        List<Integer> displayIds = mDisplayIdCaptor.getAllValues();
+        List<IDisplayWindowInsetsController> insetsControllers =
+                mInsetsControllerCaptor.getAllValues();
+        for (int i = 0; i < displayIds.size(); i++) {
+            mInsetsControllersByDisplayId.put(displayIds.get(i), insetsControllers.get(i));
+        }
+    }
+
+    private void removeDisplay(int displayId) {
+        mController.onDisplayRemoved(displayId);
+        mInsetsControllersByDisplayId.remove(displayId);
+    }
+
+    private static class TrackedListener implements
+            DisplayInsetsController.OnInsetsChangedListener {
+        int topFocusedWindowChangedCount = 0;
+        int insetsChangedCount = 0;
+        int insetsControlChangedCount = 0;
+        int showInsetsCount = 0;
+        int hideInsetsCount = 0;
+
+        @Override
+        public void topFocusedWindowChanged(String packageName) {
+            topFocusedWindowChangedCount++;
+        }
+
+        @Override
+        public void insetsChanged(InsetsState insetsState) {
+            insetsChangedCount++;
+        }
+
+        @Override
+        public void insetsControlChanged(InsetsState insetsState,
+                InsetsSourceControl[] activeControls) {
+            insetsControlChangedCount++;
+        }
+
+        @Override
+        public void showInsets(int types, boolean fromIme) {
+            showInsetsCount++;
+        }
+
+        @Override
+        public void hideInsets(int types, boolean fromIme) {
+            hideInsetsCount++;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 952dc31..b4caeb5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -24,11 +24,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
 import android.content.res.Configuration;
 import android.graphics.Rect;
-import android.view.SurfaceControl;
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -42,6 +42,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -50,42 +52,56 @@
 @RunWith(AndroidJUnit4.class)
 public class SplitLayoutTests extends ShellTestCase {
     @Mock SplitLayout.SplitLayoutHandler mSplitLayoutHandler;
-    @Mock SurfaceControl mRootLeash;
+    @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks;
     @Mock DisplayImeController mDisplayImeController;
     @Mock ShellTaskOrganizer mTaskOrganizer;
+    @Captor ArgumentCaptor<Runnable> mRunnableCaptor;
     private SplitLayout mSplitLayout;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mSplitLayout = new SplitLayout(
+        mSplitLayout = spy(new SplitLayout(
                 "TestSplitLayout",
                 mContext,
-                getConfiguration(false),
+                getConfiguration(),
                 mSplitLayoutHandler,
-                b -> b.setParent(mRootLeash),
+                mCallbacks,
                 mDisplayImeController,
-                mTaskOrganizer);
+                mTaskOrganizer));
     }
 
     @Test
     @UiThreadTest
     public void testUpdateConfiguration() {
-        mSplitLayout.init();
-        assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse();
-        assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue();
+        final Configuration config = getConfiguration();
+
+        // Verify it returns true if new config won't affect split layout.
+        assertThat(mSplitLayout.updateConfiguration(config)).isFalse();
+
+        // Verify updateConfiguration returns true if the orientation changed.
+        config.orientation = ORIENTATION_LANDSCAPE;
+        assertThat(mSplitLayout.updateConfiguration(config)).isTrue();
+
+        // Verify updateConfiguration returns true if it rotated.
+        config.windowConfiguration.setRotation(1);
+        assertThat(mSplitLayout.updateConfiguration(config)).isTrue();
+
+        // Verify updateConfiguration returns true if the root bounds changed.
+        config.windowConfiguration.setBounds(new Rect(0, 0, 2160, 1080));
+        assertThat(mSplitLayout.updateConfiguration(config)).isTrue();
     }
 
     @Test
     public void testUpdateDivideBounds() {
         mSplitLayout.updateDivideBounds(anyInt());
-        verify(mSplitLayoutHandler).onBoundsChanging(any(SplitLayout.class));
+        verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class));
     }
 
     @Test
     public void testSetDividePosition() {
         mSplitLayout.setDividePosition(anyInt());
-        verify(mSplitLayoutHandler).onBoundsChanged(any(SplitLayout.class));
+        verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class));
     }
 
     @Test
@@ -96,24 +112,40 @@
 
     @Test
     @UiThreadTest
-    public void testSnapToDismissTarget() {
+    public void testSnapToDismissStart() {
         // verify it callbacks properly when the snap target indicates dismissing split.
         DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
                 DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START);
+
         mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
+        waitDividerFlingFinished();
         verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false));
-        snapTarget = getSnapTarget(0 /* position */,
+    }
+
+    @Test
+    @UiThreadTest
+    public void testSnapToDismissEnd() {
+        // verify it callbacks properly when the snap target indicates dismissing split.
+        DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
                 DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END);
+
         mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
+        waitDividerFlingFinished();
         verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true));
     }
 
-    private static Configuration getConfiguration(boolean isLandscape) {
+    private void waitDividerFlingFinished() {
+        verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture());
+        mRunnableCaptor.getValue().run();
+    }
+
+    private static Configuration getConfiguration() {
         final Configuration configuration = new Configuration();
         configuration.unset();
-        configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
+        configuration.orientation = ORIENTATION_PORTRAIT;
+        configuration.windowConfiguration.setRotation(0);
         configuration.windowConfiguration.setBounds(
-                new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160));
+                new Rect(0, 0, 1080, 2160));
         return configuration;
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
index 698315a..9bb54a1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
@@ -22,7 +22,7 @@
 
 import android.content.res.Configuration;
 import android.graphics.Rect;
-import android.view.SurfaceControl;
+import android.view.InsetsState;
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -40,8 +40,8 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class SplitWindowManagerTests extends ShellTestCase {
-    @Mock SurfaceControl mSurfaceControl;
     @Mock SplitLayout mSplitLayout;
+    @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks;
     private SplitWindowManager mSplitWindowManager;
 
     @Before
@@ -50,7 +50,7 @@
         final Configuration configuration = new Configuration();
         configuration.setToDefaults();
         mSplitWindowManager = new SplitWindowManager("TestSplitDivider", mContext, configuration,
-                b -> b.setParent(mSurfaceControl));
+                mCallbacks);
         when(mSplitLayout.getDividerBounds()).thenReturn(
                 new Rect(0, 0, configuration.windowConfiguration.getBounds().width(),
                         configuration.windowConfiguration.getBounds().height()));
@@ -59,7 +59,7 @@
     @Test
     @UiThreadTest
     public void testInitRelease() {
-        mSplitWindowManager.init(mSplitLayout);
+        mSplitWindowManager.init(mSplitLayout, new InsetsState());
         assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull();
         mSplitWindowManager.release();
         assertThat(mSplitWindowManager.getSurfaceControl()).isNull();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
index ba73d55..734b97b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
@@ -25,6 +25,7 @@
 import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
 
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN;
 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM;
@@ -64,6 +65,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.InstanceId;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -95,6 +97,9 @@
     @Mock
     private SplitScreenController mSplitScreenStarter;
 
+    @Mock
+    private InstanceId mLoggerSessionId;
+
     private DisplayLayout mLandscapeDisplayLayout;
     private DisplayLayout mPortraitDisplayLayout;
     private Insets mInsets;
@@ -200,7 +205,7 @@
     @Test
     public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() {
         setRunningTask(mHomeTask);
-        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
 
@@ -210,15 +215,15 @@
     }
 
     @Test
-    public void testDragAppOverFullscreenApp_expectSplitScreenAndFullscreenTargets() {
+    public void testDragAppOverFullscreenApp_expectSplitScreenTargets() {
         setRunningTask(mFullscreenAppTask);
-        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
-                mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
+                mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData);
         verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
+                eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
         reset(mSplitScreenStarter);
 
         mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
@@ -227,15 +232,15 @@
     }
 
     @Test
-    public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenAndFullscreenTargets() {
+    public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() {
         setRunningTask(mFullscreenAppTask);
-        mPolicy.start(mPortraitDisplayLayout, mActivityClipData);
+        mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
-                mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
+                mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData);
         verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
+                eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
         reset(mSplitScreenStarter);
 
         mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData);
@@ -244,71 +249,61 @@
     }
 
     @Test
-    public void testDragAppOverFullscreenNonResizeableApp_expectOnlyFullscreenTargets() {
-        setRunningTask(mNonResizeableFullscreenAppTask);
-        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
-        ArrayList<Target> targets = assertExactTargetTypes(
-                mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
-
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
-        verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
-    }
-
-    @Test
-    public void testDragNonResizeableAppOverFullscreenApp_expectOnlyFullscreenTargets() {
-        setRunningTask(mFullscreenAppTask);
-        mPolicy.start(mLandscapeDisplayLayout, mNonResizeableActivityClipData);
-        ArrayList<Target> targets = assertExactTargetTypes(
-                mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
-
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
-        verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
-    }
-
-    @Test
-    public void testDragAppOverSplitApp_expectFullscreenAndSplitTargets() {
+    public void testDragAppOverSplitApp_expectSplitTargets_DropLeft() {
         setInSplitScreen(true);
         setRunningTask(mSplitPrimaryAppTask);
-        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
-                mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
+                mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData);
         verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
-        reset(mSplitScreenStarter);
+                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
+    }
 
-        // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs
+    @Test
+    public void testDragAppOverSplitApp_expectSplitTargets_DropRight() {
+        setInSplitScreen(true);
+        setRunningTask(mSplitPrimaryAppTask);
+        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
+        ArrayList<Target> targets = assertExactTargetTypes(
+                mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
+
         mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
         verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
+                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
     }
 
     @Test
-    public void testDragAppOverSplitAppPhone_expectFullscreenAndVerticalSplitTargets() {
+    public void testDragAppOverSplitAppPhone_expectVerticalSplitTargets_DropTop() {
         setInSplitScreen(true);
         setRunningTask(mSplitPrimaryAppTask);
-        mPolicy.start(mPortraitDisplayLayout, mActivityClipData);
+        mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
-                mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
+                mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData);
         verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any());
-        reset(mSplitScreenStarter);
+                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_TOP_OR_LEFT), any());
+    }
 
-        // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs
+    @Test
+    public void testDragAppOverSplitAppPhone_expectVerticalSplitTargets_DropBottom() {
+        setInSplitScreen(true);
+        setRunningTask(mSplitPrimaryAppTask);
+        mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId);
+        ArrayList<Target> targets = assertExactTargetTypes(
+                mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
+
         mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData);
         verify(mSplitScreenStarter).startIntent(any(), any(),
-                eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
+                eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
     }
 
     @Test
     public void testTargetHitRects() {
         setRunningTask(mFullscreenAppTask);
-        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData);
+        mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
         ArrayList<Target> targets = mPolicy.getTargets(mInsets);
         for (Target t : targets) {
             assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
new file mode 100644
index 0000000..d6f7e54
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.fullscreen;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.WindowConfiguration;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@SmallTest
+public class FullscreenTaskListenerTest {
+
+    @Mock
+    private SyncTransactionQueue mSyncQueue;
+    @Mock
+    private FullscreenUnfoldController mUnfoldController;
+    @Mock
+    private SurfaceControl mSurfaceControl;
+
+    private Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
+
+    private FullscreenTaskListener mListener;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mFullscreenUnfoldController = Optional.of(mUnfoldController);
+        mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController);
+    }
+
+    @Test
+    public void testAnimatableTaskAppeared_notifiesUnfoldController() {
+        RunningTaskInfo info = createTaskInfo(/* visible */ true, /* taskId */ 0);
+
+        mListener.onTaskAppeared(info, mSurfaceControl);
+
+        verify(mUnfoldController).onTaskAppeared(eq(info), any());
+    }
+
+    @Test
+    public void testMultipleAnimatableTasksAppeared_notifiesUnfoldController() {
+        RunningTaskInfo animatable1 = createTaskInfo(/* visible */ true, /* taskId */ 0);
+        RunningTaskInfo animatable2 = createTaskInfo(/* visible */ true, /* taskId */ 1);
+
+        mListener.onTaskAppeared(animatable1, mSurfaceControl);
+        mListener.onTaskAppeared(animatable2, mSurfaceControl);
+
+        InOrder order = inOrder(mUnfoldController);
+        order.verify(mUnfoldController).onTaskAppeared(eq(animatable1), any());
+        order.verify(mUnfoldController).onTaskAppeared(eq(animatable2), any());
+    }
+
+    @Test
+    public void testNonAnimatableTaskAppeared_doesNotNotifyUnfoldController() {
+        RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
+
+        mListener.onTaskAppeared(info, mSurfaceControl);
+
+        verifyNoMoreInteractions(mUnfoldController);
+    }
+
+    @Test
+    public void testNonAnimatableTaskChanged_doesNotNotifyUnfoldController() {
+        RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
+        mListener.onTaskAppeared(info, mSurfaceControl);
+
+        mListener.onTaskInfoChanged(info);
+
+        verifyNoMoreInteractions(mUnfoldController);
+    }
+
+    @Test
+    public void testNonAnimatableTaskVanished_doesNotNotifyUnfoldController() {
+        RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
+        mListener.onTaskAppeared(info, mSurfaceControl);
+
+        mListener.onTaskVanished(info);
+
+        verifyNoMoreInteractions(mUnfoldController);
+    }
+
+    @Test
+    public void testAnimatableTaskBecameInactive_notifiesUnfoldController() {
+        RunningTaskInfo animatableTask = createTaskInfo(/* visible */ true, /* taskId */ 0);
+        mListener.onTaskAppeared(animatableTask, mSurfaceControl);
+        RunningTaskInfo notAnimatableTask = createTaskInfo(/* visible */ false, /* taskId */ 0);
+
+        mListener.onTaskInfoChanged(notAnimatableTask);
+
+        verify(mUnfoldController).onTaskVanished(eq(notAnimatableTask));
+    }
+
+    @Test
+    public void testAnimatableTaskVanished_notifiesUnfoldController() {
+        RunningTaskInfo taskInfo = createTaskInfo(/* visible */ true, /* taskId */ 0);
+        mListener.onTaskAppeared(taskInfo, mSurfaceControl);
+
+        mListener.onTaskVanished(taskInfo);
+
+        verify(mUnfoldController).onTaskVanished(eq(taskInfo));
+    }
+
+    private RunningTaskInfo createTaskInfo(boolean visible, int taskId) {
+        final RunningTaskInfo info = spy(new RunningTaskInfo());
+        info.isVisible = visible;
+        info.positionInParent = new Point();
+        when(info.getWindowingMode()).thenReturn(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
+        final Configuration configuration = new Configuration();
+        configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
+        when(info.getConfiguration()).thenReturn(configuration);
+        info.taskId = taskId;
+        return info;
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
index 911fe07..0a3a849 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -42,6 +42,7 @@
 import android.view.Display;
 import android.view.Surface;
 import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
 
@@ -332,6 +333,58 @@
     }
 
     @Test
+    public void testOneHandedEnabledRotation90ShouldHandleRotate() {
+        when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true);
+        when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
+                false);
+        final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
+        mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
+                Surface.ROTATION_90, handlerWCT);
+
+        verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext),
+                eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
+    }
+
+    @Test
+    public void testOneHandedDisabledRotation90ShouldNotHandleRotate() {
+        when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(false);
+        when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
+                false);
+        final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
+        mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
+                Surface.ROTATION_90, handlerWCT);
+
+        verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext),
+                eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
+    }
+
+    @Test
+    public void testSwipeToNotificationEnabledRotation90ShouldNotHandleRotate() {
+        when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true);
+        when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
+                true);
+        final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
+        mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
+                Surface.ROTATION_90, handlerWCT);
+
+        verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext),
+                eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
+    }
+
+    @Test
+    public void testSwipeToNotificationDisabledRotation90ShouldHandleRotate() {
+        when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true);
+        when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn(
+                false);
+        final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
+        mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0,
+                Surface.ROTATION_90, handlerWCT);
+
+        verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext),
+                eq(Surface.ROTATION_90), any(WindowContainerTransaction.class));
+    }
+
+    @Test
     public void testStateActive_shortcutRequestActivate_skipActions() {
         when(mSpiedTransitionState.getState()).thenReturn(STATE_ACTIVE);
         when(mSpiedTransitionState.isTransitioning()).thenReturn(false);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 9d7c82b..0270093 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -79,6 +79,7 @@
     @Mock private ShellTaskOrganizer mMockShellTaskOrganizer;
     private TestShellExecutor mMainExecutor;
     private PipBoundsState mPipBoundsState;
+    private PipTransitionState mPipTransitionState;
     private PipBoundsAlgorithm mPipBoundsAlgorithm;
 
     private ComponentName mComponent1;
@@ -90,11 +91,12 @@
         mComponent1 = new ComponentName(mContext, "component1");
         mComponent2 = new ComponentName(mContext, "component2");
         mPipBoundsState = new PipBoundsState(mContext);
+        mPipTransitionState = new PipTransitionState();
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
                 new PipSnapAlgorithm());
         mMainExecutor = new TestShellExecutor();
         mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext,
-                mMockSyncTransactionQueue, mPipBoundsState,
+                mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
                 mPipBoundsAlgorithm, mMockPhonePipMenuController,
                 mMockPipAnimationController, mMockPipSurfaceTransactionHelper,
                 mMockPipTransitionController, mMockOptionalSplitScreen, mMockDisplayController,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
index 1bb5fd1..2bcc45e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
@@ -56,13 +56,14 @@
         MockitoAnnotations.initMocks(this);
         mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
         mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession, null);
         mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash);
     }
 
     @Test
     public void testActiveDeactivate() {
-        mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct);
+        mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct,
+                true /* reparent */);
         assertThat(mMainStage.isActive()).isTrue();
 
         mMainStage.deactivate(mWct);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
index 56a0056..838aa81 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
@@ -29,10 +29,12 @@
 import android.view.SurfaceSession;
 import android.window.WindowContainerTransaction;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.SyncTransactionQueue;
 
@@ -46,7 +48,7 @@
 /** Tests for {@link SideStage} */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SideStageTests {
+public class SideStageTests extends ShellTestCase {
     @Mock private ShellTaskOrganizer mTaskOrganizer;
     @Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
     @Mock private SyncTransactionQueue mSyncQueue;
@@ -57,11 +59,12 @@
     private SideStage mSideStage;
 
     @Before
+    @UiThreadTest
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mRootTask = new TestRunningTaskInfoBuilder().build();
-        mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue,
-                mSurfaceSession);
+        mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
+                mSyncQueue, mSurfaceSession, null);
         mSideStage.onTaskAppeared(mRootTask, mRootLeash);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index ab6f7699..f90af23 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -18,7 +18,6 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
-
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 
@@ -33,11 +32,16 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.Optional;
+
+import javax.inject.Provider;
+
 public class SplitTestUtils {
 
     static SplitLayout createMockSplitLayout() {
@@ -65,9 +69,13 @@
         TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
                 RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
                 MainStage mainStage, SideStage sideStage, DisplayImeController imeController,
-                SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) {
+                DisplayInsetsController insetsController, SplitLayout splitLayout,
+                Transitions transitions, TransactionPool transactionPool,
+                SplitscreenEventLogger logger,
+                Provider<Optional<StageTaskUnfoldController>> unfoldController) {
             super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
-                    sideStage, imeController, splitLayout, transitions, transactionPool);
+                    sideStage, imeController, insetsController, splitLayout, transitions,
+                    transactionPool, logger, unfoldController);
 
             // Prepare default TaskDisplayArea for testing.
             mDisplayAreaInfo = new DisplayAreaInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index aca80f3..05496b0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -46,10 +46,12 @@
 import android.view.SurfaceSession;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -58,6 +60,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
@@ -71,6 +74,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
 
+import java.util.Optional;
+
 /** Tests for {@link StageCoordinator} */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -79,9 +84,11 @@
     @Mock private SyncTransactionQueue mSyncQueue;
     @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
     @Mock private DisplayImeController mDisplayImeController;
+    @Mock private DisplayInsetsController mDisplayInsetsController;
     @Mock private TransactionPool mTransactionPool;
     @Mock private Transitions mTransitions;
     @Mock private SurfaceSession mSurfaceSession;
+    @Mock private SplitscreenEventLogger mLogger;
     private SplitLayout mSplitLayout;
     private MainStage mMainStage;
     private SideStage mSideStage;
@@ -92,6 +99,7 @@
     private ActivityManager.RunningTaskInfo mSideChild;
 
     @Before
+    @UiThreadTest
     public void setup() {
         MockitoAnnotations.initMocks(this);
         final ShellExecutor mockExecutor = mock(ShellExecutor.class);
@@ -100,14 +108,16 @@
         doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
         mSplitLayout = SplitTestUtils.createMockSplitLayout();
         mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
-                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
+                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null);
         mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
-        mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
-                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
+        mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
+                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null);
         mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
-                    mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
-                    mDisplayImeController, mSplitLayout, mTransitions, mTransactionPool);
+                mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
+                mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
+                mTransactionPool,
+                mLogger, Optional::empty);
         mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
         doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
                 .when(mTransitions).startTransition(anyInt(), any(), any());
@@ -125,12 +135,13 @@
         TestRemoteTransition testRemote = new TestRemoteTransition();
 
         IBinder transition = mSplitScreenTransitions.startEnterTransition(
-                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), testRemote,
-                mStageCoordinator);
+                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
+                new RemoteTransition(testRemote), mStageCoordinator);
         mMainStage.onTaskAppeared(mMainChild, createMockSurface());
         mSideStage.onTaskAppeared(mSideChild, createMockSurface());
         boolean accepted = mStageCoordinator.startAnimation(transition, info,
                 mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
         assertTrue(accepted);
 
@@ -168,6 +179,7 @@
         mSideStage.onTaskAppeared(newTask, createMockSurface());
         boolean accepted = mStageCoordinator.startAnimation(transition, info,
                 mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
         assertFalse(accepted);
         assertTrue(mStageCoordinator.isSplitScreenVisible());
@@ -188,6 +200,7 @@
         mSideStage.onTaskVanished(newTask);
         accepted = mStageCoordinator.startAnimation(transition, info,
                 mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
         assertFalse(accepted);
         assertTrue(mStageCoordinator.isSplitScreenVisible());
@@ -223,6 +236,7 @@
         mSideStage.onTaskVanished(mSideChild);
         mStageCoordinator.startAnimation(transition, info,
                 mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
         assertFalse(mStageCoordinator.isSplitScreenVisible());
     }
@@ -244,6 +258,7 @@
         mSideStage.onTaskVanished(mSideChild);
         boolean accepted = mStageCoordinator.startAnimation(transition, info,
                 mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
         assertTrue(accepted);
         assertFalse(mStageCoordinator.isSplitScreenVisible());
@@ -274,6 +289,7 @@
         mSideStage.onTaskVanished(mSideChild);
         boolean accepted = mStageCoordinator.startAnimation(transition, info,
                 mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
         assertTrue(accepted);
         assertFalse(mStageCoordinator.isSplitScreenVisible());
@@ -293,13 +309,15 @@
         TransitionInfo enterInfo = createEnterPairInfo();
         IBinder enterTransit = mSplitScreenTransitions.startEnterTransition(
                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
-                new TestRemoteTransition(), mStageCoordinator);
+                new RemoteTransition(new TestRemoteTransition()), mStageCoordinator);
         mMainStage.onTaskAppeared(mMainChild, createMockSurface());
         mSideStage.onTaskAppeared(mSideChild, createMockSurface());
         mStageCoordinator.startAnimation(enterTransit, enterInfo,
                 mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
-        mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction());
+        mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction(),
+                true /* includingTopTask */);
     }
 
     private boolean containsSplitExit(@NonNull WindowContainerTransaction wct) {
@@ -335,10 +353,11 @@
 
         @Override
         public void startAnimation(IBinder transition, TransitionInfo info,
-                SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
+                SurfaceControl.Transaction startTransaction,
+                IRemoteTransitionFinishedCallback finishCallback)
                 throws RemoteException {
             mCalled = true;
-            finishCallback.onTransitionFinished(mRemoteFinishWCT);
+            finishCallback.onTransitionFinished(mRemoteFinishWCT, null /* sct */);
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 06b0868..cd29220 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -16,17 +16,24 @@
 
 package com.android.wm.shell.splitscreen;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.graphics.Rect;
+import android.window.DisplayAreaInfo;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -37,8 +44,10 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -47,26 +56,55 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-/** Tests for {@link StageCoordinator} */
+import java.util.Optional;
+
+import javax.inject.Provider;
+
+/**
+ * Tests for {@link StageCoordinator}
+ */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class StageCoordinatorTests extends ShellTestCase {
-    @Mock private ShellTaskOrganizer mTaskOrganizer;
-    @Mock private SyncTransactionQueue mSyncQueue;
-    @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
-    @Mock private MainStage mMainStage;
-    @Mock private SideStage mSideStage;
-    @Mock private DisplayImeController mDisplayImeController;
-    @Mock private Transitions mTransitions;
-    @Mock private TransactionPool mTransactionPool;
+    @Mock
+    private ShellTaskOrganizer mTaskOrganizer;
+    @Mock
+    private SyncTransactionQueue mSyncQueue;
+    @Mock
+    private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+    @Mock
+    private MainStage mMainStage;
+    @Mock
+    private SideStage mSideStage;
+    @Mock
+    private StageTaskUnfoldController mMainUnfoldController;
+    @Mock
+    private StageTaskUnfoldController mSideUnfoldController;
+    @Mock
+    private SplitLayout mSplitLayout;
+    @Mock
+    private DisplayImeController mDisplayImeController;
+    @Mock
+    private DisplayInsetsController mDisplayInsetsController;
+    @Mock
+    private Transitions mTransitions;
+    @Mock
+    private TransactionPool mTransactionPool;
+    @Mock
+    private SplitscreenEventLogger mLogger;
+
+    private final Rect mBounds1 = new Rect(10, 20, 30, 40);
+    private final Rect mBounds2 = new Rect(5, 10, 15, 20);
+
     private StageCoordinator mStageCoordinator;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
-                mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
-                mDisplayImeController, null /* splitLayout */, mTransitions, mTransactionPool);
+        mStageCoordinator = createStageCoordinator(/* splitLayout */ null);
+
+        when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
+        when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
     }
 
     @Test
@@ -75,12 +113,45 @@
 
         mStageCoordinator.moveToSideStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT);
 
-        verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class));
+        verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class),
+                eq(true /* includingTopTask */));
         verify(mSideStage).addTask(eq(task), any(Rect.class),
                 any(WindowContainerTransaction.class));
     }
 
     @Test
+    public void testDisplayAreaAppeared_initializesUnfoldControllers() {
+        mStageCoordinator.onDisplayAreaAppeared(mock(DisplayAreaInfo.class));
+
+        verify(mMainUnfoldController).init();
+        verify(mSideUnfoldController).init();
+    }
+
+    @Test
+    public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() {
+        mStageCoordinator = createStageCoordinator(mSplitLayout);
+        mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null);
+        clearInvocations(mMainUnfoldController, mSideUnfoldController);
+
+        mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
+
+        verify(mMainUnfoldController).onLayoutChanged(mBounds2);
+        verify(mSideUnfoldController).onLayoutChanged(mBounds1);
+    }
+
+    @Test
+    public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() {
+        mStageCoordinator = createStageCoordinator(mSplitLayout);
+        mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null);
+        clearInvocations(mMainUnfoldController, mSideUnfoldController);
+
+        mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
+
+        verify(mMainUnfoldController).onLayoutChanged(mBounds1);
+        verify(mSideUnfoldController).onLayoutChanged(mBounds2);
+    }
+
+    @Test
     public void testRemoveFromSideStage() {
         final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
 
@@ -90,4 +161,61 @@
         verify(mSideStage).removeTask(
                 eq(task.taskId), any(), any(WindowContainerTransaction.class));
     }
+
+    @Test
+    public void testExitSplitScreen() {
+        mStageCoordinator.exitSplitScreen(INVALID_TASK_ID,
+                SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+        verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false));
+        verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
+    }
+
+    @Test
+    public void testExitSplitScreenToMainStage() {
+        final int testTaskId = 12345;
+        when(mMainStage.containsTask(eq(testTaskId))).thenReturn(true);
+        when(mSideStage.containsTask(eq(testTaskId))).thenReturn(false);
+        mStageCoordinator.exitSplitScreen(testTaskId,
+                SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+        verify(mMainStage).reorderChild(eq(testTaskId), eq(true),
+                any(WindowContainerTransaction.class));
+        verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false));
+        verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(true));
+    }
+
+    @Test
+    public void testExitSplitScreenToSideStage() {
+        final int testTaskId = 12345;
+        when(mMainStage.containsTask(eq(testTaskId))).thenReturn(false);
+        when(mSideStage.containsTask(eq(testTaskId))).thenReturn(true);
+        mStageCoordinator.exitSplitScreen(testTaskId,
+                SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+        verify(mSideStage).reorderChild(eq(testTaskId), eq(true),
+                any(WindowContainerTransaction.class));
+        verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true));
+        verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
+    }
+
+    private StageCoordinator createStageCoordinator(SplitLayout splitLayout) {
+        return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
+                mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
+                mDisplayImeController, mDisplayInsetsController, splitLayout,
+                mTransitions, mTransactionPool, mLogger, new UnfoldControllerProvider());
+    }
+
+    private class UnfoldControllerProvider implements
+            Provider<Optional<StageTaskUnfoldController>> {
+
+        private boolean isMain = true;
+
+        @Override
+        public Optional<StageTaskUnfoldController> get() {
+            if (isMain) {
+                isMain = false;
+                return Optional.of(mMainUnfoldController);
+            } else {
+                return Optional.of(mSideUnfoldController);
+            }
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 90b5b37..a5746a4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -21,11 +21,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
+import android.os.SystemProperties;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 
@@ -52,11 +55,16 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public final class StageTaskListenerTests {
+    private static final boolean ENABLE_SHELL_TRANSITIONS =
+            SystemProperties.getBoolean("persist.debug.shell_transit", false);
+
     @Mock private ShellTaskOrganizer mTaskOrganizer;
     @Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
     @Mock private SyncTransactionQueue mSyncQueue;
+    @Mock private StageTaskUnfoldController mStageTaskUnfoldController;
     @Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
     private SurfaceSession mSurfaceSession = new SurfaceSession();
+    private SurfaceControl mSurfaceControl;
     private ActivityManager.RunningTaskInfo mRootTask;
     private StageTaskListener mStageTaskListener;
 
@@ -68,10 +76,12 @@
                 DEFAULT_DISPLAY,
                 mCallbacks,
                 mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession,
+                mStageTaskUnfoldController);
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mRootTask.parentTaskId = INVALID_TASK_ID;
-        mStageTaskListener.onTaskAppeared(mRootTask, new SurfaceControl());
+        mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
+        mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl);
     }
 
     @Test
@@ -93,15 +103,39 @@
 
     @Test
     public void testChildTaskAppeared() {
+        // With shell transitions, the transition manages status changes, so skip this test.
+        assumeFalse(ENABLE_SHELL_TRANSITIONS);
         final ActivityManager.RunningTaskInfo childTask =
                 new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
 
-        mStageTaskListener.onTaskAppeared(childTask, new SurfaceControl());
+        mStageTaskListener.onTaskAppeared(childTask, mSurfaceControl);
 
         assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue();
         verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));
     }
 
+    @Test
+    public void testTaskAppeared_notifiesUnfoldListener() {
+        final ActivityManager.RunningTaskInfo task =
+                new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
+
+        mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
+
+        verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl));
+    }
+
+    @Test
+    public void testTaskVanished_notifiesUnfoldListener() {
+        final ActivityManager.RunningTaskInfo task =
+                new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
+        mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
+        clearInvocations(mStageTaskUnfoldController);
+
+        mStageTaskListener.onTaskVanished(task);
+
+        verify(mStageTaskUnfoldController).onTaskVanished(eq(task));
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testUnknownTaskVanished() {
         final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
@@ -110,6 +144,8 @@
 
     @Test
     public void testTaskVanished() {
+        // With shell transitions, the transition manages status changes, so skip this test.
+        assumeFalse(ENABLE_SHELL_TRANSITIONS);
         final ActivityManager.RunningTaskInfo childTask =
                 new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
         mStageTaskListener.mRootTaskInfo = mRootTask;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index eef0d9b..e5a8aa0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -30,6 +30,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -52,15 +53,16 @@
 import android.os.Looper;
 import android.os.UserHandle;
 import android.testing.TestableContext;
+import android.view.Display;
 import android.view.IWindowSession;
 import android.view.InsetsState;
 import android.view.Surface;
-import android.view.SurfaceControl;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.WindowMetrics;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskSnapshot;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -101,7 +103,6 @@
 
     static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{
         int mAddWindowForTask = 0;
-        int mViewThemeResId;
 
         TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
                 TransactionPool pool) {
@@ -109,20 +110,18 @@
         }
 
         @Override
-        protected boolean addWindow(int taskId, IBinder appToken,
-                View view, WindowManager wm, WindowManager.LayoutParams params, int suggestType) {
+        protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
+                WindowManager.LayoutParams params, int suggestType) {
             // listen for addView
             mAddWindowForTask = taskId;
-            mViewThemeResId = view.getContext().getThemeResId();
             // Do not wait for background color
             return false;
         }
 
         @Override
-        protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) {
+        protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo) {
             // listen for removeView
-            if (mAddWindowForTask == taskId) {
+            if (mAddWindowForTask == removalInfo.taskId) {
                 mAddWindowForTask = 0;
             }
         }
@@ -172,9 +171,11 @@
                 eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN));
         assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId);
 
-        mStartingSurfaceDrawer.removeStartingWindow(windowInfo.taskInfo.taskId, null, null, false);
+        StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo();
+        removalInfo.taskId = windowInfo.taskInfo.taskId;
+        mStartingSurfaceDrawer.removeStartingWindow(removalInfo);
         waitHandlerIdle(mTestHandler);
-        verify(mStartingSurfaceDrawer).removeWindowSynced(eq(taskId), any(), any(), eq(false));
+        verify(mStartingSurfaceDrawer).removeWindowSynced(any());
         assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0);
     }
 
@@ -183,12 +184,15 @@
         final int taskId = 1;
         final StartingWindowInfo windowInfo =
                 createWindowInfo(taskId, 0);
+        final int[] theme = new int[1];
+        doAnswer(invocation -> theme[0] = (Integer) invocation.callRealMethod())
+                .when(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any());
+
         mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder,
                 STARTING_WINDOW_TYPE_SPLASH_SCREEN);
         waitHandlerIdle(mTestHandler);
-        verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(),
-                eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN));
-        assertNotEquals(mStartingSurfaceDrawer.mViewThemeResId, 0);
+        verify(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any());
+        assertNotEquals(theme[0], 0);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
index a098a68..aad9528 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
@@ -83,8 +83,7 @@
                 createTaskDescription(Color.WHITE, Color.RED, Color.BLUE),
                 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */,
                 taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD,
-                100 /* delayRemovalTime */, new InsetsState(),
-                null /* clearWindow */, new TestShellExecutor());
+                new InsetsState(), null /* clearWindow */, new TestShellExecutor());
     }
 
     private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 2d2ab2c..e391713 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -20,12 +20,20 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
+import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 
@@ -48,10 +56,14 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.view.IDisplayWindowListener;
+import android.view.IWindowManager;
+import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
@@ -65,17 +77,23 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
 
 /**
  * Tests for the shell transitions.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:ShellTransitionTests
  */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -97,8 +115,7 @@
 
     @Test
     public void testBasicTransitionFlow() {
-        Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
-                mMainExecutor, mAnimExecutor);
+        Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
 
         IBinder transitToken = new Binder();
@@ -117,8 +134,7 @@
 
     @Test
     public void testNonDefaultHandler() {
-        Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
-                mMainExecutor, mAnimExecutor);
+        Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
 
         final WindowContainerTransaction handlerWCT = new WindowContainerTransaction();
@@ -127,11 +143,13 @@
         TestTransitionHandler testHandler = new TestTransitionHandler() {
             @Override
             public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-                    @NonNull SurfaceControl.Transaction t,
+                    @NonNull SurfaceControl.Transaction startTransaction,
+                    @NonNull SurfaceControl.Transaction finishTransaction,
                     @NonNull Transitions.TransitionFinishCallback finishCallback) {
                 for (TransitionInfo.Change chg : info.getChanges()) {
                     if (chg.getMode() == TRANSIT_CHANGE) {
-                        return super.startAnimation(transition, info, t, finishCallback);
+                        return super.startAnimation(transition, info, startTransaction,
+                                finishTransaction, finishCallback);
                     }
                 }
                 return false;
@@ -199,8 +217,7 @@
 
     @Test
     public void testRequestRemoteTransition() {
-        Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
-                mMainExecutor, mAnimExecutor);
+        Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
 
         final boolean[] remoteCalled = new boolean[]{false};
@@ -211,7 +228,7 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
                 remoteCalled[0] = true;
-                finishCallback.onTransitionFinished(remoteFinishWCT);
+                finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
             }
 
             @Override
@@ -222,7 +239,8 @@
         };
         IBinder transitToken = new Binder();
         transitions.requestStartTransition(transitToken,
-                new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, testRemote));
+                new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */,
+                        new RemoteTransition(testRemote)));
         verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any());
         TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
                 .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
@@ -273,9 +291,76 @@
     }
 
     @Test
+    public void testTransitionFilterNotRequirement() {
+        // filter that requires one opening and NO translucent apps
+        TransitionFilter filter = new TransitionFilter();
+        filter.mRequirements = new TransitionFilter.Requirement[]{
+                new TransitionFilter.Requirement(), new TransitionFilter.Requirement()};
+        filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+        filter.mRequirements[1].mFlags = FLAG_TRANSLUCENT;
+        filter.mRequirements[1].mNot = true;
+
+        final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        assertTrue(filter.matches(openOnly));
+
+        final TransitionInfo openAndTranslucent = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+        openAndTranslucent.getChanges().get(1).setFlags(FLAG_TRANSLUCENT);
+        assertFalse(filter.matches(openAndTranslucent));
+    }
+
+    @Test
+    public void testTransitionFilterChecksTypeSet() {
+        TransitionFilter filter = new TransitionFilter();
+        filter.mTypeSet = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+
+        final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        assertTrue(filter.matches(openOnly));
+
+        final TransitionInfo toFrontOnly = new TransitionInfoBuilder(TRANSIT_TO_FRONT)
+                .addChange(TRANSIT_TO_FRONT).build();
+        assertTrue(filter.matches(toFrontOnly));
+
+        final TransitionInfo closeOnly = new TransitionInfoBuilder(TRANSIT_CLOSE)
+                .addChange(TRANSIT_CLOSE).build();
+        assertFalse(filter.matches(closeOnly));
+    }
+
+    @Test
+    public void testTransitionFilterChecksFlags() {
+        TransitionFilter filter = new TransitionFilter();
+        filter.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+
+        final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK,
+                TRANSIT_FLAG_KEYGUARD_GOING_AWAY)
+                .addChange(TRANSIT_TO_BACK).build();
+        assertTrue(filter.matches(withFlag));
+
+        final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        assertFalse(filter.matches(withoutFlag));
+    }
+
+    @Test
+    public void testTransitionFilterChecksNotFlags() {
+        TransitionFilter filter = new TransitionFilter();
+        filter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+
+        final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK,
+                TRANSIT_FLAG_KEYGUARD_GOING_AWAY)
+                .addChange(TRANSIT_TO_BACK).build();
+        assertFalse(filter.matches(withFlag));
+
+        final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        assertTrue(filter.matches(withoutFlag));
+    }
+
+    @Test
     public void testRegisteredRemoteTransition() {
-        Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
-                mMainExecutor, mAnimExecutor);
+        Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
 
         final boolean[] remoteCalled = new boolean[]{false};
@@ -285,7 +370,7 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
                 remoteCalled[0] = true;
-                finishCallback.onTransitionFinished(null /* wct */);
+                finishCallback.onTransitionFinished(null /* wct */, null /* sct */);
             }
 
             @Override
@@ -300,7 +385,7 @@
                 new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
         filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
 
-        transitions.registerRemote(filter, testRemote);
+        transitions.registerRemote(filter, new RemoteTransition(testRemote));
         mMainExecutor.flushAll();
 
         IBinder transitToken = new Binder();
@@ -320,8 +405,7 @@
 
     @Test
     public void testOneShotRemoteHandler() {
-        Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
-                mMainExecutor, mAnimExecutor);
+        Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
 
         final boolean[] remoteCalled = new boolean[]{false};
@@ -332,7 +416,7 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
                 remoteCalled[0] = true;
-                finishCallback.onTransitionFinished(remoteFinishWCT);
+                finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
             }
 
             @Override
@@ -344,11 +428,12 @@
 
         final int transitType = TRANSIT_FIRST_CUSTOM + 1;
 
-        OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor, testRemote);
+        OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor,
+                new RemoteTransition(testRemote));
         // Verify that it responds to the remote but not other things.
         IBinder transitToken = new Binder();
         assertNotNull(oneShot.handleRequest(transitToken,
-                new TransitionRequestInfo(transitType, null, testRemote)));
+                new TransitionRequestInfo(transitType, null, new RemoteTransition(testRemote))));
         assertNull(oneShot.handleRequest(transitToken,
                 new TransitionRequestInfo(transitType, null, null)));
 
@@ -358,15 +443,16 @@
         oneShot.setTransition(transitToken);
         IBinder anotherToken = new Binder();
         assertFalse(oneShot.startAnimation(anotherToken, new TransitionInfo(transitType, 0),
-                mock(SurfaceControl.Transaction.class), testFinish));
+                mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
+                testFinish));
         assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0),
-                mock(SurfaceControl.Transaction.class), testFinish));
+                mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
+                testFinish));
     }
 
     @Test
     public void testTransitionQueueing() {
-        Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
-                mMainExecutor, mAnimExecutor);
+        Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
 
         IBinder transitToken1 = new Binder();
@@ -406,8 +492,7 @@
 
     @Test
     public void testTransitionMerging() {
-        Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext,
-                mMainExecutor, mAnimExecutor);
+        Transitions transitions = createTestTransitions();
         mDefaultHandler.setSimulateMerge(true);
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
 
@@ -443,11 +528,80 @@
         assertEquals(0, mDefaultHandler.activeCount());
     }
 
+    @Test
+    public void testShouldRotateSeamlessly() throws Exception {
+        final RunningTaskInfo taskInfo =
+                createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+        final RunningTaskInfo taskInfoPip =
+                createTaskInfo(1, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
+
+        final DisplayController displays = createTestDisplayController();
+        final @Surface.Rotation int upsideDown = displays
+                .getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation();
+
+        final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE)
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
+                        .build())
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build())
+                .build();
+        assertFalse(DefaultTransitionHandler.isRotationSeamless(normalDispRotate, displays));
+
+        // Seamless if all tasks are seamless
+        final TransitionInfo rotateSeamless = new TransitionInfoBuilder(TRANSIT_CHANGE)
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
+                        .build())
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+                        .setRotate(ROTATION_ANIMATION_SEAMLESS).build())
+                .build();
+        assertTrue(DefaultTransitionHandler.isRotationSeamless(rotateSeamless, displays));
+
+        // Not seamless if there is PiP (or any other non-seamless task)
+        final TransitionInfo pipDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE)
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
+                        .build())
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+                        .setRotate(ROTATION_ANIMATION_SEAMLESS).build())
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfoPip)
+                        .setRotate().build())
+                .build();
+        assertFalse(DefaultTransitionHandler.isRotationSeamless(pipDispRotate, displays));
+
+        // Not seamless if one of rotations is upside-down
+        final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE)
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
+                        .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build())
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+                        .setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build())
+                .build();
+        assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays));
+
+        // Not seamless if system alert windows
+        final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE)
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(
+                        FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build())
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
+                        .setRotate(ROTATION_ANIMATION_SEAMLESS).build())
+                .build();
+        assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays));
+
+        // Not seamless if there is no changed task.
+        final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE)
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
+                        .setRotate().build())
+                .build();
+        assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays));
+    }
+
     class TransitionInfoBuilder {
         final TransitionInfo mInfo;
 
         TransitionInfoBuilder(@WindowManager.TransitionType int type) {
-            mInfo = new TransitionInfo(type, 0 /* flags */);
+            this(type, 0 /* flags */);
+        }
+
+        TransitionInfoBuilder(@WindowManager.TransitionType int type,
+                @WindowManager.TransitionFlags int flags) {
+            mInfo = new TransitionInfo(type, flags);
             mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0);
         }
 
@@ -465,11 +619,53 @@
             return addChange(mode, null /* taskInfo */);
         }
 
+        TransitionInfoBuilder addChange(TransitionInfo.Change change) {
+            mInfo.addChange(change);
+            return this;
+        }
+
         TransitionInfo build() {
             return mInfo;
         }
     }
 
+    class ChangeBuilder {
+        final TransitionInfo.Change mChange;
+
+        ChangeBuilder(@WindowManager.TransitionType int mode) {
+            mChange = new TransitionInfo.Change(null /* token */, null /* leash */);
+            mChange.setMode(mode);
+        }
+
+        ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) {
+            mChange.setFlags(flags);
+            return this;
+        }
+
+        ChangeBuilder setTask(RunningTaskInfo taskInfo) {
+            mChange.setTaskInfo(taskInfo);
+            return this;
+        }
+
+        ChangeBuilder setRotate(int anim) {
+            return setRotate(Surface.ROTATION_90, anim);
+        }
+
+        ChangeBuilder setRotate() {
+            return setRotate(ROTATION_ANIMATION_UNSPECIFIED);
+        }
+
+        ChangeBuilder setRotate(@Surface.Rotation int target, int anim) {
+            mChange.setRotation(Surface.ROTATION_0, target);
+            mChange.setRotationAnimation(anim);
+            return this;
+        }
+
+        TransitionInfo.Change build() {
+            return mChange;
+        }
+    }
+
     class TestTransitionHandler implements Transitions.TransitionHandler {
         ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>();
         final ArrayList<IBinder> mMerged = new ArrayList<>();
@@ -477,7 +673,8 @@
 
         @Override
         public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction t,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction,
                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
             mFinishes.add(finishCallback);
             return true;
@@ -540,4 +737,34 @@
         return taskInfo;
     }
 
+    private DisplayController createTestDisplayController() {
+        IWindowManager mockWM = mock(IWindowManager.class);
+        final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1];
+        try {
+            doReturn(new int[] {DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any());
+        } catch (RemoteException e) {
+            // No remote stuff happening, so this can't be hit
+        }
+        DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor);
+        out.initialize();
+        return out;
+    }
+
+    private Transitions createTestTransitions() {
+        return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(),
+                mContext, mMainExecutor, mAnimExecutor);
+    }
+//
+//    private class TestDisplayController extends DisplayController {
+//        private final DisplayLayout mTestDisplayLayout;
+//        TestDisplayController() {
+//            super(mContext, mock(IWindowManager.class), mMainExecutor);
+//            mTestDisplayLayout = new DisplayLayout();
+//            mTestDisplayLayout.
+//        }
+//
+//        @Override
+//        DisplayLayout
+//    }
+
 }
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 2c299fa..2b31bcf7 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -570,6 +570,7 @@
                 "renderthread/DrawFrameTask.cpp",
                 "renderthread/EglManager.cpp",
                 "renderthread/ReliableSurface.cpp",
+                "renderthread/RenderEffectCapabilityQuery.cpp",
                 "renderthread/VulkanManager.cpp",
                 "renderthread/VulkanSurface.cpp",
                 "renderthread/RenderProxy.cpp",
@@ -696,6 +697,7 @@
         "tests/unit/MatrixTests.cpp",
         "tests/unit/OpBufferTests.cpp",
         "tests/unit/PathInterpolatorTests.cpp",
+        "tests/unit/RenderEffectCapabilityQueryTests.cpp",
         "tests/unit/RenderNodeDrawableTests.cpp",
         "tests/unit/RenderNodeTests.cpp",
         "tests/unit/RenderPropertiesTests.cpp",
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 3544987..475fd70 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -50,7 +50,8 @@
 bool Properties::skipEmptyFrames = true;
 bool Properties::useBufferAge = true;
 bool Properties::enablePartialUpdates = true;
-bool Properties::enableRenderEffectCache = false;
+// Default true unless otherwise specified in RenderThread Configuration
+bool Properties::enableRenderEffectCache = true;
 
 DebugLevel Properties::debugLevel = kDebugDisabled;
 OverdrawColorSet Properties::overdrawColorSet = OverdrawColorSet::Default;
diff --git a/libs/hwui/apex/android_matrix.cpp b/libs/hwui/apex/android_matrix.cpp
index 693b22b..04ac3cf 100644
--- a/libs/hwui/apex/android_matrix.cpp
+++ b/libs/hwui/apex/android_matrix.cpp
@@ -35,3 +35,10 @@
     }
     return false;
 }
+
+jobject AMatrix_newInstance(JNIEnv* env, float values[9]) {
+    jobject matrixObj = android::android_graphics_Matrix_newInstance(env);
+    SkMatrix* m = android::android_graphics_Matrix_getSkMatrix(env, matrixObj);
+    m->set9(values);
+    return matrixObj;
+}
diff --git a/libs/hwui/apex/include/android/graphics/matrix.h b/libs/hwui/apex/include/android/graphics/matrix.h
index 987ad13..5705ba4 100644
--- a/libs/hwui/apex/include/android/graphics/matrix.h
+++ b/libs/hwui/apex/include/android/graphics/matrix.h
@@ -34,6 +34,16 @@
  */
 ANDROID_API bool AMatrix_getContents(JNIEnv* env, jobject matrixObj, float values[9]);
 
+/**
+ * Returns a new Matrix jobject that contains the values passed in as initial values.
+ * @param values The 9 values of the 3x3 matrix in the following order.
+ *               values[0] = scaleX  values[1] = skewX   values[2] = transX
+ *               values[3] = skewY   values[4] = scaleY  values[5] = transY
+ *               values[6] = persp0  values[7] = persp1  values[8] = persp2
+ * @return The matrix jobject
+ */
+ANDROID_API jobject AMatrix_newInstance(JNIEnv* env, float values[9]);
+
 __END_DECLS
 
 #endif // ANDROID_GRAPHICS_MATRIX_H
diff --git a/libs/hwui/jni/android_graphics_Matrix.cpp b/libs/hwui/jni/android_graphics_Matrix.cpp
index 7338ef2..cf6702e 100644
--- a/libs/hwui/jni/android_graphics_Matrix.cpp
+++ b/libs/hwui/jni/android_graphics_Matrix.cpp
@@ -378,13 +378,17 @@
     {"nEquals", "(JJ)Z", (void*) SkMatrixGlue::equals}
 };
 
+static jclass sClazz;
 static jfieldID sNativeInstanceField;
+static jmethodID sCtor;
 
 int register_android_graphics_Matrix(JNIEnv* env) {
     int result = RegisterMethodsOrDie(env, "android/graphics/Matrix", methods, NELEM(methods));
 
     jclass clazz = FindClassOrDie(env, "android/graphics/Matrix");
+    sClazz = MakeGlobalRefOrDie(env, clazz);
     sNativeInstanceField = GetFieldIDOrDie(env, clazz, "native_instance", "J");
+    sCtor = GetMethodIDOrDie(env, clazz, "<init>", "()V");
 
     return result;
 }
@@ -393,4 +397,7 @@
     return reinterpret_cast<SkMatrix*>(env->GetLongField(matrixObj, sNativeInstanceField));
 }
 
+jobject android_graphics_Matrix_newInstance(JNIEnv* env) {
+    return env->NewObject(sClazz, sCtor);
+}
 }
diff --git a/libs/hwui/jni/android_graphics_Matrix.h b/libs/hwui/jni/android_graphics_Matrix.h
index fe90d2e..79de48b 100644
--- a/libs/hwui/jni/android_graphics_Matrix.h
+++ b/libs/hwui/jni/android_graphics_Matrix.h
@@ -25,6 +25,9 @@
 /* Gets the underlying SkMatrix from a Matrix object. */
 SkMatrix* android_graphics_Matrix_getSkMatrix(JNIEnv* env, jobject matrixObj);
 
+/* Creates a new Matrix java object. */
+jobject android_graphics_Matrix_newInstance(JNIEnv* env);
+
 } // namespace android
 
 #endif // _ANDROID_GRAPHICS_MATRIX_H_
diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt
index 73de0d1..77b8a44 100644
--- a/libs/hwui/libhwui.map.txt
+++ b/libs/hwui/libhwui.map.txt
@@ -28,6 +28,7 @@
     register_android_graphics_GraphicsStatsService;
     zygote_preload_graphics;
     AMatrix_getContents;
+    AMatrix_newInstance;
     APaint_createPaint;
     APaint_destroyPaint;
     APaint_setBlendMode;
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 383c79b..c7d7a17 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -28,6 +28,7 @@
 
 #include "Frame.h"
 #include "Properties.h"
+#include "RenderEffectCapabilityQuery.h"
 #include "utils/Color.h"
 #include "utils/StringUtils.h"
 
@@ -148,7 +149,11 @@
     mHasWideColorGamutSupport = EglExtensions.glColorSpace && hasWideColorSpaceExtension;
 
     auto* vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
-    Properties::enableRenderEffectCache = (strcmp(vendor, "Qualcomm") != 0);
+    auto* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
+    Properties::enableRenderEffectCache = supportsRenderEffectCache(
+        vendor, version);
+    ALOGV("RenderEffectCache supported %d on driver version %s",
+          Properties::enableRenderEffectCache, version);
 }
 
 EGLConfig EglManager::load8BitsConfig(EGLDisplay display, EglManager::SwapBehavior swapBehavior) {
diff --git a/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp b/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp
new file mode 100644
index 0000000..a003988
--- /dev/null
+++ b/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <stdio.h>
+#include <string.h>
+#include <utils/Log.h>
+
+bool supportsRenderEffectCache(const char* vendor, const char* version) {
+    if (strcmp(vendor, "Qualcomm") != 0) {
+       return true;
+    }
+
+    int major;
+    int minor;
+    int driverMajor;
+    int driverMinor;
+    int n = sscanf(version,"OpenGL ES %d.%d V@%d.%d",
+                           &major,
+                           &minor,
+                           &driverMajor,
+                           &driverMinor);
+    // Ensure we have parsed the vendor string properly and we have either
+    // a newer major driver version, or the minor version is rev'ed
+    // Based on b/198227600#comment5 it appears that the corresponding fix
+    // is in driver version 571.0
+    return n == 4 && driverMajor >= 571;
+}
\ No newline at end of file
diff --git a/libs/hwui/renderthread/RenderEffectCapabilityQuery.h b/libs/hwui/renderthread/RenderEffectCapabilityQuery.h
new file mode 100644
index 0000000..ea673dd
--- /dev/null
+++ b/libs/hwui/renderthread/RenderEffectCapabilityQuery.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+/**
+ * Verify if the provided vendor and version supports RenderEffect caching
+ * behavior.
+ *
+ * Certain Open GL Driver implementations run into blocking scenarios
+ * with Fence::waitForever without a corresponding signal to unblock
+ * This happens during attempts to cache SkImage instances across frames
+ * especially in circumstances using RenderEffect/SkImageFilter internally.
+ * So detect the corresponding GL Vendor and driver version to determine if
+ * caching SkImage instances across frames is supported.
+ * See b/197263715 & b/193145089
+ * @param vendor Vendor of the GL driver
+ * @param version Version of the GL driver from the given vendor
+ * @return True if a RenderEffect result can be cached across frames,
+ * false otherwise
+ */
+bool supportsRenderEffectCache(const char* vendor, const char* version);
diff --git a/libs/hwui/tests/unit/EglManagerTests.cpp b/libs/hwui/tests/unit/EglManagerTests.cpp
index f7f2406..7f2e158 100644
--- a/libs/hwui/tests/unit/EglManagerTests.cpp
+++ b/libs/hwui/tests/unit/EglManagerTests.cpp
@@ -17,6 +17,7 @@
 #include <gtest/gtest.h>
 
 #include "renderthread/EglManager.h"
+#include "renderthread/RenderEffectCapabilityQuery.h"
 #include "tests/common/TestContext.h"
 
 using namespace android;
@@ -41,4 +42,17 @@
     }
 
     eglManager.destroy();
+}
+
+TEST(EglManager, verifyRenderEffectCacheSupported) {
+    EglManager eglManager;
+    eglManager.initialize();
+    auto* vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
+    auto* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
+    // Make sure that EglManager initializes Properties::enableRenderEffectCache
+    // based on the given gl vendor and version within EglManager->initialize()
+    bool renderEffectCacheSupported = supportsRenderEffectCache(vendor, version);
+    EXPECT_EQ(renderEffectCacheSupported,
+              Properties::enableRenderEffectCache);
+    eglManager.destroy();
 }
\ No newline at end of file
diff --git a/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp b/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp
new file mode 100644
index 0000000..0ee6549
--- /dev/null
+++ b/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+#include "renderthread/RenderEffectCapabilityQuery.h"
+#include "tests/common/TestContext.h"
+
+TEST(RenderEffectCapabilityQuery, testSupportedVendor) {
+  ASSERT_TRUE(supportsRenderEffectCache("Google", "OpenGL ES 1.4 V@0.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testSupportedVendorWithDifferentVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Google", "OpenGL ES 1.3 V@571.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithSupportedVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithSupportedPatchVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.1"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithNewerThanSupportedMajorVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@572.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithNewerThanSupportedMinorVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.2"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithUnsupportedMajorVersion) {
+  ASSERT_FALSE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.0 V@570.1"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithUnsupportedVersion) {
+  ASSERT_FALSE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.1 V@570.0"));
+}
+
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index acd8bce..d10e688 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -153,8 +153,7 @@
                     || update.state.surfaceHeight < desiredHeight) {
                 needApplyTransaction = true;
 
-                t.setSize(update.state.surfaceControl,
-                        desiredWidth, desiredHeight);
+                update.state.surfaceControl->updateDefaultBufferSize(desiredWidth, desiredHeight);
                 update.state.surfaceWidth = desiredWidth;
                 update.state.surfaceHeight = desiredHeight;
                 update.state.surfaceDrawn = false;
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index f90ebc2..b27a00c 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -451,13 +451,26 @@
      */
     public static final int FLAG_CAPTURE_PRIVATE = 0x1 << 13;
 
+    /**
+     * @hide
+     * Flag indicating the audio content has been processed to provide a virtual multichannel
+     * audio experience
+     */
+    public static final int FLAG_CONTENT_SPATIALIZED = 0x1 << 14;
+
+    /**
+     * @hide
+     * Flag indicating the audio content is to never be spatialized
+     */
+    public static final int FLAG_NEVER_SPATIALIZE = 0x1 << 15;
 
     // Note that even though FLAG_MUTE_HAPTIC is stored as a flag bit, it is not here since
     // it is known as a boolean value outside of AudioAttributes.
     private static final int FLAG_ALL = FLAG_AUDIBILITY_ENFORCED | FLAG_SECURE | FLAG_SCO
             | FLAG_BEACON | FLAG_HW_AV_SYNC | FLAG_HW_HOTWORD | FLAG_BYPASS_INTERRUPTION_POLICY
             | FLAG_BYPASS_MUTE | FLAG_LOW_LATENCY | FLAG_DEEP_BUFFER | FLAG_NO_MEDIA_PROJECTION
-            | FLAG_NO_SYSTEM_CAPTURE | FLAG_CAPTURE_PRIVATE;
+            | FLAG_NO_SYSTEM_CAPTURE | FLAG_CAPTURE_PRIVATE | FLAG_CONTENT_SPATIALIZED
+            | FLAG_NEVER_SPATIALIZE;
     private final static int FLAG_ALL_PUBLIC = FLAG_AUDIBILITY_ENFORCED |
             FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY;
     /* mask of flags that can be set by SDK and System APIs through the Builder */
@@ -619,6 +632,49 @@
     }
 
     /**
+     * Return true if the audio content associated with these attributes has already been
+     * spatialized, that is it has already been processed to offer a binaural or transaural
+     * immersive audio experience.
+     * @return {@code true} if the content has been processed
+     */
+    public boolean isContentSpatialized() {
+        return (mFlags & FLAG_CONTENT_SPATIALIZED) != 0;
+    }
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            SPATIALIZATION_BEHAVIOR_AUTO,
+            SPATIALIZATION_BEHAVIOR_NEVER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SpatializationBehavior {};
+
+    /**
+     * Constant indicating the audio content associated with these attributes will follow the
+     * default platform behavior with regards to which content will be spatialized or not.
+     * @see #getSpatializationBehavior()
+     * @see Spatializer
+     */
+    public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0;
+
+    /**
+     * Constant indicating the audio content associated with these attributes should never
+     * be virtualized.
+     * @see #getSpatializationBehavior()
+     * @see Spatializer
+     */
+    public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1;
+
+    /**
+     * Return the behavior affecting whether spatialization will be used.
+     * @return the spatialization behavior
+     */
+    public @SpatializationBehavior int getSpatializationBehavior() {
+        return ((mFlags & FLAG_NEVER_SPATIALIZE) != 0)
+                ? SPATIALIZATION_BEHAVIOR_NEVER : SPATIALIZATION_BEHAVIOR_AUTO;
+    }
+
+    /**
      * Return the capture policy.
      * @return the capture policy set by {@link Builder#setAllowedCapturePolicy(int)} or
      *         the default if it was not called.
@@ -661,6 +717,8 @@
         private int mSource = MediaRecorder.AudioSource.AUDIO_SOURCE_INVALID;
         private int mFlags = 0x0;
         private boolean mMuteHapticChannels = true;
+        private boolean mIsContentSpatialized = false;
+        private int mSpatializationBehavior = SPATIALIZATION_BEHAVIOR_AUTO;
         private HashSet<String> mTags = new HashSet<String>();
         private Bundle mBundle;
         private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT;
@@ -691,6 +749,8 @@
             mFlags = aa.getAllFlags();
             mTags = (HashSet<String>) aa.mTags.clone();
             mMuteHapticChannels = aa.areHapticChannelsMuted();
+            mIsContentSpatialized = aa.isContentSpatialized();
+            mSpatializationBehavior = aa.getSpatializationBehavior();
         }
 
         /**
@@ -723,6 +783,12 @@
             if (mMuteHapticChannels) {
                 aa.mFlags |= FLAG_MUTE_HAPTIC;
             }
+            if (mIsContentSpatialized) {
+                aa.mFlags |= FLAG_CONTENT_SPATIALIZED;
+            }
+            if (mSpatializationBehavior == SPATIALIZATION_BEHAVIOR_NEVER) {
+                aa.mFlags |= FLAG_NEVER_SPATIALIZE;
+            }
 
             if (mPrivacySensitive == PRIVACY_SENSITIVE_DEFAULT) {
                 // capturing for camcorder or communication is private by default to
@@ -910,6 +976,35 @@
         }
 
         /**
+         * Specifies whether the content has already been processed for spatialization.
+         * If it has, setting this to true will prevent issues such as double-processing.
+         * @param isSpatialized
+         * @return the same Builder instance
+         */
+        public @NonNull Builder setIsContentSpatialized(boolean isSpatialized) {
+            mIsContentSpatialized = isSpatialized;
+            return this;
+        }
+
+        /**
+         * Sets the behavior affecting whether spatialization will be used.
+         * @param sb the spatialization behavior
+         * @return the same Builder instance
+         *
+         */
+        public @NonNull Builder setSpatializationBehavior(@SpatializationBehavior int sb) {
+            switch (sb) {
+                case SPATIALIZATION_BEHAVIOR_NEVER:
+                case SPATIALIZATION_BEHAVIOR_AUTO:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid spatialization behavior " + sb);
+            }
+            mSpatializationBehavior = sb;
+            return this;
+        }
+
+        /**
          * @hide
          * Replaces flags.
          * @param flags any combination of {@link AudioAttributes#FLAG_ALL}.
@@ -994,6 +1089,8 @@
                     mContentType = attributes.mContentType;
                     mFlags = attributes.getAllFlags();
                     mMuteHapticChannels = attributes.areHapticChannelsMuted();
+                    mIsContentSpatialized = attributes.isContentSpatialized();
+                    mSpatializationBehavior = attributes.getSpatializationBehavior();
                     mTags = attributes.mTags;
                     mBundle = attributes.mBundle;
                     mSource = attributes.mSource;
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java
index 1644ec8..a8199c4 100644
--- a/media/java/android/media/AudioFormat.java
+++ b/media/java/android/media/AudioFormat.java
@@ -175,6 +175,28 @@
  * <br>These masks are an ORed composite of individual channel masks. For example
  * {@link #CHANNEL_OUT_STEREO} is composed of {@link #CHANNEL_OUT_FRONT_LEFT} and
  * {@link #CHANNEL_OUT_FRONT_RIGHT}.
+ * <p>
+ * The following diagram represents the layout of the output channels, as seen from above
+ * the listener (in the center at the "lis" position, facing the front-center channel).
+ * <pre>
+ *       TFL ----- TFC ----- TFR     T is Top
+ *       |  \       |       /  |
+ *       |   FL --- FC --- FR  |     F is Front
+ *       |   |\     |     /|   |
+ *       |   | BFL-BFC-BFR |   |     BF is Bottom Front
+ *       |   |             |   |
+ *       |   FWL   lis   FWR   |     W is Wide
+ *       |   |             |   |
+ *      TSL  SL    TC     SR  TSR    S is Side
+ *       |   |             |   |
+ *       |   BL --- BC -- BR   |     B is Back
+ *       |  /               \  |
+ *       TBL ----- TBC ----- TBR     C is Center, L/R is Left/Right
+ * </pre>
+ * All "T" (top) channels are above the listener, all "BF" (bottom-front) channels are below the
+ * listener, all others are in the listener's horizontal plane. When used in conjunction, LFE1 and
+ * LFE2 are below the listener, when used alone, LFE plane is undefined.
+ * See the channel definitions for the abbreviations
  *
  * <h5 id="channelIndexMask">Channel index masks</h5>
  * Channel index masks are introduced in API {@link android.os.Build.VERSION_CODES#M}. They allow
@@ -417,43 +439,62 @@
 
     // Output channel mask definitions below are translated to the native values defined in
     //  in /system/media/audio/include/system/audio.h in the JNI code of AudioTrack
+    /** Front left output channel (see FL in channel diagram) */
     public static final int CHANNEL_OUT_FRONT_LEFT = 0x4;
+    /** Front right output channel (see FR in channel diagram) */
     public static final int CHANNEL_OUT_FRONT_RIGHT = 0x8;
+    /** Front center output channel (see FC in channel diagram) */
     public static final int CHANNEL_OUT_FRONT_CENTER = 0x10;
+    /** LFE "low frequency effect" channel
+     * When used in conjunction with {@link #CHANNEL_OUT_LOW_FREQUENCY_2}, it is intended
+     * to contain the left low-frequency effect signal, also referred to as "LFE1"
+     * in ITU-R BS.2159-8 */
     public static final int CHANNEL_OUT_LOW_FREQUENCY = 0x20;
+    /** Back left output channel (see BL in channel diagram) */
     public static final int CHANNEL_OUT_BACK_LEFT = 0x40;
+    /** Back right output channel (see BR in channel diagram) */
     public static final int CHANNEL_OUT_BACK_RIGHT = 0x80;
     public static final int CHANNEL_OUT_FRONT_LEFT_OF_CENTER = 0x100;
     public static final int CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 0x200;
+    /** Back center output channel (see BC in channel diagram) */
     public static final int CHANNEL_OUT_BACK_CENTER = 0x400;
+    /** Side left output channel (see SL in channel diagram) */
     public static final int CHANNEL_OUT_SIDE_LEFT =         0x800;
+    /** Side right output channel (see SR in channel diagram) */
     public static final int CHANNEL_OUT_SIDE_RIGHT =       0x1000;
-    /** @hide */
+    /** Top center (above listener) output channel (see TC in channel diagram) */
     public static final int CHANNEL_OUT_TOP_CENTER =       0x2000;
-    /** @hide */
+    /** Top front left output channel (see TFL in channel diagram above FL) */
     public static final int CHANNEL_OUT_TOP_FRONT_LEFT =   0x4000;
-    /** @hide */
+    /** Top front center output channel (see TFC in channel diagram above FC) */
     public static final int CHANNEL_OUT_TOP_FRONT_CENTER = 0x8000;
-    /** @hide */
+    /** Top front right output channel (see TFR in channel diagram above FR) */
     public static final int CHANNEL_OUT_TOP_FRONT_RIGHT = 0x10000;
-    /** @hide */
+    /** Top back left output channel (see TBL in channel diagram above BL) */
     public static final int CHANNEL_OUT_TOP_BACK_LEFT =   0x20000;
-    /** @hide */
+    /** Top back center output channel (see TBC in channel diagram above BC) */
     public static final int CHANNEL_OUT_TOP_BACK_CENTER = 0x40000;
-    /** @hide */
+    /** Top back right output channel (see TBR in channel diagram above BR) */
     public static final int CHANNEL_OUT_TOP_BACK_RIGHT =  0x80000;
-    /** @hide */
+    /** Top side left output channel (see TSL in channel diagram above SL) */
     public static final int CHANNEL_OUT_TOP_SIDE_LEFT = 0x100000;
-    /** @hide */
+    /** Top side right output channel (see TSR in channel diagram above SR) */
     public static final int CHANNEL_OUT_TOP_SIDE_RIGHT = 0x200000;
-    /** @hide */
+    /** Bottom front left output channel (see BFL in channel diagram below FL) */
     public static final int CHANNEL_OUT_BOTTOM_FRONT_LEFT = 0x400000;
-    /** @hide */
+    /** Bottom front center output channel (see BFC in channel diagram below FC) */
     public static final int CHANNEL_OUT_BOTTOM_FRONT_CENTER = 0x800000;
-    /** @hide */
+    /** Bottom front right output channel (see BFR in channel diagram below FR) */
     public static final int CHANNEL_OUT_BOTTOM_FRONT_RIGHT = 0x1000000;
-    /** @hide */
+    /** The second LFE channel
+     * When used in conjunction with {@link #CHANNEL_OUT_LOW_FREQUENCY}, it is intended
+     * to contain the right low-frequency effect signal, also referred to as "LFE2"
+     * in ITU-R BS.2159-8 */
     public static final int CHANNEL_OUT_LOW_FREQUENCY_2 = 0x2000000;
+    /** Front wide left output channel (see FWL in channel diagram) */
+    public static final int CHANNEL_OUT_FRONT_WIDE_LEFT = 0x4000000;
+    /** Front wide right output channel (see FWR in channel diagram) */
+    public static final int CHANNEL_OUT_FRONT_WIDE_RIGHT = 0x8000000;
 
     public static final int CHANNEL_OUT_MONO = CHANNEL_OUT_FRONT_LEFT;
     public static final int CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT);
@@ -466,6 +507,7 @@
     public static final int CHANNEL_OUT_SURROUND = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT |
             CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_BACK_CENTER);
     // aka 5POINT1_BACK
+    /** Output channel mask for 5.1 */
     public static final int CHANNEL_OUT_5POINT1 = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT |
             CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT);
     /** @hide */
@@ -477,26 +519,39 @@
     @Deprecated    public static final int CHANNEL_OUT_7POINT1 = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT |
             CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT |
             CHANNEL_OUT_FRONT_LEFT_OF_CENTER | CHANNEL_OUT_FRONT_RIGHT_OF_CENTER);
+    /** Output channel mask for 7.1 */
     // matches AUDIO_CHANNEL_OUT_7POINT1
     public static final int CHANNEL_OUT_7POINT1_SURROUND = (
             CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_FRONT_RIGHT |
             CHANNEL_OUT_SIDE_LEFT | CHANNEL_OUT_SIDE_RIGHT |
             CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT |
             CHANNEL_OUT_LOW_FREQUENCY);
-    /** @hide */
+    /** Output channel mask for 5.1.2
+     *  Same as 5.1 with the addition of left and right top channels */
     public static final int CHANNEL_OUT_5POINT1POINT2 = (CHANNEL_OUT_5POINT1 |
             CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT);
-    /** @hide */
+    /** Output channel mask for 5.1.4
+     * Same as 5.1 with the addition of four top channels */
     public static final int CHANNEL_OUT_5POINT1POINT4 = (CHANNEL_OUT_5POINT1 |
             CHANNEL_OUT_TOP_FRONT_LEFT | CHANNEL_OUT_TOP_FRONT_RIGHT |
             CHANNEL_OUT_TOP_BACK_LEFT | CHANNEL_OUT_TOP_BACK_RIGHT);
-    /** @hide */
+    /** Output channel mask for 7.1.2
+     * Same as 7.1 with the addition of left and right top channels*/
     public static final int CHANNEL_OUT_7POINT1POINT2 = (CHANNEL_OUT_7POINT1_SURROUND |
             CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT);
-    /** @hide */
+    /** Output channel mask for 7.1.4
+     *  Same as 7.1 with the addition of four top channels */
     public static final int CHANNEL_OUT_7POINT1POINT4 = (CHANNEL_OUT_7POINT1_SURROUND |
             CHANNEL_OUT_TOP_FRONT_LEFT | CHANNEL_OUT_TOP_FRONT_RIGHT |
             CHANNEL_OUT_TOP_BACK_LEFT | CHANNEL_OUT_TOP_BACK_RIGHT);
+    /** Output channel mask for 9.1.4
+     * Same as 7.1.4 with the addition of left and right front wide channels */
+    public static final int CHANNEL_OUT_9POINT1POINT4 = (CHANNEL_OUT_7POINT1POINT4
+            | CHANNEL_OUT_FRONT_WIDE_LEFT | CHANNEL_OUT_FRONT_WIDE_RIGHT);
+    /** Output channel mask for 9.1.6
+     * Same as 9.1.4 with the addition of left and right top side channels */
+    public static final int CHANNEL_OUT_9POINT1POINT6 = (CHANNEL_OUT_9POINT1POINT4
+            | CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT);
     /** @hide */
     public static final int CHANNEL_OUT_13POINT_360RA = (
             CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_FRONT_RIGHT |
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index c068963..d06d37a 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -805,7 +805,7 @@
     }
 
     @UnsupportedAppUsage
-    private static IAudioService getService()
+    static IAudioService getService()
     {
         if (sService != null) {
             return sService;
@@ -2439,6 +2439,19 @@
     }
 
     //====================================================================
+    // Immersive audio
+
+    /**
+     * Return a handle to the optional platform's {@link Spatializer}
+     * @return the {@code Spatializer} instance.
+     * @see Spatializer#getImmersiveAudioLevel() to check for the level of support of the effect
+     *   on the platform
+     */
+    public @NonNull Spatializer getSpatializer() {
+        return new Spatializer(this);
+    }
+
+    //====================================================================
     // Bluetooth SCO control
     /**
      * Sticky broadcast intent action indicating that the Bluetooth SCO audio
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 69d1889..5d9f290 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.TestApi;
 import android.bluetooth.BluetoothCodecConfig;
@@ -2000,6 +2001,46 @@
      */
     public static native int setVibratorInfos(@NonNull List<Vibrator> vibrators);
 
+    /**
+     * @hide
+     * If a spatializer effect is present on the platform, this will return an
+     * ISpatializer interface to control this feature.
+     * If no spatializer is present, a null interface is returned.
+     * The INativeSpatializerCallback passed must not be null.
+     * Only one ISpatializer interface can exist at a given time. The native audio policy
+     * service will reject the request if an interface was already acquired and previous owner
+     * did not die or call ISpatializer.release().
+     * @param callback the callback to receive state updates if the ISpatializer
+     *        interface is acquired.
+     * @return the ISpatializer interface made available to control the
+     *        platform spatializer
+     */
+    @Nullable
+    public static ISpatializer getSpatializer(INativeSpatializerCallback callback) {
+        return ISpatializer.Stub.asInterface(nativeGetSpatializer(callback));
+    }
+    private static native IBinder nativeGetSpatializer(INativeSpatializerCallback callback);
+
+    /**
+     * @hide
+     * Queries if some kind of spatialization will be performed if the audio playback context
+     * described by the provided arguments is present.
+     * The context is made of:
+     * - The audio attributes describing the playback use case.
+     * - The audio configuration describing the audio format, channels, sampling rate ...
+     * - The devices describing the sink audio device selected for playback.
+     * All arguments are optional and only the specified arguments are used to match against
+     * supported criteria. For instance, supplying no argument will tell if spatialization is
+     * supported or not in general.
+     * @param attributes audio attributes describing the playback use case
+     * @param format audio configuration describing the audio format, channels, sampling rate...
+     * @param devices the sink audio device selected for playback
+     * @return true if spatialization is enabled for this context, false otherwise.
+     */
+    public static native boolean canBeSpatialized(AudioAttributes attributes,
+                                              AudioFormat format,
+                                              AudioDeviceAttributes[] devices);
+
     // Items shared with audio service
 
     /**
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 23d9532..476a9a58 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1613,7 +1613,9 @@
             AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT |
             AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_CENTER |
             AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT |
-            AudioFormat.CHANNEL_OUT_LOW_FREQUENCY_2;
+            AudioFormat.CHANNEL_OUT_LOW_FREQUENCY_2 |
+            AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT |
+            AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT;
 
     // Returns a boolean whether the attributes, format, bufferSizeInBytes, mode allow
     // power saving to be automatically enabled for an AudioTrack. Returns false if
@@ -1787,6 +1789,8 @@
                 | AudioFormat.CHANNEL_OUT_TOP_SIDE_RIGHT);
         put("bottom front", AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT
                 | AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT);
+        put("front wide", AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT
+                | AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT);
     }};
 
     /**
@@ -1801,9 +1805,15 @@
             return false;
         }
         final int channelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig);
-        final int channelCountLimit = AudioFormat.isEncodingLinearFrames(encoding)
-                ? AudioSystem.OUT_CHANNEL_COUNT_MAX  // PCM limited to OUT_CHANNEL_COUNT_MAX
-                : AudioSystem.FCC_24;                // Compressed limited to 24 channels
+        final int channelCountLimit;
+        try {
+            channelCountLimit = AudioFormat.isEncodingLinearFrames(encoding)
+                    ? AudioSystem.OUT_CHANNEL_COUNT_MAX  // PCM limited to OUT_CHANNEL_COUNT_MAX
+                    : AudioSystem.FCC_24;                // Compressed limited to 24 channels
+        } catch (IllegalArgumentException iae) {
+            loge("Unsupported encoding " + iae);
+            return false;
+        }
         if (channelCount > channelCountLimit) {
             loge("Channel configuration contains too many channels for encoding "
                     + encoding + "(" + channelCount + " > " + channelCountLimit + ")");
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 7191280..bc6d2cd 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -20,6 +20,7 @@
 import android.content.ComponentName;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
+import android.media.AudioFormat;
 import android.media.AudioFocusInfo;
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioRecordingConfiguration;
@@ -34,6 +35,10 @@
 import android.media.IRecordingConfigDispatcher;
 import android.media.IRingtonePlayer;
 import android.media.IStrategyPreferredDevicesDispatcher;
+import android.media.ISpatializerCallback;
+import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.ISpatializerHeadToSoundStagePoseCallback;
+import android.media.ISpatializerOutputCallback;
 import android.media.IVolumeController;
 import android.media.IVolumeController;
 import android.media.PlayerBase;
@@ -397,4 +402,54 @@
     void registerModeDispatcher(IAudioModeDispatcher dispatcher);
 
     oneway void unregisterModeDispatcher(IAudioModeDispatcher dispatcher);
+
+    int getSpatializerImmersiveAudioLevel();
+
+    boolean isSpatializerEnabled();
+
+    boolean isSpatializerAvailable();
+
+    void setSpatializerEnabled(boolean enabled);
+
+    boolean canBeSpatialized(in AudioAttributes aa, in AudioFormat af);
+
+    void registerSpatializerCallback(in ISpatializerCallback cb);
+
+    void unregisterSpatializerCallback(in ISpatializerCallback cb);
+
+    void registerSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb);
+
+    void unregisterSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb);
+
+    void registerHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb);
+
+    void unregisterHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb);
+
+    List<AudioDeviceAttributes> getSpatializerCompatibleAudioDevices();
+
+    void addSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada);
+
+    void removeSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada);
+
+    void setDesiredHeadTrackingMode(int mode);
+
+    int getDesiredHeadTrackingMode();
+
+    int[] getSupportedHeadTrackingModes();
+
+    int getActualHeadTrackingMode();
+
+    oneway void setSpatializerGlobalTransform(in float[] transform);
+
+    oneway void recenterHeadTracker();
+
+    void setSpatializerParameter(int key, in byte[] value);
+
+    void getSpatializerParameter(int key, inout byte[] value);
+
+    int getSpatializerOutput();
+
+    void registerSpatializerOutputCallback(in ISpatializerOutputCallback cb);
+
+    void unregisterSpatializerOutputCallback(in ISpatializerOutputCallback cb);
 }
diff --git a/media/java/android/media/IMediaRouterClient.aidl b/media/java/android/media/IMediaRouterClient.aidl
index 9b49123..6b754e1 100644
--- a/media/java/android/media/IMediaRouterClient.aidl
+++ b/media/java/android/media/IMediaRouterClient.aidl
@@ -23,4 +23,5 @@
     void onStateChanged();
     void onRestoreRoute();
     void onGroupRouteSelected(String routeId);
+    void onGlobalA2dpChanged(boolean a2dpOn);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/media/java/android/media/ISpatializerCallback.aidl
similarity index 62%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to media/java/android/media/ISpatializerCallback.aidl
index b391bd9..50f91e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/media/java/android/media/ISpatializerCallback.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package android.media;
 
-import com.android.systemui.R;
+/**
+ * AIDL for the AudioService to signal Spatializer state changes.
+ *
+ * {@hide}
+ */
+oneway interface ISpatializerCallback {
 
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
+    void dispatchSpatializerEnabledChanged(boolean enabled);
+
+    void dispatchSpatializerAvailableChanged(boolean available);
 }
diff --git a/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl
new file mode 100644
index 0000000..01a1465
--- /dev/null
+++ b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+/**
+ * AIDL for the AudioService to signal Spatializer state changes.
+ *
+ * {@hide}
+ */
+oneway interface ISpatializerHeadToSoundStagePoseCallback {
+
+    /**
+     * The pose is sent as an array of 6 float values, the first 3 are the translation vector, the
+     * other 3 are the rotation vector.
+     */
+    void dispatchPoseChanged(in float[] pose);
+}
diff --git a/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl
new file mode 100644
index 0000000..c61f86e
--- /dev/null
+++ b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+/**
+ * AIDL for the AudioService to signal Spatializer head tracking mode changes.
+ *
+ * {@hide}
+ */
+oneway interface ISpatializerHeadTrackingModeCallback {
+
+    void dispatchSpatializerActualHeadTrackingModeChanged(int mode);
+
+    void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/media/java/android/media/ISpatializerOutputCallback.aidl
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to media/java/android/media/ISpatializerOutputCallback.aidl
index b391bd9..57572a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/media/java/android/media/ISpatializerOutputCallback.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package android.media;
 
-import com.android.systemui.R;
+/**
+ * AIDL for the AudioService to signal Spatializer output changes.
+ *
+ * {@hide}
+ */
+oneway interface ISpatializerOutputCallback {
 
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
+    void dispatchSpatializerOutputChanged(int output);
 }
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 9bf0db5..028ae2b3 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -683,6 +683,19 @@
     public static final String KEY_CHANNEL_MASK = "channel-mask";
 
     /**
+     * A key describing the maximum number of channels that can be output by an audio decoder.
+     * By default, the decoder will output the same number of channels as present in the encoded
+     * stream, if supported. Set this value to limit the number of output channels, and use
+     * the downmix information in the stream, if available.
+     * <p>Values larger than the number of channels in the content to decode behave like the number
+     * of channels in the content (if applicable), for instance passing 99 for a 5.1 audio stream
+     * behaves like passing 6.
+     * <p>This key is only used during decoding.
+     */
+    public static final String KEY_MAX_OUTPUT_CHANNEL_COUNT =
+            "max-output-channel_count";
+
+    /**
      * A key describing the number of frames to trim from the start of the decoded audio stream.
      * The associated value is an integer.
      */
diff --git a/media/java/android/media/MediaMetrics.java b/media/java/android/media/MediaMetrics.java
index 3a5216e..6eb1af8 100644
--- a/media/java/android/media/MediaMetrics.java
+++ b/media/java/android/media/MediaMetrics.java
@@ -53,6 +53,7 @@
         public static final String AUDIO_VOLUME = AUDIO + SEPARATOR + "volume";
         public static final String AUDIO_VOLUME_EVENT = AUDIO_VOLUME + SEPARATOR + "event";
         public static final String AUDIO_MODE = AUDIO + SEPARATOR + "mode";
+        public static final String METRICS_MANAGER = "metrics" + SEPARATOR + "manager";
     }
 
     /**
@@ -120,10 +121,11 @@
                 createKey("gainDb", Double.class);
         public static final Key<String> GROUP =
                 createKey("group", String.class);
-        // For volume
-        public static final Key<Integer> INDEX = createKey("index", Integer.class);
-        public static final Key<Integer> MAX_INDEX = createKey("maxIndex", Integer.class);
-        public static final Key<Integer> MIN_INDEX = createKey("minIndex", Integer.class);
+
+        public static final Key<Integer> INDEX = createKey("index", Integer.class); // volume
+        public static final Key<String> LOG_SESSION_ID = createKey("logSessionId", String.class);
+        public static final Key<Integer> MAX_INDEX = createKey("maxIndex", Integer.class); // vol
+        public static final Key<Integer> MIN_INDEX = createKey("minIndex", Integer.class); // vol
         public static final Key<String> MODE =
                 createKey("mode", String.class); // audio_mode
         public static final Key<String> MUTE =
diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java
index dfdfeac..748ae52 100644
--- a/media/java/android/media/MediaRouter.java
+++ b/media/java/android/media/MediaRouter.java
@@ -654,12 +654,9 @@
         final class Client extends IMediaRouterClient.Stub {
             @Override
             public void onStateChanged() {
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        if (Client.this == mClient) {
-                            updateClientState();
-                        }
+                mHandler.post(() -> {
+                    if (Client.this == mClient) {
+                        updateClientState();
                     }
                 });
             }
@@ -693,6 +690,26 @@
                     }
                 });
             }
+
+            // Called when the selection of a connected device (phone speaker or BT devices)
+            // is changed.
+            @Override
+            public void onGlobalA2dpChanged(boolean a2dpOn) {
+                mHandler.post(() -> {
+                    if (mSelectedRoute == null || mBluetoothA2dpRoute == null) {
+                        return;
+                    }
+                    if (mSelectedRoute.isDefault() && a2dpOn) {
+                        setSelectedRoute(mBluetoothA2dpRoute, /*explicit=*/ false);
+                        dispatchRouteUnselected(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo);
+                        dispatchRouteSelected(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute);
+                    } else if (mSelectedRoute.isBluetooth() && !a2dpOn) {
+                        setSelectedRoute(mDefaultAudioVideo, /*explicit=*/ false);
+                        dispatchRouteUnselected(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute);
+                        dispatchRouteSelected(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo);
+                    }
+                });
+            }
         }
     }
 
@@ -1351,6 +1368,9 @@
     }
 
     static void dispatchRouteSelected(int type, RouteInfo info) {
+        if (DEBUG) {
+            Log.d(TAG, "Dispatching route selected: " + info);
+        }
         for (CallbackInfo cbi : sStatic.mCallbacks) {
             if (cbi.filterRouteEvent(info)) {
                 cbi.cb.onRouteSelected(cbi.router, type, info);
@@ -1359,6 +1379,9 @@
     }
 
     static void dispatchRouteUnselected(int type, RouteInfo info) {
+        if (DEBUG) {
+            Log.d(TAG, "Dispatching route unselected: " + info);
+        }
         for (CallbackInfo cbi : sStatic.mCallbacks) {
             if (cbi.filterRouteEvent(info)) {
                 cbi.cb.onRouteUnselected(cbi.router, type, info);
diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java
new file mode 100644
index 0000000..e6fff39
--- /dev/null
+++ b/media/java/android/media/Spatializer.java
@@ -0,0 +1,1089 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Spatializer provides access to querying capabilities and behavior of sound spatialization
+ * on the device.
+ * Sound spatialization simulates sounds originating around the listener as if they were coming
+ * from virtual speakers placed around the listener.<br>
+ * Support for spatialization is optional, use {@link AudioManager#getSpatializer()} to obtain an
+ * instance of this class if the feature is supported.
+ *
+ */
+public class Spatializer {
+
+    private final @NonNull AudioManager mAm;
+
+    private static final String TAG = "Spatializer";
+
+    /**
+     * @hide
+     * Constructor with AudioManager acting as proxy to AudioService
+     * @param am a non-null AudioManager
+     */
+    protected Spatializer(@NonNull AudioManager am) {
+        mAm = Objects.requireNonNull(am);
+    }
+
+    /**
+     * Returns whether spatialization is enabled or not.
+     * A false value can originate for instance from the user electing to
+     * disable the feature, or when the feature is not supported on the device (indicated
+     * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}).
+     * <br>
+     * Note that this state reflects a platform-wide state of the "desire" to use spatialization,
+     * but availability of the audio processing is still dictated by the compatibility between
+     * the effect and the hardware configuration, as indicated by {@link #isAvailable()}.
+     * @return {@code true} if spatialization is enabled
+     * @see #isAvailable()
+     */
+    public boolean isEnabled() {
+        try {
+            return mAm.getService().isSpatializerEnabled();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error querying isSpatializerEnabled, returning false", e);
+            return false;
+        }
+    }
+
+    /**
+     * Returns whether spatialization is available.
+     * Reasons for spatialization being unavailable include situations where audio output is
+     * incompatible with sound spatialization, such as playback on a monophonic speaker.<br>
+     * Note that spatialization can be available, but disabled by the user, in which case this
+     * method would still return {@code true}, whereas {@link #isEnabled()}
+     * would return {@code false}.<br>
+     * Also when the feature is not supported on the device (indicated
+     * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}),
+     * the return value will be false.
+     * @return {@code true} if the spatializer effect is available and capable
+     *         of processing the audio for the current configuration of the device,
+     *         {@code false} otherwise.
+     * @see #isEnabled()
+     */
+    public boolean isAvailable()  {
+        try {
+            return mAm.getService().isSpatializerAvailable();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error querying isSpatializerAvailable, returning false", e);
+            return false;
+        }
+    }
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            SPATIALIZER_IMMERSIVE_LEVEL_OTHER,
+            SPATIALIZER_IMMERSIVE_LEVEL_NONE,
+            SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ImmersiveAudioLevel {};
+
+    /**
+     * Constant indicating the {@code Spatializer} on this device supports a spatialization
+     * mode that differs from the ones available at this SDK level.
+     * @see #getImmersiveAudioLevel()
+     */
+    public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1;
+
+    /**
+     * Constant indicating there are no spatialization capabilities supported on this device.
+     * @see #getImmersiveAudioLevel()
+     */
+    public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0;
+
+    /**
+     * Constant indicating the {@code Spatializer} on this device supports multichannel
+     * spatialization.
+     * @see #getImmersiveAudioLevel()
+     */
+    public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
+
+    /**
+     * @hide
+     * Constant indicating the {@code Spatializer} on this device supports the spatialization of
+     * multichannel bed plus objects.
+     * @see #getImmersiveAudioLevel()
+     */
+    public static final int SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS = 2;
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            HEAD_TRACKING_MODE_UNSUPPORTED,
+            HEAD_TRACKING_MODE_DISABLED,
+            HEAD_TRACKING_MODE_RELATIVE_WORLD,
+            HEAD_TRACKING_MODE_RELATIVE_DEVICE,
+    }) public @interface HeadTrackingMode {};
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            HEAD_TRACKING_MODE_DISABLED,
+            HEAD_TRACKING_MODE_RELATIVE_WORLD,
+            HEAD_TRACKING_MODE_RELATIVE_DEVICE,
+    }) public @interface HeadTrackingModeSet {};
+
+    /** @hide */
+    @IntDef(flag = false, value = {
+            HEAD_TRACKING_MODE_RELATIVE_WORLD,
+            HEAD_TRACKING_MODE_RELATIVE_DEVICE,
+    }) public @interface HeadTrackingModeSupported {};
+
+    /**
+     * @hide
+     * Constant indicating head tracking is not supported by this {@code Spatializer}
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is disabled on this {@code Spatializer}
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_DISABLED = -1;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is in a mode whose behavior is unknown. This is not an
+     * error state but represents a customized behavior not defined by this API.
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_OTHER = 0;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is tracking the user's position / orientation relative to
+     * the world around them
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1;
+
+    /**
+     * @hide
+     * Constant indicating head tracking is tracking the user's position / orientation relative to
+     * the device
+     * @see #getHeadTrackingMode()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2;
+
+    /**
+     * Return the level of support for the spatialization feature on this device.
+     * This level of support is independent of whether the {@code Spatializer} is currently
+     * enabled or available and will not change over time.
+     * @return the level of spatialization support
+     * @see #isEnabled()
+     * @see #isAvailable()
+     */
+    public @ImmersiveAudioLevel int getImmersiveAudioLevel() {
+        int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+        try {
+            level = mAm.getService().getSpatializerImmersiveAudioLevel();
+        } catch (Exception e) { /* using NONE */ }
+        return level;
+    }
+
+    /**
+     * @hide
+     * Enables / disables the spatializer effect.
+     * Changing the enabled state will trigger the public
+     * {@link OnSpatializerStateChangedListener#onSpatializerEnabledChanged(Spatializer, boolean)}
+     * registered listeners.
+     * @param enabled {@code true} for enabling the effect
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setEnabled(boolean enabled) {
+        try {
+            mAm.getService().setSpatializerEnabled(enabled);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setSpatializerEnabled", e);
+        }
+    }
+
+    /**
+     * An interface to be notified of changes to the state of the spatializer effect.
+     */
+    public interface OnSpatializerStateChangedListener {
+        /**
+         * Called when the enabled state of the spatializer effect changes
+         * @param spat the {@code Spatializer} instance whose state changed
+         * @param enabled {@code true} if the spatializer effect is enabled on the device,
+         *                            {@code false} otherwise
+         * @see #isEnabled()
+         */
+        void onSpatializerEnabledChanged(@NonNull Spatializer spat, boolean enabled);
+
+        /**
+         * Called when the availability of the spatializer effect changes
+         * @param spat the {@code Spatializer} instance whose state changed
+         * @param available {@code true} if the spatializer effect is available and capable
+         *                  of processing the audio for the current configuration of the device,
+         *                  {@code false} otherwise.
+         * @see #isAvailable()
+         */
+        void onSpatializerAvailableChanged(@NonNull Spatializer spat, boolean available);
+    }
+
+    /**
+     * @hide
+     * An interface to be notified of changes to the head tracking mode, used by the spatializer
+     * effect.
+     * Changes to the mode may come from explicitly setting a different mode
+     * (see {@link #setDesiredHeadTrackingMode(int)}) or a change in system conditions (see
+     * {@link #getHeadTrackingMode()}
+     * @see #addOnHeadTrackingModeChangedListener(Executor, OnHeadTrackingModeChangedListener)
+     * @see #removeOnHeadTrackingModeChangedListener(OnHeadTrackingModeChangedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    public interface OnHeadTrackingModeChangedListener {
+        /**
+         * Called when the actual head tracking mode of the spatializer changed.
+         * @param spatializer the {@code Spatializer} instance whose head tracking mode is changing
+         * @param mode the new head tracking mode
+         */
+        void onHeadTrackingModeChanged(@NonNull Spatializer spatializer,
+                @HeadTrackingMode int mode);
+
+        /**
+         * Called when the desired head tracking mode of the spatializer changed
+         * @param spatializer the {@code Spatializer} instance whose head tracking mode was set
+         * @param mode the newly set head tracking mode
+         */
+        void onDesiredHeadTrackingModeChanged(@NonNull Spatializer spatializer,
+                @HeadTrackingModeSet int mode);
+    }
+
+
+    /**
+     * @hide
+     * An interface to be notified of changes to the output stream used by the spatializer
+     * effect.
+     * @see #getOutput()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    public interface OnSpatializerOutputChangedListener {
+        /**
+         * Called when the id of the output stream of the spatializer effect changed.
+         * @param spatializer the {@code Spatializer} instance whose output is updated
+         * @param output the id of the output stream, or 0 when there is no spatializer output
+         */
+        void onSpatializerOutputChanged(@NonNull Spatializer spatializer,
+                @IntRange(from = 0) int output);
+    }
+
+    /**
+     * @hide
+     * An interface to be notified of updates to the head to soundstage pose, as represented by the
+     * current head tracking mode.
+     * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    public interface OnHeadToSoundstagePoseUpdatedListener {
+        /**
+         * Called when the head to soundstage transform is updated
+         * @param spatializer the {@code Spatializer} instance affected by the pose update
+         * @param pose the new pose data representing the transform between the frame
+         *                 of reference for the current head tracking mode (see
+         *                 {@link #getHeadTrackingMode()}) and the device being tracked (for
+         *                 instance a pair of headphones with a head tracker).<br>
+         *                 The head pose data is represented as an array of six float values, where
+         *                 the first three values are the translation vector, and the next three
+         *                 are the rotation vector.
+         */
+        void onHeadToSoundstagePoseUpdated(@NonNull Spatializer spatializer,
+                @NonNull float[] pose);
+    }
+
+    /**
+     * Returns whether audio of the given {@link AudioFormat}, played with the given
+     * {@link AudioAttributes} can be spatialized.
+     * Note that the result reflects the capabilities of the device and may change when
+     * audio accessories are connected/disconnected (e.g. wired headphones plugged in or not).
+     * The result is independent from whether spatialization processing is enabled or not.
+     * @param attributes the {@code AudioAttributes} of the content as used for playback
+     * @param format the {@code AudioFormat} of the content as used for playback
+     * @return {@code true} if the device is capable of spatializing the combination of audio format
+     *     and attributes, {@code false} otherwise.
+     */
+    public boolean canBeSpatialized(
+            @NonNull AudioAttributes attributes, @NonNull AudioFormat format) {
+        try {
+            return mAm.getService().canBeSpatialized(
+                    Objects.requireNonNull(attributes), Objects.requireNonNull(format));
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error querying canBeSpatialized for attr:" + attributes
+                    + " format:" + format + " returning false", e);
+            return false;
+        }
+    }
+
+    /**
+     * Adds a listener to be notified of changes to the enabled state of the
+     * {@code Spatializer}.
+     * @param executor the {@code Executor} handling the callback
+     * @param listener the listener to receive enabled state updates
+     * @see #isEnabled()
+     */
+    public void addOnSpatializerStateChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnSpatializerStateChangedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mStateListenerLock) {
+            if (hasSpatializerStateListener(listener)) {
+                throw new IllegalArgumentException(
+                        "Called addOnSpatializerStateChangedListener() "
+                        + "on a previously registered listener");
+            }
+            // lazy initialization of the list of strategy-preferred device listener
+            if (mStateListeners == null) {
+                mStateListeners = new ArrayList<>();
+            }
+            mStateListeners.add(new StateListenerInfo(listener, executor));
+            if (mStateListeners.size() == 1) {
+                // register binder for callbacks
+                if (mInfoDispatcherStub == null) {
+                    mInfoDispatcherStub =
+                            new SpatializerInfoDispatcherStub();
+                }
+                try {
+                    mAm.getService().registerSpatializerCallback(
+                            mInfoDispatcherStub);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+        }
+    }
+
+    /**
+     * Removes a previously added listener for changes to the enabled state of the
+     * {@code Spatializer}.
+     * @param listener the listener to receive enabled state updates
+     * @see #isEnabled()
+     */
+    public void removeOnSpatializerStateChangedListener(
+            @NonNull OnSpatializerStateChangedListener listener) {
+        Objects.requireNonNull(listener);
+        synchronized (mStateListenerLock) {
+            if (!removeStateListener(listener)) {
+                throw new IllegalArgumentException(
+                        "Called removeOnSpatializerStateChangedListener() "
+                        + "on an unregistered listener");
+            }
+            if (mStateListeners.size() == 0) {
+                // unregister binder for callbacks
+                try {
+                    mAm.getService().unregisterSpatializerCallback(mInfoDispatcherStub);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                } finally {
+                    mInfoDispatcherStub = null;
+                    mStateListeners = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Returns the list of playback devices that are compatible with the playback of multichannel
+     * audio through virtualization
+     * @return a list of devices. An empty list indicates virtualization is not supported.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() {
+        try {
+            return mAm.getService().getSpatializerCompatibleAudioDevices();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error querying getSpatializerCompatibleAudioDevices(), "
+                    + " returning empty list", e);
+            return new ArrayList<AudioDeviceAttributes>(0);
+        }
+    }
+
+    /**
+     * @hide
+     * Adds a playback device to the list of devices compatible with the playback of multichannel
+     * audio through spatialization.
+     * @see #getCompatibleAudioDevices()
+     * @param ada the audio device compatible with spatialization
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        try {
+            mAm.getService().addSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada));
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling addSpatializerCompatibleAudioDevice(), ", e);
+        }
+    }
+
+    /**
+     * @hide
+     * Remove a playback device from the list of devices compatible with the playback of
+     * multichannel audio through spatialization.
+     * @see #getCompatibleAudioDevices()
+     * @param ada the audio device incompatible with spatialization
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        try {
+            mAm.getService().removeSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada));
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling removeSpatializerCompatibleAudioDevice(), ", e);
+        }
+    }
+
+    private final Object mStateListenerLock = new Object();
+    /**
+     * List of listeners for state listener and their associated Executor.
+     * List is lazy-initialized on first registration
+     */
+    @GuardedBy("mStateListenerLock")
+    private @Nullable ArrayList<StateListenerInfo> mStateListeners;
+
+    @GuardedBy("mStateListenerLock")
+    private @Nullable SpatializerInfoDispatcherStub mInfoDispatcherStub;
+
+    private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub {
+        @Override
+        public void dispatchSpatializerEnabledChanged(boolean enabled) {
+            // make a shallow copy of listeners so callback is not executed under lock
+            final ArrayList<StateListenerInfo> stateListeners;
+            synchronized (mStateListenerLock) {
+                if (mStateListeners == null || mStateListeners.size() == 0) {
+                    return;
+                }
+                stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone();
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                for (StateListenerInfo info : stateListeners) {
+                    info.mExecutor.execute(() ->
+                            info.mListener.onSpatializerEnabledChanged(Spatializer.this, enabled));
+                }
+            }
+        }
+
+        @Override
+        public void dispatchSpatializerAvailableChanged(boolean available) {
+            // make a shallow copy of listeners so callback is not executed under lock
+            final ArrayList<StateListenerInfo> stateListeners;
+            synchronized (mStateListenerLock) {
+                if (mStateListeners == null || mStateListeners.size() == 0) {
+                    return;
+                }
+                stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone();
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                for (StateListenerInfo info : stateListeners) {
+                    info.mExecutor.execute(() ->
+                            info.mListener.onSpatializerAvailableChanged(
+                                    Spatializer.this, available));
+                }
+            }
+        }
+    }
+
+    private static class StateListenerInfo {
+        final @NonNull OnSpatializerStateChangedListener mListener;
+        final @NonNull Executor mExecutor;
+
+        StateListenerInfo(@NonNull OnSpatializerStateChangedListener listener,
+                @NonNull Executor exe) {
+            mListener = listener;
+            mExecutor = exe;
+        }
+    }
+
+    @GuardedBy("mStateListenerLock")
+    private boolean hasSpatializerStateListener(OnSpatializerStateChangedListener listener) {
+        return getStateListenerInfo(listener) != null;
+    }
+
+    @GuardedBy("mStateListenerLock")
+    private @Nullable StateListenerInfo getStateListenerInfo(
+            OnSpatializerStateChangedListener listener) {
+        if (mStateListeners == null) {
+            return null;
+        }
+        for (StateListenerInfo info : mStateListeners) {
+            if (info.mListener == listener) {
+                return info;
+            }
+        }
+        return null;
+    }
+
+    @GuardedBy("mStateListenerLock")
+    /**
+     * @return true if the listener was removed from the list
+     */
+    private boolean removeStateListener(OnSpatializerStateChangedListener listener) {
+        final StateListenerInfo infoToRemove = getStateListenerInfo(listener);
+        if (infoToRemove != null) {
+            mStateListeners.remove(infoToRemove);
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * @hide
+     * Return the current head tracking mode as used by the system.
+     * Note this may differ from the desired head tracking mode. Reasons for the two to differ
+     * include: a head tracking device is not available for the current audio output device,
+     * the transmission conditions between the tracker and device have deteriorated and tracking
+     * has been disabled.
+     * @see #getDesiredHeadTrackingMode()
+     * @return the current head tracking mode
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @HeadTrackingMode int getHeadTrackingMode() {
+        try {
+            return mAm.getService().getActualHeadTrackingMode();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getActualHeadTrackingMode", e);
+            return HEAD_TRACKING_MODE_UNSUPPORTED;
+        }
+
+    }
+
+    /**
+     * @hide
+     * Return the desired head tracking mode.
+     * Note this may differ from the actual head tracking mode, reflected by
+     * {@link #getHeadTrackingMode()}.
+     * @return the desired head tring mode
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @HeadTrackingMode int getDesiredHeadTrackingMode() {
+        try {
+            return mAm.getService().getDesiredHeadTrackingMode();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getDesiredHeadTrackingMode", e);
+            return HEAD_TRACKING_MODE_UNSUPPORTED;
+        }
+    }
+
+    /**
+     * @hide
+     * Returns the list of supported head tracking modes.
+     * @return the list of modes that can be used in {@link #setDesiredHeadTrackingMode(int)} to
+     *         enable head tracking. The list will be empty if {@link #getHeadTrackingMode()}
+     *         is {@link #HEAD_TRACKING_MODE_UNSUPPORTED}. Values can be
+     *         {@link #HEAD_TRACKING_MODE_OTHER},
+     *         {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD} or
+     *         {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE}
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @NonNull List<Integer> getSupportedHeadTrackingModes() {
+        try {
+            final int[] modes = mAm.getService().getSupportedHeadTrackingModes();
+            final ArrayList<Integer> list = new ArrayList<>(0);
+            for (int mode : modes) {
+                list.add(mode);
+            }
+            return list;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getSupportedHeadTrackModes", e);
+            return new ArrayList(0);
+        }
+    }
+
+    /**
+     * @hide
+     * Sets the desired head tracking mode.
+     * Note a set desired mode may differ from the actual head tracking mode.
+     * @see #getHeadTrackingMode()
+     * @param mode the desired head tracking mode, one of the values returned by
+     *             {@link #getSupportedHeadTrackModes()}, or {@link #HEAD_TRACKING_MODE_DISABLED} to
+     *             disable head tracking.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setDesiredHeadTrackingMode(@HeadTrackingModeSet int mode) {
+        try {
+            mAm.getService().setDesiredHeadTrackingMode(mode);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setDesiredHeadTrackingMode to " + mode, e);
+        }
+    }
+
+    /**
+     * @hide
+     * Recenters the head tracking at the current position / orientation.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void recenterHeadTracker() {
+        try {
+            mAm.getService().recenterHeadTracker();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling recenterHeadTracker", e);
+        }
+    }
+
+    /**
+     * @hide
+     * Adds a listener to be notified of changes to the head tracking mode of the
+     * {@code Spatializer}
+     * @param executor the {@code Executor} handling the callbacks
+     * @param listener the listener to register
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void addOnHeadTrackingModeChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnHeadTrackingModeChangedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mHeadTrackingListenerLock) {
+            if (hasListener(listener, mHeadTrackingListeners)) {
+                throw new IllegalArgumentException(
+                        "Called addOnHeadTrackingModeChangedListener() "
+                                + "on a previously registered listener");
+            }
+            // lazy initialization of the list of strategy-preferred device listener
+            if (mHeadTrackingListeners == null) {
+                mHeadTrackingListeners = new ArrayList<>();
+            }
+            mHeadTrackingListeners.add(
+                    new ListenerInfo<OnHeadTrackingModeChangedListener>(listener, executor));
+            if (mHeadTrackingListeners.size() == 1) {
+                // register binder for callbacks
+                if (mHeadTrackingDispatcherStub == null) {
+                    mHeadTrackingDispatcherStub =
+                            new SpatializerHeadTrackingDispatcherStub();
+                }
+                try {
+                    mAm.getService().registerSpatializerHeadTrackingCallback(
+                            mHeadTrackingDispatcherStub);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Removes a previously added listener for changes to the head tracking mode of the
+     * {@code Spatializer}.
+     * @param listener the listener to unregister
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void removeOnHeadTrackingModeChangedListener(
+            @NonNull OnHeadTrackingModeChangedListener listener) {
+        Objects.requireNonNull(listener);
+        synchronized (mHeadTrackingListenerLock) {
+            if (!removeListener(listener, mHeadTrackingListeners)) {
+                throw new IllegalArgumentException(
+                        "Called removeOnHeadTrackingModeChangedListener() "
+                                + "on an unregistered listener");
+            }
+            if (mHeadTrackingListeners.size() == 0) {
+                // unregister binder for callbacks
+                try {
+                    mAm.getService().unregisterSpatializerHeadTrackingCallback(
+                            mHeadTrackingDispatcherStub);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                } finally {
+                    mHeadTrackingDispatcherStub = null;
+                    mHeadTrackingListeners = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Set the listener to receive head to soundstage pose updates.
+     * @param executor the {@code Executor} handling the callbacks
+     * @param listener the listener to register
+     * @see #clearOnHeadToSoundstagePoseUpdatedListener()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setOnHeadToSoundstagePoseUpdatedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnHeadToSoundstagePoseUpdatedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mPoseListenerLock) {
+            if (mPoseListener != null) {
+                throw new IllegalStateException("Trying to overwrite existing listener");
+            }
+            mPoseListener =
+                    new ListenerInfo<OnHeadToSoundstagePoseUpdatedListener>(listener, executor);
+            mPoseDispatcher = new SpatializerPoseDispatcherStub();
+            try {
+                mAm.getService().registerHeadToSoundstagePoseCallback(mPoseDispatcher);
+            } catch (RemoteException e) {
+                mPoseListener = null;
+                mPoseDispatcher = null;
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Clears the listener for head to soundstage pose updates
+     * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void clearOnHeadToSoundstagePoseUpdatedListener() {
+        synchronized (mPoseListenerLock) {
+            if (mPoseDispatcher == null) {
+                throw (new IllegalStateException("No listener to clear"));
+            }
+            try {
+                mAm.getService().unregisterHeadToSoundstagePoseCallback(mPoseDispatcher);
+            } catch (RemoteException e) { }
+            mPoseListener = null;
+            mPoseDispatcher = null;
+        }
+    }
+
+    /**
+     * @hide
+     * Sets an additional transform over the soundstage.
+     * The transform represents the pose of the soundstage, relative
+     * to either the device (in {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} mode), the world (in
+     * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD}) or the listener’s head (in
+     * {@link #HEAD_TRACKING_MODE_DISABLED} mode).
+     * @param transform an array of 6 float values, the first 3 are the translation vector, the
+     *                  other 3 are the rotation vector.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setGlobalTransform(@NonNull float[] transform) {
+        if (Objects.requireNonNull(transform).length != 6) {
+            throw new IllegalArgumentException("transform array must be of size 6, was "
+                    + transform.length);
+        }
+        try {
+            mAm.getService().setSpatializerGlobalTransform(transform);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setGlobalTransform", e);
+        }
+    }
+
+    /**
+     * @hide
+     * Sets a parameter on the platform spatializer effect implementation.
+     * This is to be used for vendor-specific configurations of their effect, keys and values are
+     * not reuseable across implementations.
+     * @param key the parameter to change
+     * @param value an array for the value of the parameter to change
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setEffectParameter(int key, @NonNull byte[] value) {
+        Objects.requireNonNull(value);
+        try {
+            mAm.getService().setSpatializerParameter(key, value);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setEffectParameter", e);
+        }
+    }
+
+    /**
+     * @hide
+     * Retrieves a parameter value from the platform spatializer effect implementation.
+     * This is to be used for vendor-specific configurations of their effect, keys and values are
+     * not reuseable across implementations.
+     * @param key the parameter for which the value is queried
+     * @param value a non-empty array to contain the return value. The caller is responsible for
+     *              passing an array of size matching the parameter.
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void getEffectParameter(int key, @NonNull byte[] value) {
+        Objects.requireNonNull(value);
+        try {
+            mAm.getService().getSpatializerParameter(key, value);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getEffectParameter", e);
+        }
+    }
+
+    /**
+     * @hide
+     * Returns the id of the output stream used for the spatializer effect playback
+     * @return id of the output stream, or 0 if no spatializer playback is active
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @IntRange(from = 0) int getOutput() {
+        try {
+            return mAm.getService().getSpatializerOutput();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getSpatializerOutput", e);
+            return 0;
+        }
+    }
+
+    /**
+     * @hide
+     * Sets the listener to receive spatializer effect output updates
+     * @param executor the {@code Executor} handling the callbacks
+     * @param listener the listener to register
+     * @see #clearOnSpatializerOutputChangedListener()
+     * @see #getOutput()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setOnSpatializerOutputChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnSpatializerOutputChangedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mOutputListenerLock) {
+            if (mOutputListener != null) {
+                throw new IllegalStateException("Trying to overwrite existing listener");
+            }
+            mOutputListener =
+                    new ListenerInfo<OnSpatializerOutputChangedListener>(listener, executor);
+            mOutputDispatcher = new SpatializerOutputDispatcherStub();
+            try {
+                mAm.getService().registerSpatializerOutputCallback(mOutputDispatcher);
+            } catch (RemoteException e) {
+                mOutputListener = null;
+                mOutputDispatcher = null;
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Clears the listener for spatializer effect output updates
+     * @see #setOnSpatializerOutputChangedListener(Executor, OnSpatializerOutputChangedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void clearOnSpatializerOutputChangedListener() {
+        synchronized (mOutputListenerLock) {
+            if (mOutputDispatcher == null) {
+                throw (new IllegalStateException("No listener to clear"));
+            }
+            try {
+                mAm.getService().unregisterSpatializerOutputCallback(mOutputDispatcher);
+            } catch (RemoteException e) { }
+            mOutputListener = null;
+            mOutputDispatcher = null;
+        }
+    }
+
+    //-----------------------------------------------------------------------------
+    // callback helper definitions
+
+    private static class ListenerInfo<T> {
+        final @NonNull T mListener;
+        final @NonNull Executor mExecutor;
+
+        ListenerInfo(T listener, Executor exe) {
+            mListener = listener;
+            mExecutor = exe;
+        }
+    }
+
+    private static <T> ListenerInfo<T> getListenerInfo(
+            T listener, ArrayList<ListenerInfo<T>> listeners) {
+        if (listeners == null) {
+            return null;
+        }
+        for (ListenerInfo<T> info : listeners) {
+            if (info.mListener == listener) {
+                return info;
+            }
+        }
+        return null;
+    }
+
+    private static <T> boolean hasListener(T listener, ArrayList<ListenerInfo<T>> listeners) {
+        return getListenerInfo(listener, listeners) != null;
+    }
+
+    private static <T> boolean removeListener(T listener, ArrayList<ListenerInfo<T>> listeners) {
+        final ListenerInfo<T> infoToRemove = getListenerInfo(listener, listeners);
+        if (infoToRemove != null) {
+            listeners.remove(infoToRemove);
+            return true;
+        }
+        return false;
+    }
+
+    //-----------------------------------------------------------------------------
+    // head tracking callback management and stub
+
+    private final Object mHeadTrackingListenerLock = new Object();
+    /**
+     * List of listeners for head tracking mode listener and their associated Executor.
+     * List is lazy-initialized on first registration
+     */
+    @GuardedBy("mHeadTrackingListenerLock")
+    private @Nullable ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>
+            mHeadTrackingListeners;
+
+    @GuardedBy("mHeadTrackingListenerLock")
+    private @Nullable SpatializerHeadTrackingDispatcherStub mHeadTrackingDispatcherStub;
+
+    private final class SpatializerHeadTrackingDispatcherStub
+            extends ISpatializerHeadTrackingModeCallback.Stub {
+        @Override
+        public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) {
+            // make a shallow copy of listeners so callback is not executed under lock
+            final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners;
+            synchronized (mHeadTrackingListenerLock) {
+                if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) {
+                    return;
+                }
+                headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>)
+                        mHeadTrackingListeners.clone();
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) {
+                    info.mExecutor.execute(() -> info.mListener
+                            .onHeadTrackingModeChanged(Spatializer.this, mode));
+                }
+            }
+        }
+
+        @Override
+        public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) {
+            // make a shallow copy of listeners so callback is not executed under lock
+            final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners;
+            synchronized (mHeadTrackingListenerLock) {
+                if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) {
+                    return;
+                }
+                headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>)
+                        mHeadTrackingListeners.clone();
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) {
+                    info.mExecutor.execute(() -> info.mListener
+                            .onDesiredHeadTrackingModeChanged(Spatializer.this, mode));
+                }
+            }
+        }
+    }
+
+    //-----------------------------------------------------------------------------
+    // head pose callback management and stub
+    private final Object mPoseListenerLock = new Object();
+    /**
+     * Listener for head to soundstage updates
+     */
+    @GuardedBy("mPoseListenerLock")
+    private @Nullable ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> mPoseListener;
+    @GuardedBy("mPoseListenerLock")
+    private @Nullable SpatializerPoseDispatcherStub mPoseDispatcher;
+
+    private final class SpatializerPoseDispatcherStub
+            extends ISpatializerHeadToSoundStagePoseCallback.Stub {
+
+        @Override
+        public void dispatchPoseChanged(float[] pose) {
+            // make a copy of ref to listener so callback is not executed under lock
+            final ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> listener;
+            synchronized (mPoseListenerLock) {
+                listener = mPoseListener;
+            }
+            if (listener == null) {
+                return;
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                listener.mExecutor.execute(() -> listener.mListener
+                        .onHeadToSoundstagePoseUpdated(Spatializer.this, pose));
+            }
+        }
+    }
+
+    //-----------------------------------------------------------------------------
+    // output callback management and stub
+    private final Object mOutputListenerLock = new Object();
+    /**
+     * Listener for output updates
+     */
+    @GuardedBy("mOutputListenerLock")
+    private @Nullable ListenerInfo<OnSpatializerOutputChangedListener> mOutputListener;
+    @GuardedBy("mOutputListenerLock")
+    private @Nullable SpatializerOutputDispatcherStub mOutputDispatcher;
+
+    private final class SpatializerOutputDispatcherStub
+            extends ISpatializerOutputCallback.Stub {
+
+        @Override
+        public void dispatchSpatializerOutputChanged(int output) {
+            // make a copy of ref to listener so callback is not executed under lock
+            final ListenerInfo<OnSpatializerOutputChangedListener> listener;
+            synchronized (mOutputListenerLock) {
+                listener = mOutputListener;
+            }
+            if (listener == null) {
+                return;
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                listener.mExecutor.execute(() -> listener.mListener
+                        .onSpatializerOutputChanged(Spatializer.this, output));
+            }
+        }
+    }
+}
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 37e1415..5259c4f 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -16,15 +16,16 @@
 
 package android.media.projection;
 
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
 import android.hardware.display.VirtualDisplayConfig;
-import android.media.projection.IMediaProjection;
-import android.media.projection.IMediaProjectionCallback;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -100,19 +101,22 @@
     public VirtualDisplay createVirtualDisplay(@NonNull String name,
             int width, int height, int dpi, boolean isSecure, @Nullable Surface surface,
             @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
-        DisplayManager dm = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
         int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
                 | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
         if (isSecure) {
             flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE;
         }
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width,
-                height, dpi);
+        Context windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(),
+                TYPE_APPLICATION, null /* options */);
+        final VirtualDisplayConfig.Builder builder = buildMirroredVirtualDisplay(name, width,
+                height, dpi, windowContext.getWindowContextToken());
         builder.setFlags(flags);
         if (surface != null) {
             builder.setSurface(surface);
         }
-        return dm.createVirtualDisplay(this, builder.build(), callback, handler);
+        VirtualDisplay virtualDisplay = createVirtualDisplay(builder.build(), callback, handler,
+                windowContext);
+        return virtualDisplay;
     }
 
     /**
@@ -141,13 +145,35 @@
     public VirtualDisplay createVirtualDisplay(@NonNull String name,
             int width, int height, int dpi, int flags, @Nullable Surface surface,
             @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width,
-                height, dpi);
+        Context windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(),
+                TYPE_APPLICATION, null /* options */);
+        final VirtualDisplayConfig.Builder builder = buildMirroredVirtualDisplay(name, width,
+                height, dpi, windowContext.getWindowContextToken());
         builder.setFlags(flags);
         if (surface != null) {
             builder.setSurface(surface);
         }
-        return createVirtualDisplay(builder.build(), callback, handler);
+        VirtualDisplay virtualDisplay = createVirtualDisplay(builder.build(), callback, handler,
+                windowContext);
+        return virtualDisplay;
+    }
+
+    /**
+     * Constructs a {@link VirtualDisplayConfig.Builder}, which will mirror the contents of a
+     * DisplayArea. The DisplayArea to mirror is from the DisplayArea the caller is launched on.
+     *
+     * @param name   The name of the virtual display, must be non-empty.
+     * @param width  The width of the virtual display in pixels. Must be greater than 0.
+     * @param height The height of the virtual display in pixels. Must be greater than 0.
+     * @param dpi    The density of the virtual display in dpi. Must be greater than 0.
+     * @return a config representing a VirtualDisplay
+     */
+    private VirtualDisplayConfig.Builder buildMirroredVirtualDisplay(@NonNull String name,
+            int width, int height, int dpi, IBinder windowContextToken) {
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width,
+                height, dpi);
+        builder.setWindowTokenClientToMirror(windowContextToken);
+        return builder;
     }
 
     /**
@@ -156,20 +182,22 @@
      *
      * @param virtualDisplayConfig The arguments for the virtual display configuration. See
      * {@link VirtualDisplayConfig} for using it.
-     * @param callback Callback to call when the virtual display's state
-     * changes, or null if none.
-     * @param handler The {@link android.os.Handler} on which the callback should be
-     * invoked, or null if the callback should be invoked on the calling
-     * thread's main {@link android.os.Looper}.
+     * @param callback Callback to call when the virtual display's state changes, or null if none.
+     * @param handler The {@link android.os.Handler} on which the callback should be invoked, or
+     *                null if the callback should be invoked on the calling thread's main
+     *                {@link android.os.Looper}.
+     * @param windowContext the WindowContext associated with the caller.
      *
      * @see android.hardware.display.VirtualDisplay
      * @hide
      */
     @Nullable
     public VirtualDisplay createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig,
-            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
+            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler,
+            Context windowContext) {
         DisplayManager dm = mContext.getSystemService(DisplayManager.class);
-        return dm.createVirtualDisplay(this, virtualDisplayConfig, callback, handler);
+        return dm.createVirtualDisplay(this, virtualDisplayConfig, callback, handler,
+                windowContext);
     }
 
     /**
diff --git a/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java b/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java
index 8216edf..fe7988f 100644
--- a/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java
+++ b/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java
@@ -43,6 +43,7 @@
     View.OnClickListener mLearnMoreListener;
     private CharSequence mContentDescription;
     private CharSequence mLearnMoreContentDescription;
+    private FooterLearnMoreSpan mLearnMoreSpan;
 
     public FooterPreference(Context context, AttributeSet attrs) {
         super(context, attrs, R.attr.footerPreferenceStyle);
@@ -68,7 +69,11 @@
         if (learnMore != null && mLearnMoreListener != null) {
             learnMore.setVisibility(View.VISIBLE);
             SpannableString learnMoreText = new SpannableString(learnMore.getText());
-            learnMoreText.setSpan(new FooterLearnMoreSpan(mLearnMoreListener), 0,
+            if (mLearnMoreSpan != null) {
+                learnMoreText.removeSpan(mLearnMoreSpan);
+            }
+            mLearnMoreSpan = new FooterLearnMoreSpan(mLearnMoreListener);
+            learnMoreText.setSpan(mLearnMoreSpan, 0,
                     learnMoreText.length(), 0);
             learnMore.setText(learnMoreText);
             if (!TextUtils.isEmpty(mLearnMoreContentDescription)) {
diff --git a/packages/SettingsLib/IllustrationPreference/res/layout/illustration_preference.xml b/packages/SettingsLib/IllustrationPreference/res/layout/illustration_preference.xml
index 54145d6..eecb4bf 100644
--- a/packages/SettingsLib/IllustrationPreference/res/layout/illustration_preference.xml
+++ b/packages/SettingsLib/IllustrationPreference/res/layout/illustration_preference.xml
@@ -34,17 +34,21 @@
         android:orientation="vertical">
 
         <ImageView
+            android:id="@+id/background_view"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:scaleType="centerInside"
+            android:layout_gravity="center"
+            android:adjustViewBounds="true"
             android:src="@drawable/protection_background"/>
 
         <com.airbnb.lottie.LottieAnimationView
             android:id="@+id/lottie_view"
-            android:adjustViewBounds="true"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_gravity="center" />
+            android:layout_gravity="center"
+            android:maxWidth="@dimen/settingslib_illustration_width"
+            android:maxHeight="@dimen/settingslib_illustration_height"
+            android:adjustViewBounds="true"/>
 
         <FrameLayout
             android:id="@+id/middleground_layout"
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 266fc78..468a976 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -17,6 +17,7 @@
 package com.android.settingslib.widget;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Animatable2;
@@ -50,7 +51,9 @@
     private static final String TAG = "IllustrationPreference";
 
     private static final boolean IS_ENABLED_LOTTIE_ADAPTIVE_COLOR = false;
+    private static final int SIZE_UNSPECIFIED = -1;
 
+    private int mMaxHeight = SIZE_UNSPECIFIED;
     private int mImageResId;
     private boolean mIsAutoScale;
     private Uri mImageUri;
@@ -98,6 +101,8 @@
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
 
+        final ImageView backgroundView =
+                (ImageView) holder.findViewById(R.id.background_view);
         final FrameLayout middleGroundLayout =
                 (FrameLayout) holder.findViewById(R.id.middleground_layout);
         final LottieAnimationView illustrationView =
@@ -115,6 +120,7 @@
         illustrationFrame.setLayoutParams(lp);
 
         handleImageWithAnimation(illustrationView);
+        handleImageFrameMaxHeight(backgroundView, illustrationView);
 
         if (mIsAutoScale) {
             illustrationView.setScaleType(mIsAutoScale
@@ -170,7 +176,14 @@
     }
 
     /**
-     * Sets image drawable to display image in {@link LottieAnimationView}
+     * Gets the lottie illustration resource id.
+     */
+    public int getLottieAnimationResId() {
+        return mImageResId;
+    }
+
+    /**
+     * Sets the image drawable to display image in {@link LottieAnimationView}.
      *
      * @param imageDrawable the drawable of an image
      */
@@ -183,7 +196,16 @@
     }
 
     /**
-     * Sets image uri to display image in {@link LottieAnimationView}
+     * Gets the image drawable from display image in {@link LottieAnimationView}.
+     *
+     * @return the drawable of an image
+     */
+    public Drawable getImageDrawable() {
+        return mImageDrawable;
+    }
+
+    /**
+     * Sets the image uri to display image in {@link LottieAnimationView}.
      *
      * @param imageUri the Uri of an image
      */
@@ -195,6 +217,28 @@
         }
     }
 
+    /**
+     * Gets the image uri from display image in {@link LottieAnimationView}.
+     *
+     * @return the Uri of an image
+     */
+    public Uri getImageUri() {
+        return mImageUri;
+    }
+
+    /**
+     * Sets the maximum height of the views, still use the specific one if the maximum height was
+     * larger than the specific height from XML.
+     *
+     * @param maxHeight the maximum height of the frame views in terms of pixels.
+     */
+    public void setMaxHeight(int maxHeight) {
+        if (maxHeight != mMaxHeight) {
+            mMaxHeight = maxHeight;
+            notifyChanged();
+        }
+    }
+
     private void resetImageResourceCache() {
         mImageDrawable = null;
         mImageUri = null;
@@ -249,6 +293,23 @@
         }
     }
 
+    private void handleImageFrameMaxHeight(ImageView backgroundView, ImageView illustrationView) {
+        if (mMaxHeight == SIZE_UNSPECIFIED) {
+            return;
+        }
+
+        final Resources res = backgroundView.getResources();
+        final int frameWidth = res.getDimensionPixelSize(R.dimen.settingslib_illustration_width);
+        final int frameHeight = res.getDimensionPixelSize(R.dimen.settingslib_illustration_height);
+        final int restrictedMaxHeight = Math.min(mMaxHeight, frameHeight);
+        backgroundView.setMaxHeight(restrictedMaxHeight);
+        illustrationView.setMaxHeight(restrictedMaxHeight);
+
+        // Ensures the illustration view size is smaller than or equal to the background view size.
+        final float aspectRatio = (float) frameWidth / frameHeight;
+        illustrationView.setMaxWidth((int) (restrictedMaxHeight * aspectRatio));
+    }
+
     private void startAnimation(Drawable drawable) {
         if (!(drawable instanceof Animatable)) {
             return;
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
index 8f3e4bd..220cf6b 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
@@ -18,6 +18,7 @@
 
 import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_NEW_TASK;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
@@ -329,6 +330,18 @@
     }
 
     /**
+     * Whether the {@link Activity} should be launched in a separate task.
+     */
+    public boolean isNewTask(Context context) {
+        ensureMetadataNotStale(context);
+        if (mMetaData != null
+                && mMetaData.containsKey(META_DATA_NEW_TASK)) {
+            return mMetaData.getBoolean(META_DATA_NEW_TASK);
+        }
+        return false;
+    }
+
+    /**
      * Ensures metadata is not stale for this tile.
      */
     private void ensureMetadataNotStale(Context context) {
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
index a2bec33..acc0087 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
@@ -230,6 +230,13 @@
     public static final String META_DATA_KEY_PROFILE = "com.android.settings.profile";
 
     /**
+     * Name of the meta-data item that should be set in the AndroidManifest.xml
+     * to specify whether the {@link android.app.Activity} should be launched in a separate task.
+     * This should be a boolean value {@code true} or {@code false}, set using {@code android:value}
+     */
+    public static final String META_DATA_NEW_TASK = "com.android.settings.new_task";
+
+    /**
      * Build a list of DashboardCategory.
      */
     public static List<DashboardCategory> getCategories(Context context,
diff --git a/packages/SettingsLib/TwoTargetPreference/res/layout-v31/preference_two_target_divider.xml b/packages/SettingsLib/TwoTargetPreference/res/layout-v31/preference_two_target_divider.xml
index f21e51c..4641421 100644
--- a/packages/SettingsLib/TwoTargetPreference/res/layout-v31/preference_two_target_divider.xml
+++ b/packages/SettingsLib/TwoTargetPreference/res/layout-v31/preference_two_target_divider.xml
@@ -22,6 +22,8 @@
     android:layout_height="match_parent"
     android:gravity="start|center_vertical"
     android:orientation="horizontal"
+    android:paddingStart="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingLeft="?android:attr/listPreferredItemPaddingEnd"
     android:paddingTop="16dp"
     android:paddingBottom="16dp">
     <View
diff --git a/packages/SettingsLib/Utils/src/com/android/settingslib/utils/BuildCompatUtils.java b/packages/SettingsLib/Utils/src/com/android/settingslib/utils/BuildCompatUtils.java
index 44f6f54..9dcb5bc 100644
--- a/packages/SettingsLib/Utils/src/com/android/settingslib/utils/BuildCompatUtils.java
+++ b/packages/SettingsLib/Utils/src/com/android/settingslib/utils/BuildCompatUtils.java
@@ -52,9 +52,9 @@
         }
 
         return (VERSION.CODENAME.equals("REL") && VERSION.SDK_INT >= 31)
-                || (VERSION.CODENAME.length() == 1
-                && VERSION.CODENAME.compareTo("S") >= 0
-                && VERSION.CODENAME.compareTo("Z") <= 0);
+                || (VERSION.CODENAME.length() >= 1
+                && VERSION.CODENAME.toUpperCase().charAt(0) >= 'S'
+                && VERSION.CODENAME.toUpperCase().charAt(0) <= 'Z');
     }
 
     private BuildCompatUtils() {}
diff --git a/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java b/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java
index bc0c6f3..794b0eb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java
+++ b/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java
@@ -15,6 +15,8 @@
  */
 package com.android.settingslib;
 
+import com.android.settingslib.mobile.TelephonyIcons;
+
 import java.text.SimpleDateFormat;
 import java.util.Objects;
 
@@ -40,9 +42,17 @@
         // For logging.
         public final String name;
 
-        public IconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc,
-                int sbNullState, int qsNullState, int sbDiscState, int qsDiscState,
-                int discContentDesc) {
+        public IconGroup(
+                String name,
+                int[][] sbIcons,
+                int[][] qsIcons,
+                int[] contentDesc,
+                int sbNullState,
+                int qsNullState,
+                int sbDiscState,
+                int qsDiscState,
+                int discContentDesc
+        ) {
             this.name = name;
             this.sbIcons = sbIcons;
             this.qsIcons = qsIcons;
@@ -131,6 +141,19 @@
                     && other.activityOut == activityOut
                     && other.rssi == rssi;
         }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(
+                    connected,
+                    enabled,
+                    level,
+                    inetCondition,
+                    iconGroup,
+                    activityIn,
+                    activityOut,
+                    rssi);
+        }
     }
 
     /**
@@ -139,18 +162,31 @@
     public static class MobileIconGroup extends IconGroup {
         public final int dataContentDescription; // mContentDescriptionDataType
         public final int dataType;
-        public final boolean isWide;
-        public final int qsDataType;
 
-        public MobileIconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc,
-                int sbNullState, int qsNullState, int sbDiscState, int qsDiscState,
-                int discContentDesc, int dataContentDesc, int dataType, boolean isWide) {
-            super(name, sbIcons, qsIcons, contentDesc, sbNullState, qsNullState, sbDiscState,
-                qsDiscState, discContentDesc);
+        public MobileIconGroup(
+                String name,
+                int[][] sbIcons,
+                int[][] qsIcons,
+                int[] contentDesc,
+                int sbNullState,
+                int qsNullState,
+                int sbDiscState,
+                int qsDiscState,
+                int discContentDesc,
+                int dataContentDesc,
+                int dataType
+        ) {
+            super(name,
+                    sbIcons,
+                    qsIcons,
+                    contentDesc,
+                    sbNullState,
+                    qsNullState,
+                    sbDiscState,
+                    qsDiscState,
+                    discContentDesc);
             this.dataContentDescription = dataContentDesc;
             this.dataType = dataType;
-            this.isWide = isWide;
-            this.qsDataType = dataType; // TODO: remove this field
         }
     }
 
@@ -187,6 +223,27 @@
             defaultDataOff = state.defaultDataOff;
         }
 
+        /** @return true if this state is disabled or not default data */
+        public boolean isDataDisabledOrNotDefault() {
+            return (iconGroup == TelephonyIcons.DATA_DISABLED
+                    || (iconGroup == TelephonyIcons.NOT_DEFAULT_DATA)) && userSetup;
+        }
+
+        /** @return if this state is considered to have inbound activity */
+        public boolean hasActivityIn() {
+            return dataConnected && !carrierNetworkChangeMode && activityIn;
+        }
+
+        /** @return if this state is considered to have outbound activity */
+        public boolean hasActivityOut() {
+            return dataConnected && !carrierNetworkChangeMode && activityOut;
+        }
+
+        /** @return true if this state should show a RAT icon in quick settings */
+        public boolean showQuickSettingsRatIcon() {
+            return dataConnected || isDataDisabledOrNotDefault();
+        }
+
         @Override
         protected void toString(StringBuilder builder) {
             super.toString(builder);
@@ -202,23 +259,40 @@
             builder.append("carrierNetworkChangeMode=").append(carrierNetworkChangeMode)
                     .append(',');
             builder.append("userSetup=").append(userSetup).append(',');
-            builder.append("defaultDataOff=").append(defaultDataOff);
+            builder.append("defaultDataOff=").append(defaultDataOff).append(',');
+            builder.append("showQuickSettingsRatIcon=").append(showQuickSettingsRatIcon());
         }
 
         @Override
         public boolean equals(Object o) {
             return super.equals(o)
-                && Objects.equals(((MobileState) o).networkName, networkName)
-                && Objects.equals(((MobileState) o).networkNameData, networkNameData)
-                && ((MobileState) o).dataSim == dataSim
-                && ((MobileState) o).dataConnected == dataConnected
-                && ((MobileState) o).isEmergency == isEmergency
-                && ((MobileState) o).airplaneMode == airplaneMode
-                && ((MobileState) o).carrierNetworkChangeMode == carrierNetworkChangeMode
-                && ((MobileState) o).userSetup == userSetup
-                && ((MobileState) o).isDefault == isDefault
-                && ((MobileState) o).roaming == roaming
-                && ((MobileState) o).defaultDataOff == defaultDataOff;
+                    && Objects.equals(((MobileState) o).networkName, networkName)
+                    && Objects.equals(((MobileState) o).networkNameData, networkNameData)
+                    && ((MobileState) o).dataSim == dataSim
+                    && ((MobileState) o).dataConnected == dataConnected
+                    && ((MobileState) o).isEmergency == isEmergency
+                    && ((MobileState) o).airplaneMode == airplaneMode
+                    && ((MobileState) o).carrierNetworkChangeMode == carrierNetworkChangeMode
+                    && ((MobileState) o).userSetup == userSetup
+                    && ((MobileState) o).isDefault == isDefault
+                    && ((MobileState) o).roaming == roaming
+                    && ((MobileState) o).defaultDataOff == defaultDataOff;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(super.hashCode(),
+                    networkName,
+                    networkNameData,
+                    dataSim,
+                    dataConnected,
+                    isEmergency,
+                    airplaneMode,
+                    carrierNetworkChangeMode,
+                    userSetup,
+                    isDefault,
+                    roaming,
+                    defaultDataOff);
         }
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationAccesses.java b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
similarity index 76%
rename from packages/SettingsLib/src/com/android/settingslib/location/RecentLocationAccesses.java
rename to packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
index 877dd2d..2e7cfcb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationAccesses.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.location;
+package com.android.settingslib.applications;
+
 
 import android.app.AppOpsManager;
 import android.content.Context;
@@ -39,14 +40,24 @@
 import java.util.List;
 
 /**
- * Retrieves the information of applications which accessed location recently.
+ * Retrieval of app ops information for the specified ops.
  */
-public class RecentLocationAccesses {
-    private static final String TAG = RecentLocationAccesses.class.getSimpleName();
+public class RecentAppOpsAccess {
     @VisibleForTesting
-    static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
+    static final int[] LOCATION_OPS = new int[]{
+            AppOpsManager.OP_FINE_LOCATION,
+            AppOpsManager.OP_COARSE_LOCATION,
+    };
+    private static final int[] MICROPHONE_OPS = new int[]{
+            AppOpsManager.OP_RECORD_AUDIO,
+    };
 
-    // Keep last 24 hours of location app information.
+
+    private static final String TAG = RecentAppOpsAccess.class.getSimpleName();
+    @VisibleForTesting
+    public static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
+
+    // Keep last 24 hours of access app information.
     private static final long RECENT_TIME_INTERVAL_MILLIS = DateUtils.DAY_IN_MILLIS;
 
     /** The flags for querying ops that are trusted for showing in the UI. */
@@ -54,47 +65,55 @@
             | AppOpsManager.OP_FLAG_UNTRUSTED_PROXY
             | AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
 
-    @VisibleForTesting
-    static final int[] LOCATION_OPS = new int[]{
-            AppOpsManager.OP_FINE_LOCATION,
-            AppOpsManager.OP_COARSE_LOCATION,
-    };
-
     private final PackageManager mPackageManager;
     private final Context mContext;
+    private final int[] mOps;
     private final IconDrawableFactory mDrawableFactory;
     private final Clock mClock;
 
-    public RecentLocationAccesses(Context context) {
-        this(context, Clock.systemDefaultZone());
+    public RecentAppOpsAccess(Context context, int[] ops) {
+        this(context, Clock.systemDefaultZone(), ops);
     }
 
     @VisibleForTesting
-    RecentLocationAccesses(Context context, Clock clock) {
+    RecentAppOpsAccess(Context context, Clock clock, int[] ops) {
         mContext = context;
         mPackageManager = context.getPackageManager();
+        mOps = ops;
         mDrawableFactory = IconDrawableFactory.newInstance(context);
         mClock = clock;
     }
 
     /**
-     * Fills a list of applications which queried location recently within specified time.
-     * Apps are sorted by recency. Apps with more recent location accesses are in the front.
+     * Creates an instance of {@link RecentAppOpsAccess} for location (coarse and fine) access.
+     */
+    public static RecentAppOpsAccess createForLocation(Context context) {
+        return new RecentAppOpsAccess(context, LOCATION_OPS);
+    }
+
+    /**
+     * Creates an instance of {@link RecentAppOpsAccess} for microphone access.
+     */
+    public static RecentAppOpsAccess createForMicrophone(Context context) {
+        return new RecentAppOpsAccess(context, MICROPHONE_OPS);
+    }
+
+    /**
+     * Fills a list of applications which queried for access recently within specified time.
+     * Apps are sorted by recency. Apps with more recent accesses are in the front.
      */
     @VisibleForTesting
-    List<Access> getAppList(boolean showSystemApps) {
-        // Retrieve a location usage list from AppOps
-        PackageManager pm = mContext.getPackageManager();
-        AppOpsManager aoManager =
-                (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
-        List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(LOCATION_OPS);
+    public List<Access> getAppList(boolean showSystemApps) {
+        // Retrieve a access usage list from AppOps
+        AppOpsManager aoManager = mContext.getSystemService(AppOpsManager.class);
+        List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(mOps);
 
         final int appOpsCount = appOps != null ? appOps.size() : 0;
 
         // Process the AppOps list and generate a preference list.
         ArrayList<Access> accesses = new ArrayList<>(appOpsCount);
         final long now = mClock.millis();
-        final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        final UserManager um = mContext.getSystemService(UserManager.class);
         final List<UserHandle> profiles = um.getUserProfiles();
 
         for (int i = 0; i < appOpsCount; ++i) {
@@ -111,9 +130,10 @@
             // Don't show apps that do not have user sensitive location permissions
             boolean showApp = true;
             if (!showSystemApps) {
-                for (int op : LOCATION_OPS) {
+                for (int op : mOps) {
                     final String permission = AppOpsManager.opToPermission(op);
-                    final int permissionFlags = pm.getPermissionFlags(permission, packageName,
+                    final int permissionFlags = mPackageManager.getPermissionFlags(permission,
+                            packageName,
                             user);
                     if (PermissionChecker.checkPermissionForPreflight(mContext, permission,
                             PermissionChecker.PID_UNKNOWN, uid, packageName)
@@ -144,12 +164,11 @@
         return accesses;
     }
 
-
     /**
-     * Gets a list of apps that accessed location recently, sorting by recency.
+     * Gets a list of apps that accessed the app op recently, sorting by recency.
      *
      * @param showSystemApps whether includes system apps in the list.
-     * @return the list of apps that recently accessed location.
+     * @return the list of apps that recently accessed the app op.
      */
     public List<Access> getAppListSorted(boolean showSystemApps) {
         List<Access> accesses = getAppList(showSystemApps);
@@ -174,18 +193,18 @@
             AppOpsManager.PackageOps ops) {
         String packageName = ops.getPackageName();
         List<AppOpsManager.OpEntry> entries = ops.getOps();
-        long locationAccessFinishTime = 0L;
-        // Earliest time for a location access to end and still be shown in list.
-        long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
+        long accessFinishTime = 0L;
+        // Earliest time for a access to end and still be shown in list.
+        long recentAccessCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
         // Compute the most recent access time from all op entries.
         for (AppOpsManager.OpEntry entry : entries) {
             long lastAccessTime = entry.getLastAccessTime(TRUSTED_STATE_FLAGS);
-            if (lastAccessTime > locationAccessFinishTime) {
-                locationAccessFinishTime = lastAccessTime;
+            if (lastAccessTime > accessFinishTime) {
+                accessFinishTime = lastAccessTime;
             }
         }
         // Bail out if the entry is out of date.
-        if (locationAccessFinishTime < recentLocationCutoffTime) {
+        if (accessFinishTime < recentAccessCutoffTime) {
             return null;
         }
 
@@ -213,13 +232,16 @@
                 badgedAppLabel = null;
             }
             access = new Access(packageName, userHandle, icon, appLabel, badgedAppLabel,
-                    locationAccessFinishTime);
+                    accessFinishTime);
         } catch (NameNotFoundException e) {
             Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
         }
         return access;
     }
 
+    /**
+     * Information about when an app last accessed a particular app op.
+     */
     public static class Access {
         public final String packageName;
         public final UserHandle userHandle;
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
index c501b3a..2e8f368 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
@@ -110,6 +110,18 @@
     }
 
     /**
+     * Check if target package is in allow list except idle app
+     */
+    public boolean isAllowlistedExceptIdle(String pkg) {
+        try {
+            return mDeviceIdleService.isPowerSaveWhitelistExceptIdleApp(pkg);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+            return true;
+        }
+    }
+
+    /**
      *
      * @param pkgs a list of packageName
      * @return true when one of package is in allow list
diff --git a/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java
index a0c8663..ea5105b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java
+++ b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java
@@ -319,23 +319,11 @@
 
         @Override
         public boolean onPreferenceClick(Preference preference) {
-            // Activity to start if they click on the preference. Must start in new task to ensure
-            // that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to
-            // Settings > Location.
+            // Activity to start if they click on the preference.
             Intent settingIntent = new Intent();
             settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity);
+            // No flags set to ensure the activity is launched within the same settings task.
             logPreferenceClick(settingIntent);
-            // Sometimes the user may navigate back to "Settings" and launch another different
-            // injected setting after one injected setting has been launched.
-            //
-            // FLAG_ACTIVITY_CLEAR_TOP allows multiple Activities to stack on each other. When
-            // "back" button is clicked, the user will navigate through all the injected settings
-            // launched before. Such behavior could be quite confusing sometimes.
-            //
-            // In order to avoid such confusion, we use FLAG_ACTIVITY_CLEAR_TASK, which always clear
-            // up all existing injected settings and make sure that "back" button always brings the
-            // user back to "Settings" directly.
-            settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
             mContext.startActivityAsUser(settingIntent, mInfo.mUserHandle);
             return true;
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index 215e2a0..865c2f0b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -489,9 +489,9 @@
         @Override
         public void onDeviceListAdded(List<MediaDevice> devices) {
             synchronized (mMediaDevicesLock) {
+                Collections.sort(devices, COMPARATOR);
                 mMediaDevices.clear();
                 mMediaDevices.addAll(devices);
-                Collections.sort(devices, COMPARATOR);
                 // Add disconnected bluetooth devices only when phone output device is available.
                 for (MediaDevice device : devices) {
                     final int type = device.getDeviceType();
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java b/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java
index f8565bc..d4e58f7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java
@@ -50,178 +50,194 @@
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.carrier_network_change_mode,
-            0,
-            false);
+            0
+    );
 
     public static final MobileIconGroup THREE_G = new MobileIconGroup(
             "3G",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_3g,
-            TelephonyIcons.ICON_3G,
-            true);
+            TelephonyIcons.ICON_3G
+    );
 
     public static final MobileIconGroup WFC = new MobileIconGroup(
             "WFC",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
-            0, 0, false);
+            0,
+            0);
 
     public static final MobileIconGroup UNKNOWN = new MobileIconGroup(
             "Unknown",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
-            0, 0, false);
+            0,
+            0);
 
     public static final MobileIconGroup E = new MobileIconGroup(
             "E",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_edge,
-            TelephonyIcons.ICON_E,
-            false);
+            TelephonyIcons.ICON_E
+    );
 
     public static final MobileIconGroup ONE_X = new MobileIconGroup(
             "1X",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_cdma,
-            TelephonyIcons.ICON_1X,
-            true);
+            TelephonyIcons.ICON_1X
+    );
 
     public static final MobileIconGroup G = new MobileIconGroup(
             "G",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_gprs,
-            TelephonyIcons.ICON_G,
-            false);
+            TelephonyIcons.ICON_G
+    );
 
     public static final MobileIconGroup H = new MobileIconGroup(
             "H",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_3_5g,
-            TelephonyIcons.ICON_H,
-            false);
+            TelephonyIcons.ICON_H
+    );
 
     public static final MobileIconGroup H_PLUS = new MobileIconGroup(
             "H+",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_3_5g_plus,
-            TelephonyIcons.ICON_H_PLUS,
-            false);
+            TelephonyIcons.ICON_H_PLUS
+    );
 
     public static final MobileIconGroup FOUR_G = new MobileIconGroup(
             "4G",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_4g,
-            TelephonyIcons.ICON_4G,
-            true);
+            TelephonyIcons.ICON_4G
+    );
 
     public static final MobileIconGroup FOUR_G_PLUS = new MobileIconGroup(
             "4G+",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_4g_plus,
-            TelephonyIcons.ICON_4G_PLUS,
-            true);
+            TelephonyIcons.ICON_4G_PLUS
+    );
 
     public static final MobileIconGroup LTE = new MobileIconGroup(
             "LTE",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_lte,
-            TelephonyIcons.ICON_LTE,
-            true);
+            TelephonyIcons.ICON_LTE
+    );
 
     public static final MobileIconGroup LTE_PLUS = new MobileIconGroup(
             "LTE+",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_lte_plus,
-            TelephonyIcons.ICON_LTE_PLUS,
-            true);
+            TelephonyIcons.ICON_LTE_PLUS
+    );
 
     public static final MobileIconGroup LTE_CA_5G_E = new MobileIconGroup(
             "5Ge",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_5ge_html,
-            TelephonyIcons.ICON_5G_E,
-            true);
+            TelephonyIcons.ICON_5G_E
+    );
 
     public static final MobileIconGroup NR_5G = new MobileIconGroup(
             "5G",
@@ -234,8 +250,8 @@
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_5g,
-            TelephonyIcons.ICON_5G,
-            true);
+            TelephonyIcons.ICON_5G
+    );
 
     public static final MobileIconGroup NR_5G_PLUS = new MobileIconGroup(
             "5G_PLUS",
@@ -248,34 +264,36 @@
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_5g_plus,
-            TelephonyIcons.ICON_5G_PLUS,
-            true);
+            TelephonyIcons.ICON_5G_PLUS
+    );
 
     public static final MobileIconGroup DATA_DISABLED = new MobileIconGroup(
             "DataDisabled",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.cell_data_off_content_description,
-            0,
-            false);
+            0
+    );
 
     public static final MobileIconGroup NOT_DEFAULT_DATA = new MobileIconGroup(
             "NotDefaultData",
             null,
             null,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
-            0, 0,
+            0,
+            0,
             0,
             0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.not_default_data_content_description,
-            0,
-            false);
+            0
+    );
 
     public static final MobileIconGroup CARRIER_MERGED_WIFI = new MobileIconGroup(
             "CWF",
@@ -288,8 +306,8 @@
             /* qsDiscState= */ 0,
             AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
             R.string.data_connection_carrier_wifi,
-            TelephonyIcons.ICON_CWF,
-            /* isWide= */ true);
+            TelephonyIcons.ICON_CWF
+    );
 
     // When adding a new MobileIconGround, check if the dataContentDescription has to be filtered
     // in QSCarrier#hasValidTypeContentDescription
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java
index b65637f..5ee919b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.icu.text.MeasureFormat;
 import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.text.MessageFormat;
 import android.icu.text.RelativeDateTimeFormatter;
 import android.icu.text.RelativeDateTimeFormatter.RelativeUnit;
 import android.icu.util.Measure;
@@ -31,7 +32,9 @@
 import com.android.settingslib.R;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Locale;
+import java.util.Map;
 
 /** Utility class for generally useful string methods **/
 public class StringUtil {
@@ -183,4 +186,37 @@
         return formatRelativeTime(context, millis, withSeconds,
                 RelativeDateTimeFormatter.Style.LONG);
     }
+
+    /**
+     * Get ICU plural string without additional arguments
+     *
+     * @param context Context used to get the string
+     * @param count The number used to get the correct string for the current language's plural
+     *              rules.
+     * @param resId Resource id of the string
+     *
+     * @return Formatted plural string
+     */
+    public static String getIcuPluralsString(Context context, int count, int resId) {
+        MessageFormat msgFormat = new MessageFormat(context.getResources().getString(resId),
+                Locale.getDefault());
+        Map<String, Object> arguments = new HashMap<>();
+        arguments.put("count", count);
+        return msgFormat.format(arguments);
+    }
+
+    /**
+     * Get ICU plural string with additional arguments
+     *
+     * @param context Context used to get the string
+     * @param args String arguments
+     * @param resId Resource id of the string
+     *
+     * @return Formatted plural string
+     */
+    public static String getIcuPluralsString(Context context, Map<String, Object> args, int resId) {
+        MessageFormat msgFormat = new MessageFormat(context.getResources().getString(resId),
+                Locale.getDefault());
+        return msgFormat.format(args);
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileStateTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileStateTest.java
new file mode 100644
index 0000000..92a32bc
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileStateTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import com.android.settingslib.mobile.TelephonyIcons;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class MobileStateTest {
+
+    private SignalIcon.MobileState mState = new SignalIcon.MobileState();
+
+    @Before
+    public void setUp() {
+    }
+
+    @Test
+    public void testIsDataDisabledOrNotDefault_dataDisabled() {
+        mState.iconGroup = TelephonyIcons.DATA_DISABLED;
+        mState.userSetup = true;
+
+        assertTrue(mState.isDataDisabledOrNotDefault());
+    }
+
+    @Test
+    public void testIsDataDisabledOrNotDefault_notDefaultData() {
+        mState.iconGroup = TelephonyIcons.NOT_DEFAULT_DATA;
+        mState.userSetup = true;
+
+        assertTrue(mState.isDataDisabledOrNotDefault());
+    }
+
+    @Test
+    public void testIsDataDisabledOrNotDefault_notDisabled() {
+        mState.iconGroup = TelephonyIcons.G;
+        mState.userSetup = true;
+
+        assertFalse(mState.isDataDisabledOrNotDefault());
+    }
+
+    @Test
+    public void testHasActivityIn_noData_noActivity() {
+        mState.dataConnected = false;
+        mState.carrierNetworkChangeMode = false;
+        mState.activityIn = false;
+
+        assertFalse(mState.hasActivityIn());
+    }
+
+    @Test
+    public void testHasActivityIn_noData_activityIn() {
+        mState.dataConnected = false;
+        mState.carrierNetworkChangeMode = false;
+        mState.activityIn = true;
+
+        assertFalse(mState.hasActivityIn());
+    }
+
+    @Test
+    public void testHasActivityIn_dataConnected_activityIn() {
+        mState.dataConnected = true;
+        mState.carrierNetworkChangeMode = false;
+        mState.activityIn = true;
+
+        assertTrue(mState.hasActivityIn());
+    }
+
+    @Test
+    public void testHasActivityIn_carrierNetworkChange() {
+        mState.dataConnected = true;
+        mState.carrierNetworkChangeMode = true;
+        mState.activityIn = true;
+
+        assertFalse(mState.hasActivityIn());
+    }
+
+    @Test
+    public void testHasActivityOut_noData_noActivity() {
+        mState.dataConnected = false;
+        mState.carrierNetworkChangeMode = false;
+        mState.activityOut = false;
+
+        assertFalse(mState.hasActivityOut());
+    }
+
+    @Test
+    public void testHasActivityOut_noData_activityOut() {
+        mState.dataConnected = false;
+        mState.carrierNetworkChangeMode = false;
+        mState.activityOut = true;
+
+        assertFalse(mState.hasActivityOut());
+    }
+
+    @Test
+    public void testHasActivityOut_dataConnected_activityOut() {
+        mState.dataConnected = true;
+        mState.carrierNetworkChangeMode = false;
+        mState.activityOut = true;
+
+        assertTrue(mState.hasActivityOut());
+    }
+
+    @Test
+    public void testHasActivityOut_carrierNetworkChange() {
+        mState.dataConnected = true;
+        mState.carrierNetworkChangeMode = true;
+        mState.activityOut = true;
+
+        assertFalse(mState.hasActivityOut());
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/location/RecentLocationAccessesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
similarity index 72%
rename from packages/SettingsLib/tests/robotests/src/com/android/settingslib/location/RecentLocationAccessesTest.java
rename to packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
index 16d73a3..cb62a73 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/location/RecentLocationAccessesTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
@@ -1,15 +1,34 @@
-package com.android.settingslib.location;
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.applications;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import android.Manifest;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OpEntry;
 import android.app.AppOpsManager.PackageOps;
 import android.content.Context;
+import android.content.PermissionChecker;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -19,13 +38,14 @@
 import android.util.LongSparseArray;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowPermissionChecker;
 
 import java.time.Clock;
 import java.util.ArrayList;
@@ -34,7 +54,8 @@
 import java.util.concurrent.TimeUnit;
 
 @RunWith(RobolectricTestRunner.class)
-public class RecentLocationAccessesTest {
+@Config(shadows = {ShadowPermissionChecker.class})
+public class RecentAppOpsAccessesTest {
 
     private static final int TEST_UID = 1234;
     private static final long NOW = 1_000_000_000;  // Approximately 9/8/2001
@@ -54,7 +75,7 @@
     private Clock mClock;
     private Context mContext;
     private int mTestUserId;
-    private RecentLocationAccesses mRecentLocationAccesses;
+    private RecentAppOpsAccess mRecentAppOpsAccess;
 
     @Before
     public void setUp() throws NameNotFoundException {
@@ -69,24 +90,37 @@
                 .thenReturn("testApplicationLabel");
         when(mPackageManager.getUserBadgedLabel(isA(CharSequence.class), isA(UserHandle.class)))
                 .thenReturn("testUserBadgedLabel");
+        when(mPackageManager.getPermissionFlags(any(), any(), any()))
+                .thenReturn(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED
+                        | PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED);
+        for (String testPackageName : TEST_PACKAGE_NAMES) {
+            ShadowPermissionChecker.setResult(
+                    testPackageName,
+                    Manifest.permission.ACCESS_COARSE_LOCATION,
+                    PermissionChecker.PERMISSION_GRANTED);
+            ShadowPermissionChecker.setResult(
+                    testPackageName,
+                    Manifest.permission.ACCESS_FINE_LOCATION,
+                    PermissionChecker.PERMISSION_GRANTED);
+        }
         mTestUserId = UserHandle.getUserId(TEST_UID);
         when(mUserManager.getUserProfiles())
                 .thenReturn(Collections.singletonList(new UserHandle(mTestUserId)));
 
         long[] testRequestTime = {ONE_MIN_AGO, TWENTY_THREE_HOURS_AGO, TWO_DAYS_AGO};
         List<PackageOps> appOps = createTestPackageOpsList(TEST_PACKAGE_NAMES, testRequestTime);
-        when(mAppOpsManager.getPackagesForOps(RecentLocationAccesses.LOCATION_OPS)).thenReturn(
+        when(mAppOpsManager.getPackagesForOps(RecentAppOpsAccess.LOCATION_OPS)).thenReturn(
                 appOps);
         mockTestApplicationInfos(mTestUserId, TEST_PACKAGE_NAMES);
 
         when(mClock.millis()).thenReturn(NOW);
-        mRecentLocationAccesses = new RecentLocationAccesses(mContext, mClock);
+        mRecentAppOpsAccess = new RecentAppOpsAccess(mContext, mClock,
+                RecentAppOpsAccess.LOCATION_OPS);
     }
 
     @Test
-    @Ignore
     public void testGetAppList_shouldFilterRecentAccesses() {
-        List<RecentLocationAccesses.Access> requests = mRecentLocationAccesses.getAppList(false);
+        List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false);
         // Only two of the apps have requested location within 15 min.
         assertThat(requests).hasSize(2);
         // Make sure apps are ordered by recency
@@ -97,12 +131,11 @@
     }
 
     @Test
-    @Ignore
     public void testGetAppList_shouldNotShowAndroidOS() throws NameNotFoundException {
         // Add android OS to the list of apps.
         PackageOps androidSystemPackageOps =
                 createPackageOps(
-                        RecentLocationAccesses.ANDROID_SYSTEM_PACKAGE_NAME,
+                        RecentAppOpsAccess.ANDROID_SYSTEM_PACKAGE_NAME,
                         Process.SYSTEM_UID,
                         AppOpsManager.OP_FINE_LOCATION,
                         ONE_MIN_AGO);
@@ -110,12 +143,12 @@
                 {ONE_MIN_AGO, TWENTY_THREE_HOURS_AGO, TWO_DAYS_AGO, ONE_MIN_AGO};
         List<PackageOps> appOps = createTestPackageOpsList(TEST_PACKAGE_NAMES, testRequestTime);
         appOps.add(androidSystemPackageOps);
-        when(mAppOpsManager.getPackagesForOps(RecentLocationAccesses.LOCATION_OPS)).thenReturn(
+        when(mAppOpsManager.getPackagesForOps(RecentAppOpsAccess.LOCATION_OPS)).thenReturn(
                 appOps);
         mockTestApplicationInfos(
-                Process.SYSTEM_UID, RecentLocationAccesses.ANDROID_SYSTEM_PACKAGE_NAME);
+                Process.SYSTEM_UID, RecentAppOpsAccess.ANDROID_SYSTEM_PACKAGE_NAME);
 
-        List<RecentLocationAccesses.Access> requests = mRecentLocationAccesses.getAppList(true);
+        List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(true);
         // Android OS shouldn't show up in the list of apps.
         assertThat(requests).hasSize(2);
         // Make sure apps are ordered by recency
@@ -159,7 +192,7 @@
         // Slot for background access timestamp.
         final LongSparseArray<AppOpsManager.NoteOpEvent> accessEvents = new LongSparseArray<>();
         accessEvents.put(AppOpsManager.makeKey(AppOpsManager.UID_STATE_BACKGROUND,
-            AppOpsManager.OP_FLAG_SELF), new AppOpsManager.NoteOpEvent(time, -1, null));
+                AppOpsManager.OP_FLAG_SELF), new AppOpsManager.NoteOpEvent(time, -1, null));
 
         return new OpEntry(op, AppOpsManager.MODE_ALLOWED, Collections.singletonMap(null,
                 new AppOpsManager.AttributedOpEntry(op, false, accessEvents, null)));
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerAllowlistBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerAllowlistBackendTest.java
index 4f11fb1..6caf762 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerAllowlistBackendTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/PowerAllowlistBackendTest.java
@@ -151,4 +151,14 @@
         assertThat(mPowerAllowlistBackend.isSysAllowlisted(PACKAGE_TWO)).isFalse();
         assertThat(mPowerAllowlistBackend.isAllowlisted(PACKAGE_ONE)).isFalse();
     }
+
+    @Test
+    public void testIsPowerSaveWhitelistExceptIdleApp() throws Exception {
+        doReturn(true).when(mDeviceIdleService)
+                .isPowerSaveWhitelistExceptIdleApp(PACKAGE_ONE);
+
+        mPowerAllowlistBackend.refreshList();
+
+        assertThat(mPowerAllowlistBackend.isAllowlistedExceptIdle(PACKAGE_ONE)).isTrue();
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
index 9e3312a..29549d9 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
@@ -55,6 +55,7 @@
     @Mock
     private ViewGroup mRootView;
     private Uri mImageUri;
+    private ImageView mBackgroundView;
     private LottieAnimationView mAnimationView;
     private IllustrationPreference mPreference;
     private PreferenceViewHolder mViewHolder;
@@ -66,6 +67,7 @@
         MockitoAnnotations.initMocks(this);
 
         mImageUri = new Uri.Builder().build();
+        mBackgroundView = new ImageView(mContext);
         mAnimationView = spy(new LottieAnimationView(mContext));
         mMiddleGroundLayout = new FrameLayout(mContext);
         final FrameLayout illustrationFrame = new FrameLayout(mContext);
@@ -73,6 +75,7 @@
                 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                         ViewGroup.LayoutParams.WRAP_CONTENT));
         doReturn(mMiddleGroundLayout).when(mRootView).findViewById(R.id.middleground_layout);
+        doReturn(mBackgroundView).when(mRootView).findViewById(R.id.background_view);
         doReturn(mAnimationView).when(mRootView).findViewById(R.id.lottie_view);
         doReturn(illustrationFrame).when(mRootView).findViewById(R.id.illustration_frame);
         mViewHolder = spy(PreferenceViewHolder.createInstanceForTests(mRootView));
@@ -155,4 +158,32 @@
 
         verify(mAnimationView).setFailureListener(any());
     }
+
+    @Test
+    public void setMaxHeight_smallerThanRestrictedHeight_matchResult() {
+        final int restrictedHeight =
+                mContext.getResources().getDimensionPixelSize(
+                        R.dimen.settingslib_illustration_height);
+        final int maxHeight = restrictedHeight - 200;
+
+        mPreference.setMaxHeight(maxHeight);
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mBackgroundView.getMaxHeight()).isEqualTo(maxHeight);
+        assertThat(mAnimationView.getMaxHeight()).isEqualTo(maxHeight);
+    }
+
+    @Test
+    public void setMaxHeight_largerThanRestrictedHeight_specificHeight() {
+        final int restrictedHeight =
+                mContext.getResources().getDimensionPixelSize(
+                        R.dimen.settingslib_illustration_height);
+        final int maxHeight = restrictedHeight + 200;
+
+        mPreference.setMaxHeight(maxHeight);
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mBackgroundView.getMaxHeight()).isEqualTo(restrictedHeight);
+        assertThat(mAnimationView.getMaxHeight()).isEqualTo(restrictedHeight);
+    }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index a46d28b..db33c3f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -76,6 +76,8 @@
         Settings.Global.ARE_USER_DISABLED_HDR_FORMATS_ALLOWED,
         Settings.Global.DEVICE_CONFIG_SYNC_DISABLED,
         Settings.Global.POWER_BUTTON_LONG_PRESS,
+        Settings.Global.AUTOMATIC_POWER_SAVE_MODE,
+        Settings.Global.ADVANCED_BATTERY_USAGE_AMOUNT,
         Settings.Global.POWER_BUTTON_LONG_PRESS_DURATION_MS
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 84c5feb..d0448ef 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -141,6 +141,8 @@
                         /* last= */Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT));
         VALIDATORS.put(Global.DISABLE_WINDOW_BLURS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.DEVICE_CONFIG_SYNC_DISABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Global.AUTOMATIC_POWER_SAVE_MODE, ANY_INTEGER_VALIDATOR);
+        VALIDATORS.put(Global.ADVANCED_BATTERY_USAGE_AMOUNT, PERCENTAGE_INTEGER_VALIDATOR);
         VALIDATORS.put(Global.POWER_BUTTON_LONG_PRESS_DURATION_MS, NONE_NEGATIVE_LONG_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 6d7fb02..7aeacdc 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -34,7 +34,9 @@
 import static android.provider.settings.validators.SettingsValidators.TTS_LIST_VALIDATOR;
 
 import android.provider.Settings.Secure;
+import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 
 import java.util.Map;
 
@@ -276,7 +278,7 @@
         VALIDATORS.put(Secure.ACCESSIBILITY_BUTTON_MODE,
                 new InclusiveIntegerRangeValidator(
                         Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR,
-                        Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU));
+                        Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE));
         VALIDATORS.put(Secure.ACCESSIBILITY_FLOATING_MENU_SIZE,
                 new DiscreteValueValidator(new String[] {"0", "1"}));
         VALIDATORS.put(Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
@@ -287,5 +289,32 @@
         VALIDATORS.put(Secure.CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.NOTIFICATION_BUBBLES, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.DEVICE_STATE_ROTATION_LOCK, value -> {
+            if (TextUtils.isEmpty(value)) {
+                return true;
+            }
+            String[] intValues = value.split(":");
+            if (intValues.length % 2 != 0) {
+                return false;
+            }
+            InclusiveIntegerRangeValidator enumValidator =
+                    new InclusiveIntegerRangeValidator(
+                            Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED,
+                            Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED);
+            ArraySet<String> keys = new ArraySet<>();
+            for (int i = 0; i < intValues.length - 1; ) {
+                String entryKey = intValues[i++];
+                String entryValue = intValues[i++];
+                if (!NON_NEGATIVE_INTEGER_VALIDATOR.validate(entryKey)
+                        || !enumValidator.validate(entryValue)) {
+                    return false;
+                }
+                // If the same device state key was specified more than once, this is invalid
+                if (!keys.add(entryKey)) {
+                    return false;
+                }
+            }
+            return true;
+        });
     }
 }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 7db73c6..4a896e7 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -137,7 +137,6 @@
                     Settings.Global.AUTOFILL_LOGGING_LEVEL,
                     Settings.Global.AUTOFILL_MAX_PARTITIONS_SIZE,
                     Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS,
-                    Settings.Global.AUTOMATIC_POWER_SAVE_MODE,
                     Settings.Global.AVERAGE_TIME_TO_DISCHARGE,
                     Settings.Global.BATTERY_CHARGING_STATE_UPDATE_DELAY,
                     Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME,
@@ -592,8 +591,7 @@
                     Settings.Global.INTEGRITY_CHECK_INCLUDES_RULE_PROVIDER,
                     Settings.Global.CACHED_APPS_FREEZER_ENABLED,
                     Settings.Global.APP_INTEGRITY_VERIFICATION_TIMEOUT,
-                    Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
-                    Settings.Global.ADVANCED_BATTERY_USAGE_AMOUNT);
+                    Settings.Global.KEY_CHORD_POWER_VOLUME_UP);
 
     private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS =
              newHashSet(
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d051290..c6cca5a 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -50,6 +50,16 @@
     srcs: ["src/com/android/systemui/EventLogTags.logtags"],
 }
 
+filegroup {
+    name: "ReleaseJavaFiles",
+    srcs: ["src/com/android/systemui/flags/FeatureFlagManager.java"],
+}
+
+filegroup {
+    name: "DebugJavaFiles",
+    srcs: ["src-debug/com/android/systemui/flags/FeatureFlagManager.java"],
+}
+
 android_library {
     name: "SystemUI-core",
     srcs: [
@@ -57,6 +67,12 @@
         "src/**/*.java",
         "src/**/I*.aidl",
     ],
+    product_variables: {
+        debuggable: {
+            srcs: [":DebugJavaFiles"],
+            exclude_srcs: [":ReleaseJavaFiles"],
+        },
+    },
     resource_dirs: [
         "res-product",
         "res-keyguard",
@@ -92,6 +108,7 @@
         "iconloader_base",
         "SystemUI-tags",
         "SystemUI-proto",
+        "monet",
         "dagger2",
         "jsr330",
         "lottie",
@@ -179,6 +196,7 @@
         "mockito-target-extended-minus-junit4",
         "testables",
         "truth-prebuilt",
+        "monet",
         "dagger2",
         "jsr330",
         "WindowManager-Shell",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 9de1c5e..58e3d39 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -183,6 +183,9 @@
     <permission android:name="com.android.systemui.permission.PLUGIN"
             android:protectionLevel="signature" />
 
+    <permission android:name="com.android.systemui.permission.FLAGS"
+                android:protectionLevel="signature" />
+
     <!-- Adding Quick Settings tiles -->
     <uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
 
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 0d18b8d..e509777 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -22,6 +22,9 @@
         },
         {
             "exclude-annotation": "android.platform.test.annotations.Postsubmit"
+        },
+        {
+            "exclude-annotation": "android.platform.test.scenario.annotation.LargeScreenOnly"
         }
       ]
     },
@@ -82,6 +85,9 @@
         },
         {
             "exclude-annotation": "android.platform.helpers.Staging"
+        },
+        {
+            "exclude-annotation": "android.platform.test.scenario.annotation.LargeScreenOnly"
         }
       ]
     }
@@ -101,6 +107,9 @@
         },
         {
             "exclude-annotation": "androidx.test.filters.FlakyTest"
+        },
+        {
+            "exclude-annotation": "android.platform.test.scenario.annotation.LargeScreenOnly"
         }
       ]
     }
@@ -117,5 +126,24 @@
         }
       ]
     }
+  ],
+  "large-screen-postsubmit": [
+      {
+        "name": "PlatformScenarioTests",
+        "options" : [
+          {
+              "include-filter": "android.platform.test.scenario.sysui"
+          },
+          {
+              "include-annotation": "android.platform.test.scenario.annotation.LargeScreenOnly"
+          },
+          {
+              "exclude-annotation": "org.junit.Ignore"
+          },
+          {
+              "exclude-annotation": "androidx.test.filters.FlakyTest"
+          }
+        ]
+      }
   ]
 }
diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml b/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml
new file mode 100644
index 0000000..c6b87d3
--- /dev/null
+++ b/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- The enter animation of the host dialog is a translation of 0px that lasts 500ms so that the -->
+<!-- host dialog is directly visible but the dim background still takes 500ms to fade in. -->
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+  android:fromXDelta="0"
+  android:toXDelta="0"
+  android:duration="500" />
\ No newline at end of file
diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml b/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml
new file mode 100644
index 0000000..a0f441e
--- /dev/null
+++ b/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<alpha
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:interpolator="@android:interpolator/decelerate_cubic"
+    android:duration="150"
+    android:fromAlpha="1.0"
+    android:toAlpha="0.0" />
\ No newline at end of file
diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml
new file mode 100644
index 0000000..ef60a24
--- /dev/null
+++ b/packages/SystemUI/animation/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <item type="id" name="launch_animation_running"/>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/animation/res/values/styles.xml b/packages/SystemUI/animation/res/values/styles.xml
new file mode 100644
index 0000000..ad06c91
--- /dev/null
+++ b/packages/SystemUI/animation/res/values/styles.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+  <style name="HostDialogTheme">
+    <item name="android:windowAnimationStyle">@style/Animation.HostDialog</item>
+    <item name="android:windowIsFloating">false</item>
+    <item name="android:backgroundDimEnabled">true</item>
+    <item name="android:navigationBarColor">@android:color/transparent</item>
+  </style>
+
+  <style name="Animation.HostDialog" parent="@android:style/Animation">
+    <item name="android:windowEnterAnimation">@anim/launch_host_dialog_enter</item>
+    <item name="android:windowExitAnimation">@anim/launch_host_dialog_exit</item>
+  </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index a50efd7..7020603 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -1,24 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.animation
 
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
 import android.app.ActivityManager
 import android.app.ActivityTaskManager
-import android.app.AppGlobals
 import android.app.PendingIntent
 import android.app.TaskInfo
-import android.content.Context
 import android.graphics.Matrix
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffXfermode
 import android.graphics.Rect
 import android.graphics.RectF
-import android.graphics.drawable.GradientDrawable
 import android.os.Looper
 import android.os.RemoteException
 import android.util.Log
-import android.util.MathUtils
 import android.view.IRemoteAnimationFinishedCallback
 import android.view.IRemoteAnimationRunner
 import android.view.RemoteAnimationAdapter
@@ -27,7 +34,6 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
-import android.view.animation.AnimationUtils
 import android.view.animation.PathInterpolator
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
@@ -39,53 +45,23 @@
  * A class that allows activities to be started in a seamless way from a view that is transforming
  * nicely into the starting window.
  */
-class ActivityLaunchAnimator(
-    private val callback: Callback,
-    context: Context
-) {
+class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
     companion object {
-        const val ANIMATION_DURATION = 500L
-        private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
-        private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
-        private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
         private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
         private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
         private const val ANIMATION_DELAY_NAV_FADE_IN =
-                ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
+            LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
         private const val LAUNCH_TIMEOUT = 1000L
 
-        @JvmField val CONTENT_FADE_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
-        private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
         private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
         private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
-
-        private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
-
-        /**
-         * Given the [linearProgress] of a launch animation, return the linear progress of the
-         * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
-         */
-        @JvmStatic
-        fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
-            return MathUtils.constrain(
-                    (linearProgress * ANIMATION_DURATION - delay) / duration,
-                    0.0f,
-                    1.0f
-            )
-        }
     }
 
-    private val packageManager = AppGlobals.getPackageManager()
-
-    /** The interpolator used for the width, height, Y position and corner radius. */
-    private val animationInterpolator = AnimationUtils.loadInterpolator(context,
-            R.interpolator.launch_animation_interpolator_y)
-
-    /** The interpolator used for the X position. */
-    private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
-            R.interpolator.launch_animation_interpolator_x)
-
-    private val cornerRadii = FloatArray(8)
+    /**
+     * The callback of this animator. This should be set before any call to
+     * [start(Pending)IntentWithAnimation].
+     */
+    var callback: Callback? = null
 
     /**
      * Start an intent and animate the opening window. The intent will be started by running
@@ -100,6 +76,10 @@
      * If possible, you should pass the [packageName] of the intent that will be started so that
      * trampoline activity launches will also be animated.
      *
+     * If the device is currently locked, the user will have to unlock it before the intent is
+     * started unless [showOverLockscreen] is true. In that case, the activity will be started
+     * directly over the lockscreen.
+     *
      * This method will throw any exception thrown by [intentStarter].
      */
     @JvmOverloads
@@ -107,25 +87,28 @@
         controller: Controller?,
         animate: Boolean = true,
         packageName: String? = null,
+        showOverLockscreen: Boolean = false,
         intentStarter: (RemoteAnimationAdapter?) -> Int
     ) {
         if (controller == null || !animate) {
-            Log.d(TAG, "Starting intent with no animation")
+            Log.i(TAG, "Starting intent with no animation")
             intentStarter(null)
             controller?.callOnIntentStartedOnMainThread(willAnimate = false)
             return
         }
 
-        Log.d(TAG, "Starting intent with a launch animation")
+        val callback = this.callback ?: throw IllegalStateException(
+            "ActivityLaunchAnimator.callback must be set before using this animator")
         val runner = Runner(controller)
-        val isOnKeyguard = callback.isOnKeyguard()
+        val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
 
-        // Pass the RemoteAnimationAdapter to the intent starter only if we are not on the keyguard.
-        val animationAdapter = if (!isOnKeyguard) {
+        // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the
+        // keyguard with the animation
+        val animationAdapter = if (!hideKeyguardWithAnimation) {
             RemoteAnimationAdapter(
-                    runner,
-                    ANIMATION_DURATION,
-                    ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
+                runner,
+                LaunchAnimator.ANIMATION_DURATION,
+                LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
             )
         } else {
             null
@@ -147,11 +130,13 @@
         // Only animate if the app is not already on top and will be opened, unless we are on the
         // keyguard.
         val willAnimate =
-                launchResult == ActivityManager.START_TASK_TO_FRONT ||
-                        launchResult == ActivityManager.START_SUCCESS ||
-                        (launchResult == ActivityManager.START_DELIVERED_TO_TOP && isOnKeyguard)
+            launchResult == ActivityManager.START_TASK_TO_FRONT ||
+                    launchResult == ActivityManager.START_SUCCESS ||
+                    (launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
+                            hideKeyguardWithAnimation)
 
-        Log.d(TAG, "launchResult=$launchResult willAnimate=$willAnimate isOnKeyguard=$isOnKeyguard")
+        Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " +
+                "hideKeyguardWithAnimation=$hideKeyguardWithAnimation")
         controller.callOnIntentStartedOnMainThread(willAnimate)
 
         // If we expect an animation, post a timeout to cancel it in case the remote animation is
@@ -160,7 +145,7 @@
             runner.postTimeout()
 
             // Hide the keyguard using the launch animation instead of the default unlock animation.
-            if (isOnKeyguard) {
+            if (hideKeyguardWithAnimation) {
                 callback.hideKeyguardWithAnimation(runner)
             }
         }
@@ -229,7 +214,7 @@
      *
      * Note that all callbacks (onXXX methods) are all called on the main thread.
      */
-    interface Controller {
+    interface Controller : LaunchAnimator.Controller {
         companion object {
             /**
              * Return a [Controller] that will animate and expand [view] into the opening window.
@@ -254,53 +239,12 @@
         }
 
         /**
-         * The container in which the view that started the intent will be animating together with
-         * the opening window.
-         *
-         * This will be used to:
-         *  - Get the associated [Context].
-         *  - Compute whether we are expanding fully above the current window.
-         *  - Apply surface transactions in sync with RenderThread.
-         *
-         * This container can be changed to force this [Controller] to animate the expanding view
-         * inside a different location, for instance to ensure correct layering during the
-         * animation.
-         */
-        var launchContainer: ViewGroup
-
-        /**
-         * Return the [State] of the view that will be animated. We will animate from this state to
-         * the final window state.
-         *
-         * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
-         * animation.
-         */
-        fun createAnimatorState(): State
-
-        /**
          * The intent was started. If [willAnimate] is false, nothing else will happen and the
          * animation will not be started.
          */
         fun onIntentStarted(willAnimate: Boolean) {}
 
         /**
-         * The animation started. This is typically used to initialize any additional resource
-         * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
-         * fully above the [root view][getRootView].
-         */
-        fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
-
-        /** The animation made progress and the expandable view [state] should be updated. */
-        fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
-
-        /**
-         * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
-         * called previously. This is typically used to clean up the resources initialized when the
-         * animation was started.
-         */
-        fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
-
-        /**
          * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
          * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
          * before the cancellation.
@@ -308,66 +252,11 @@
         fun onLaunchAnimationCancelled() {}
     }
 
-    /** The state of an expandable view during an [ActivityLaunchAnimator] animation. */
-    open class State(
-        /** The position of the view in screen space coordinates. */
-        var top: Int,
-        var bottom: Int,
-        var left: Int,
-        var right: Int,
-
-        var topCornerRadius: Float = 0f,
-        var bottomCornerRadius: Float = 0f
-    ) {
-        private val startTop = top
-        private val startBottom = bottom
-        private val startLeft = left
-        private val startRight = right
-        private val startWidth = width
-        private val startHeight = height
-        val startCenterX = centerX
-        val startCenterY = centerY
-
-        val width: Int
-            get() = right - left
-
-        val height: Int
-            get() = bottom - top
-
-        open val topChange: Int
-            get() = top - startTop
-
-        open val bottomChange: Int
-            get() = bottom - startBottom
-
-        val leftChange: Int
-            get() = left - startLeft
-
-        val rightChange: Int
-            get() = right - startRight
-
-        val widthRatio: Float
-            get() = width.toFloat() / startWidth
-
-        val heightRatio: Float
-            get() = height.toFloat() / startHeight
-
-        val centerX: Float
-            get() = left + width / 2f
-
-        val centerY: Float
-            get() = top + height / 2f
-
-        /** Whether the expanded view should be visible or hidden. */
-        var visible: Boolean = true
-    }
-
     @VisibleForTesting
     inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
         private val launchContainer = controller.launchContainer
         private val context = launchContainer.context
         private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
-        private var animator: ValueAnimator? = null
 
         private val matrix = Matrix()
         private val invertMatrix = Matrix()
@@ -375,6 +264,7 @@
         private var windowCropF = RectF()
         private var timedOut = false
         private var cancelled = false
+        private var animation: LaunchAnimator.Animation? = null
 
         // A timeout to cancel the remote animation if it is not started within X milliseconds after
         // the intent was started.
@@ -424,13 +314,16 @@
             nonApps: Array<out RemoteAnimationTarget>?,
             iCallback: IRemoteAnimationFinishedCallback?
         ) {
-            Log.d(TAG, "Remote animation started")
+            if (LaunchAnimator.DEBUG) {
+                Log.d(TAG, "Remote animation started")
+            }
+
             val window = apps?.firstOrNull {
                 it.mode == RemoteAnimationTarget.MODE_OPENING
             }
 
             if (window == null) {
-                Log.d(TAG, "Aborting the animation as no window is opening")
+                Log.i(TAG, "Aborting the animation as no window is opening")
                 removeTimeout()
                 iCallback?.invoke()
                 controller.onLaunchAnimationCancelled()
@@ -441,130 +334,63 @@
                 it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
             }
 
-            // Start state.
-            val state = controller.createAnimatorState()
-
-            val startTop = state.top
-            val startBottom = state.bottom
-            val startLeft = state.left
-            val startRight = state.right
-            val startXCenter = (startLeft + startRight) / 2f
-            val startWidth = startRight - startLeft
-
-            val startTopCornerRadius = state.topCornerRadius
-            val startBottomCornerRadius = state.bottomCornerRadius
-
-            // End state.
             val windowBounds = window.screenSpaceBounds
-            val endTop = windowBounds.top
-            val endBottom = windowBounds.bottom
-            val endLeft = windowBounds.left
-            val endRight = windowBounds.right
-            val endXCenter = (endLeft + endRight) / 2f
-            val endWidth = endRight - endLeft
+            val endState = LaunchAnimator.State(
+                top = windowBounds.top,
+                bottom = windowBounds.bottom,
+                left = windowBounds.left,
+                right = windowBounds.right
+            )
+            val callback = this@ActivityLaunchAnimator.callback!!
+            val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
 
-            // TODO(b/184121838): Ensure that we are launching on the same screen.
-            val rootViewLocation = launchContainer.locationOnScreen
-            val isExpandingFullyAbove = endTop <= rootViewLocation[1] &&
-                endBottom >= rootViewLocation[1] + launchContainer.height &&
-                endLeft <= rootViewLocation[0] &&
-                endRight >= rootViewLocation[0] + launchContainer.width
-
-            // TODO(b/184121838): We should somehow get the top and bottom radius of the window.
+            // TODO(b/184121838): We should somehow get the top and bottom radius of the window
+            // instead of recomputing isExpandingFullyAbove here.
+            val isExpandingFullyAbove =
+                launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
             val endRadius = if (isExpandingFullyAbove) {
                 // Most of the time, expanding fully above the root view means expanding in full
                 // screen.
-                ScreenDecorationsUtils.getWindowCornerRadius(context.resources)
+                ScreenDecorationsUtils.getWindowCornerRadius(context)
             } else {
                 // This usually means we are in split screen mode, so 2 out of 4 corners will have
                 // a radius of 0.
                 0f
             }
+            endState.topCornerRadius = endRadius
+            endState.bottomCornerRadius = endRadius
 
-            // We add an extra layer with the same color as the app splash screen background color,
-            // which is usually the same color of the app background. We first fade in this layer
-            // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
-            // launch container and reveal the opening window.
-            val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
-            val windowBackgroundLayer = GradientDrawable().apply {
-                setColor(windowBackgroundColor)
-                alpha = 0
-            }
-
-            // Update state.
-            val animator = ValueAnimator.ofFloat(0f, 1f)
-            this.animator = animator
-            animator.duration = ANIMATION_DURATION
-            animator.interpolator = Interpolators.LINEAR
-
-            val launchContainerOverlay = launchContainer.overlay
-            animator.addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
-                    Log.d(TAG, "Animation started")
+            // We animate the opening window and delegate the view expansion to [this.controller].
+            val delegate = this.controller
+            val controller = object : LaunchAnimator.Controller by delegate {
+                override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
                     callback.setBlursDisabledForAppLaunch(true)
-                    controller.onLaunchAnimationStart(isExpandingFullyAbove)
-
-                    // Add the drawable to the launch container overlay. Overlays always draw
-                    // drawables after views, so we know that it will be drawn above any view added
-                    // by the controller.
-                    launchContainerOverlay.add(windowBackgroundLayer)
+                    delegate.onLaunchAnimationStart(isExpandingFullyAbove)
                 }
 
-                override fun onAnimationEnd(animation: Animator?) {
-                    Log.d(TAG, "Animation ended")
+                override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
                     callback.setBlursDisabledForAppLaunch(false)
                     iCallback?.invoke()
-                    controller.onLaunchAnimationEnd(isExpandingFullyAbove)
-                    launchContainerOverlay.remove(windowBackgroundLayer)
-                }
-            })
-
-            animator.addUpdateListener { animation ->
-                if (cancelled) {
-                    return@addUpdateListener
+                    delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
                 }
 
-                val linearProgress = animation.animatedFraction
-                val progress = animationInterpolator.getInterpolation(linearProgress)
-                val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
-                val xCenter = MathUtils.lerp(startXCenter, endXCenter, xProgress)
-                val halfWidth = lerp(startWidth, endWidth, progress) / 2
-
-                state.top = lerp(startTop, endTop, progress).roundToInt()
-                state.bottom = lerp(startBottom, endBottom, progress).roundToInt()
-                state.left = (xCenter - halfWidth).roundToInt()
-                state.right = (xCenter + halfWidth).roundToInt()
-
-                state.topCornerRadius = MathUtils.lerp(startTopCornerRadius, endRadius, progress)
-                state.bottomCornerRadius =
-                    MathUtils.lerp(startBottomCornerRadius, endRadius, progress)
-
-                // The expanding view can/should be hidden once it is completely coverred by the
-                // windowBackgroundLayer.
-                state.visible =
-                        getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
-
-                applyStateToWindow(window, state)
-                applyStateToWindowBackgroundLayer(windowBackgroundLayer, state, linearProgress)
-                navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
-
-                // If we started expanding the view, we make it 1 pixel smaller on all sides to
-                // avoid artefacts on the corners caused by anti-aliasing of the view background and
-                // the window background layer.
-                if (state.top != startTop && state.left != startLeft &&
-                        state.bottom != startBottom && state.right != startRight) {
-                    state.top += 1
-                    state.left += 1
-                    state.right -= 1
-                    state.bottom -= 1
+                override fun onLaunchAnimationProgress(
+                    state: LaunchAnimator.State,
+                    progress: Float,
+                    linearProgress: Float
+                ) {
+                    applyStateToWindow(window, state)
+                    navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
+                    delegate.onLaunchAnimationProgress(state, progress, linearProgress)
                 }
-                controller.onLaunchAnimationProgress(state, progress, linearProgress)
             }
 
-            animator.start()
+            // We draw a hole when the additional layer is fading out to reveal the opening window.
+            animation = launchAnimator.startAnimation(
+                controller, endState, windowBackgroundColor, drawHole = true)
         }
 
-        private fun applyStateToWindow(window: RemoteAnimationTarget, state: State) {
+        private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
             val screenBounds = window.screenSpaceBounds
             val centerX = (screenBounds.left + screenBounds.right) / 2f
             val centerY = (screenBounds.top + screenBounds.bottom) / 2f
@@ -618,48 +444,13 @@
             transactionApplier.scheduleApply(params)
         }
 
-        private fun applyStateToWindowBackgroundLayer(
-            drawable: GradientDrawable,
-            state: State,
-            linearProgress: Float
-        ) {
-            // Update position.
-            drawable.setBounds(state.left, state.top, state.right, state.bottom)
-
-            // Update radius.
-            cornerRadii[0] = state.topCornerRadius
-            cornerRadii[1] = state.topCornerRadius
-            cornerRadii[2] = state.topCornerRadius
-            cornerRadii[3] = state.topCornerRadius
-            cornerRadii[4] = state.bottomCornerRadius
-            cornerRadii[5] = state.bottomCornerRadius
-            cornerRadii[6] = state.bottomCornerRadius
-            cornerRadii[7] = state.bottomCornerRadius
-            drawable.cornerRadii = cornerRadii
-
-            // We first fade in the background layer to hide the expanding view, then fade it out
-            // with SRC mode to draw a hole punch in the status bar and reveal the opening window.
-            val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
-            if (fadeInProgress < 1) {
-                val alpha = CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(fadeInProgress)
-                drawable.alpha = (alpha * 0xFF).roundToInt()
-                drawable.setXfermode(null)
-            } else {
-                val fadeOutProgress = getProgress(linearProgress,
-                        ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
-                val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
-                drawable.alpha = (alpha * 0xFF).roundToInt()
-                drawable.setXfermode(SRC_MODE)
-            }
-        }
-
         private fun applyStateToNavigationBar(
             navigationBar: RemoteAnimationTarget,
-            state: State,
+            state: LaunchAnimator.State,
             linearProgress: Float
         ) {
-            val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN,
-                    ANIMATION_DURATION_NAV_FADE_OUT)
+            val fadeInProgress = LaunchAnimator.getProgress(linearProgress,
+                ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
 
             val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
             if (fadeInProgress > 0) {
@@ -668,13 +459,13 @@
                     0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
                 windowCrop.set(state.left, 0, state.right, state.height)
                 params
-                        .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
-                        .withMatrix(matrix)
-                        .withWindowCrop(windowCrop)
-                        .withVisibility(true)
+                    .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
+                    .withMatrix(matrix)
+                    .withWindowCrop(windowCrop)
+                    .withVisibility(true)
             } else {
-                val fadeOutProgress = getProgress(linearProgress, 0,
-                        ANIMATION_DURATION_NAV_FADE_OUT)
+                val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0,
+                    ANIMATION_DURATION_NAV_FADE_OUT)
                 params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
             }
 
@@ -686,7 +477,7 @@
                 return
             }
 
-            Log.d(TAG, "Remote animation timed out")
+            Log.i(TAG, "Remote animation timed out")
             timedOut = true
             controller.onLaunchAnimationCancelled()
         }
@@ -696,11 +487,11 @@
                 return
             }
 
-            Log.d(TAG, "Remote animation was cancelled")
+            Log.i(TAG, "Remote animation was cancelled")
             cancelled = true
             removeTimeout()
             context.mainExecutor.execute {
-                animator?.cancel()
+                animation?.cancel()
                 controller.onLaunchAnimationCancelled()
             }
         }
@@ -712,9 +503,5 @@
                 e.printStackTrace()
             }
         }
-
-        private fun lerp(start: Int, stop: Int, amount: Float): Float {
-            return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount)
-        }
     }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
index d4be253..258ca6b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.animation
 
 /**
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
new file mode 100644
index 0000000..c2b3608
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation
+
+import android.app.Dialog
+import android.content.Context
+import android.graphics.Color
+import android.os.Looper
+import android.util.Log
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.view.WindowManager
+import android.widget.FrameLayout
+
+private const val TAG = "DialogLaunchAnimator"
+
+/**
+ * A class that allows dialogs to be started in a seamless way from a view that is transforming
+ * nicely into the starting dialog.
+ *
+ * Important: Don't forget to call [DialogLaunchAnimator.onDozeAmountChanged] when the doze amount
+ * changes to gracefully handle dialogs fading out when the device is dozing.
+ */
+class DialogLaunchAnimator(
+    private val context: Context,
+    private val launchAnimator: LaunchAnimator,
+    private val hostDialogProvider: HostDialogProvider
+) {
+    private companion object {
+        private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
+    }
+
+    // TODO(b/201264644): Remove this set.
+    private val currentAnimations = hashSetOf<DialogLaunchAnimation>()
+
+    /**
+     * Show [dialog] by expanding it from [view].
+     *
+     * Caveats: When calling this function, the dialog content view will actually be stolen and
+     * attached to a different dialog (and thus a different window) which means that the actual
+     * dialog window will never be drawn. Moreover, unless [dialog] is a [ListenableDialog], you
+     * must call dismiss(), hide() and show() on the [Dialog] returned by this function to actually
+     * dismiss, hide or show the dialog.
+     */
+    fun showFromView(dialog: Dialog, view: View): Dialog {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw IllegalStateException(
+                "showFromView must be called from the main thread and dialog must be created in " +
+                    "the main thread")
+        }
+
+        // Make sure we don't run the launch animation from the same view twice at the same time.
+        if (view.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
+            Log.e(TAG, "Not running dialog launch animation as there is already one running")
+            dialog.show()
+            return dialog
+        }
+
+        view.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
+
+        val launchAnimation = DialogLaunchAnimation(
+            context, launchAnimator, hostDialogProvider, view,
+            onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog)
+        val hostDialog = launchAnimation.hostDialog
+        currentAnimations.add(launchAnimation)
+
+        // If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the
+        // host dialog.
+        if (dialog is ListenableDialog) {
+            dialog.addListener(object : DialogListener {
+                override fun onDismiss() {
+                    dialog.removeListener(this)
+                    hostDialog.dismiss()
+                }
+
+                override fun onHide() {
+                    if (launchAnimation.ignoreNextCallToHide) {
+                        launchAnimation.ignoreNextCallToHide = false
+                        return
+                    }
+
+                    hostDialog.hide()
+                }
+
+                override fun onShow() {
+                    hostDialog.show()
+
+                    // We don't actually want to show the original dialog, so hide it.
+                    launchAnimation.ignoreNextCallToHide = true
+                    dialog.hide()
+                }
+            })
+        }
+
+        launchAnimation.start()
+        return hostDialog
+    }
+
+    /** Notify the current doze amount, to ensure that dialogs fade out when dozing. */
+    // TODO(b/193634619): Replace this by some mandatory constructor parameter to make sure that we
+    // don't forget to call this when the doze amount changes.
+    fun onDozeAmountChanged(amount: Float) {
+        currentAnimations.forEach { it.onDozeAmountChanged(amount) }
+    }
+
+    /**
+     * Ensure that all dialogs currently shown won't animate into their touch surface when
+     * dismissed.
+     *
+     * This is a temporary API meant to be called right before we both dismiss a dialog and start
+     * an activity, which currently does not look good if we animate the dialog into the touch
+     * surface at the same time as the activity starts.
+     *
+     * TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
+     */
+    fun disableAllCurrentDialogsExitAnimations() {
+        currentAnimations.forEach { it.exitAnimationDisabled = true }
+    }
+}
+
+interface HostDialogProvider {
+    /**
+     * Create a host dialog that will be used to host a launch animation. This host dialog must:
+     *   1. call [onCreateCallback] in its onCreate() method, e.g. right after calling
+     *      super.onCreate().
+     *   2. call [dismissOverride] instead of doing any dismissing logic. The actual dismissing
+     *      logic should instead be done inside the lambda passed to [dismissOverride], which will
+     *      be called after the exit animation.
+     *
+     * See SystemUIHostDialogProvider for an example of implementation.
+     */
+    fun createHostDialog(
+        context: Context,
+        theme: Int,
+        onCreateCallback: () -> Unit,
+        dismissOverride: (() -> Unit) -> Unit
+    ): Dialog
+}
+
+/** A dialog to/from which we can add/remove listeners. */
+interface ListenableDialog {
+    /** Add [listener] to the listeners. */
+    fun addListener(listener: DialogListener)
+
+    /** Remove [listener] from the listeners. */
+    fun removeListener(listener: DialogListener)
+}
+
+interface DialogListener {
+    /** Called when this dialog dismiss() is called. */
+    fun onDismiss()
+
+    /** Called when this dialog hide() is called. */
+    fun onHide()
+
+    /** Called when this dialog show() is called. */
+    fun onShow()
+}
+
+private class DialogLaunchAnimation(
+    private val context: Context,
+    private val launchAnimator: LaunchAnimator,
+    hostDialogProvider: HostDialogProvider,
+
+    /** The view that triggered the dialog after being tapped. */
+    private val touchSurface: View,
+
+    /**
+     * A callback that will be called with this [DialogLaunchAnimation] after the dialog was
+     * dismissed and the exit animation is done.
+     */
+    private val onDialogDismissed: (DialogLaunchAnimation) -> Unit,
+
+    /** The original dialog whose content will be shown and animate in/out in [hostDialog]. */
+    private val originalDialog: Dialog
+) {
+    /**
+     * The fullscreen dialog to which we will add the content view [originalDialogView] of
+     * [originalDialog].
+     */
+    val hostDialog = hostDialogProvider.createHostDialog(
+        context, R.style.HostDialogTheme, this::onHostDialogCreated, this::onHostDialogDismissed)
+
+    /** The root content view of [hostDialog]. */
+    private val hostDialogRoot = FrameLayout(context)
+
+    /**
+     * The content view of [originalDialog], which will be stolen from that dialog and added to
+     * [hostDialogRoot].
+     */
+    private var originalDialogView: View? = null
+
+    /**
+     * The background color of [originalDialogView], taking into consideration the [originalDialog]
+     * window background color.
+     */
+    private var originalDialogBackgroundColor = Color.BLACK
+
+    /**
+     * Whether we are currently launching/showing the dialog by animating it from [touchSurface].
+     */
+    private var isLaunching = true
+
+    /** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */
+    private var isDismissing = false
+
+    private var dismissRequested = false
+    private var drawHostDialog = false
+    var ignoreNextCallToHide = false
+    var exitAnimationDisabled = false
+
+    fun start() {
+        // Show the host (fullscreen) dialog, to which we will add the stolen dialog view.
+        hostDialog.show()
+
+        // Steal the dialog view. We do that by showing it but preventing it from drawing, then
+        // hiding it as soon as its content is available.
+        stealOriginalDialogContentView(then = this::showDialogFromView)
+    }
+
+    private fun onHostDialogCreated() {
+        // Make the dialog fullscreen with a transparent background.
+        hostDialog.setContentView(
+            hostDialogRoot,
+            ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT
+            )
+        )
+
+        val window = hostDialog.window
+            ?: throw IllegalStateException("There is no window associated to the host dialog")
+        window.setBackgroundDrawableResource(android.R.color.transparent)
+        window.setLayout(
+            WindowManager.LayoutParams.MATCH_PARENT,
+            WindowManager.LayoutParams.MATCH_PARENT
+        )
+
+        // Prevent the host dialog from drawing until the animation starts.
+        hostDialogRoot.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    if (drawHostDialog) {
+                        hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this)
+                        return true
+                    }
+
+                    return false
+                }
+            }
+        )
+    }
+
+    /** Get the content view of [originalDialog] and pass it to [then]. */
+    private fun stealOriginalDialogContentView(then: (View) -> Unit) {
+        // The original dialog content view will be attached to android.R.id.content when the dialog
+        // is shown, so we show the dialog and add an observer to get the view but also prevents the
+        // original dialog from being drawn.
+        val androidContent = originalDialog.findViewById<ViewGroup>(android.R.id.content)
+            ?: throw IllegalStateException("Dialog does not have any android.R.id.content view")
+
+        androidContent.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    if (androidContent.childCount == 1) {
+                        androidContent.viewTreeObserver.removeOnPreDrawListener(this)
+
+                        // Hide the animated dialog. Because of the dialog listener set up
+                        // earlier, this would also hide the host dialog, but in this case we
+                        // need to keep the host dialog visible.
+                        ignoreNextCallToHide = true
+                        originalDialog.hide()
+
+                        then(androidContent.getChildAt(0))
+                        return false
+                    }
+
+                    // Never draw the original dialog content.
+                    return false
+                }
+            })
+        originalDialog.show()
+    }
+
+    private fun showDialogFromView(dialogView: View) {
+        // Save the dialog view for later as we will need it for the close animation.
+        this.originalDialogView = dialogView
+
+        // Close the dialog when clicking outside of it.
+        hostDialogRoot.setOnClickListener { hostDialog.dismiss() }
+        dialogView.isClickable = true
+
+        // Set the background of the window dialog to the dialog itself.
+        // TODO(b/193634619): Support dialog windows without background.
+        // TODO(b/193634619): Support dialog whose background comes from the content view instead of
+        // the window.
+        val typedArray =
+            originalDialog.context.obtainStyledAttributes(com.android.internal.R.styleable.Window)
+        val backgroundRes =
+            typedArray.getResourceId(com.android.internal.R.styleable.Window_windowBackground, 0)
+        typedArray.recycle()
+        if (backgroundRes == 0) {
+            throw IllegalStateException("Dialogs with no backgrounds on window are not supported")
+        }
+
+        dialogView.setBackgroundResource(backgroundRes)
+        originalDialogBackgroundColor =
+            GhostedViewLaunchAnimatorController.findGradientDrawable(dialogView.background!!)
+                ?.color
+                ?.defaultColor ?: Color.BLACK
+
+        // Add the dialog view to the host (fullscreen) dialog and make it invisible to make sure
+        // it's not drawn yet.
+        (dialogView.parent as? ViewGroup)?.removeView(dialogView)
+        hostDialogRoot.addView(
+            dialogView,
+
+            // We give it the size of its original dialog window.
+            FrameLayout.LayoutParams(
+                originalDialog.window.attributes.width,
+                originalDialog.window.attributes.height,
+                Gravity.CENTER
+            )
+        )
+        dialogView.visibility = View.INVISIBLE
+
+        // Start the animation when the dialog is laid out in the center of the host dialog.
+        dialogView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
+            override fun onLayoutChange(
+                view: View,
+                left: Int,
+                top: Int,
+                right: Int,
+                bottom: Int,
+                oldLeft: Int,
+                oldTop: Int,
+                oldRight: Int,
+                oldBottom: Int
+            ) {
+                dialogView.removeOnLayoutChangeListener(this)
+                startAnimation(
+                    isLaunching = true,
+                    onLaunchAnimationStart = { drawHostDialog = true },
+                    onLaunchAnimationEnd = {
+                        touchSurface.setTag(R.id.launch_animation_running, null)
+
+                        // We hide the touch surface when the dialog is showing. We will make this
+                        // view visible again when dismissing the dialog.
+                        // TODO(b/193634619): Provide an easy way for views to check if they should
+                        // be hidden because of a dialog launch so that they don't override this
+                        // visibility when updating/refreshing itself.
+                        touchSurface.visibility = View.INVISIBLE
+
+                        isLaunching = false
+
+                        // dismiss was called during the animation, dismiss again now to actually
+                        // dismiss.
+                        if (dismissRequested) {
+                            hostDialog.dismiss()
+                        }
+                    }
+                )
+            }
+        })
+    }
+
+    private fun onHostDialogDismissed(actualDismiss: () -> Unit) {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            context.mainExecutor.execute { onHostDialogDismissed(actualDismiss) }
+            return
+        }
+
+        // TODO(b/193634619): Support interrupting the launch animation in the middle.
+        if (isLaunching) {
+            dismissRequested = true
+            return
+        }
+
+        if (isDismissing) {
+            return
+        }
+
+        isDismissing = true
+        hideDialogIntoView { instantDismiss: Boolean ->
+            if (instantDismiss) {
+                originalDialog.hide()
+                hostDialog.hide()
+            }
+
+            originalDialog.dismiss()
+            actualDismiss()
+        }
+    }
+
+    /**
+     * Hide the dialog into the touch surface and call [dismissDialogs] when the animation is done
+     * (passing instantDismiss=true) or if it's skipped (passing instantDismiss=false) to actually
+     * dismiss the dialogs.
+     */
+    private fun hideDialogIntoView(dismissDialogs: (Boolean) -> Unit) {
+        if (!shouldAnimateDialogIntoView()) {
+            Log.i(TAG, "Skipping animation of dialog into the touch surface")
+
+            // If the view is invisible it's probably because of us, so we make it visible again.
+            if (touchSurface.visibility == View.INVISIBLE) {
+                touchSurface.visibility = View.VISIBLE
+            }
+
+            dismissDialogs(false /* instantDismiss */)
+            onDialogDismissed(this@DialogLaunchAnimation)
+            return
+        }
+
+        startAnimation(
+            isLaunching = false,
+            onLaunchAnimationStart = {
+                // Remove the dim background as soon as we start the animation.
+                hostDialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+            },
+            onLaunchAnimationEnd = {
+                touchSurface.visibility = View.VISIBLE
+                originalDialogView!!.visibility = View.INVISIBLE
+                dismissDialogs(true /* instantDismiss */)
+                onDialogDismissed(this@DialogLaunchAnimation)
+            }
+        )
+    }
+
+    private fun startAnimation(
+        isLaunching: Boolean,
+        onLaunchAnimationStart: () -> Unit = {},
+        onLaunchAnimationEnd: () -> Unit = {}
+    ) {
+        val dialogView = this.originalDialogView!!
+
+        // Create 2 ghost controllers to animate both the dialog and the touch surface in the host
+        // dialog.
+        val startView = if (isLaunching) touchSurface else dialogView
+        val endView = if (isLaunching) dialogView else touchSurface
+        val startViewController = GhostedViewLaunchAnimatorController(startView)
+        val endViewController = GhostedViewLaunchAnimatorController(endView)
+        startViewController.launchContainer = hostDialogRoot
+        endViewController.launchContainer = hostDialogRoot
+
+        val endState = endViewController.createAnimatorState()
+        val controller = object : LaunchAnimator.Controller {
+            override var launchContainer: ViewGroup
+                get() = startViewController.launchContainer
+                set(value) {
+                    startViewController.launchContainer = value
+                    endViewController.launchContainer = value
+                }
+
+            override fun createAnimatorState(): LaunchAnimator.State {
+                return startViewController.createAnimatorState()
+            }
+
+            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+                startViewController.onLaunchAnimationStart(isExpandingFullyAbove)
+                endViewController.onLaunchAnimationStart(isExpandingFullyAbove)
+
+                onLaunchAnimationStart()
+            }
+
+            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                startViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
+                endViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
+
+                onLaunchAnimationEnd()
+            }
+
+            override fun onLaunchAnimationProgress(
+                state: LaunchAnimator.State,
+                progress: Float,
+                linearProgress: Float
+            ) {
+                startViewController.onLaunchAnimationProgress(state, progress, linearProgress)
+
+                // The end view is visible only iff the starting view is not visible.
+                state.visible = !state.visible
+                endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
+
+                // If the dialog content is complex, its dimension might change during the launch
+                // animation. The animation end position might also change during the exit
+                // animation, for instance when locking the phone when the dialog is open. Therefore
+                // we update the end state to the new position/size. Usually the dialog dimension or
+                // position will change in the early frames, so changing the end state shouldn't
+                // really be noticeable.
+                endViewController.fillGhostedViewState(endState)
+            }
+        }
+
+        launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
+    }
+
+    private fun shouldAnimateDialogIntoView(): Boolean {
+        if (exitAnimationDisabled) {
+            return false
+        }
+
+        // The touch surface should be invisible by now, if it's not then something else changed its
+        // visibility and we probably don't want to run the animation.
+        if (touchSurface.visibility != View.INVISIBLE) {
+            return false
+        }
+
+        // If the touch surface is not attached or one of its ancestors is not visible, then we
+        // don't run the animation either.
+        if (!touchSurface.isAttachedToWindow) {
+            return false
+        }
+
+        return (touchSurface.parent as? View)?.isShown ?: true
+    }
+
+    internal fun onDozeAmountChanged(amount: Float) {
+        val alpha = Interpolators.ALPHA_OUT.getInterpolation(1 - amount)
+        val decorView = this.hostDialog.window?.decorView ?: return
+        if (decorView.hasOverlappingRendering() && alpha > 0.0f &&
+            alpha < 1.0f && decorView.layerType != View.LAYER_TYPE_HARDWARE) {
+            decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+        }
+        decorView.alpha = alpha
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index b4ffb3f..3ccf5e4 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -1,7 +1,24 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.animation
 
 import android.graphics.Canvas
 import android.graphics.ColorFilter
+import android.graphics.Insets
 import android.graphics.Matrix
 import android.graphics.PixelFormat
 import android.graphics.Rect
@@ -42,6 +59,7 @@
     override var launchContainer = ghostedView.rootView as ViewGroup
     private val launchContainerOverlay: ViewGroupOverlay
         get() = launchContainer.overlay
+    private val launchContainerLocation = IntArray(2)
 
     /** The ghost view that is drawn and animated instead of the ghosted view. */
     private var ghostView: GhostView? = null
@@ -59,8 +77,12 @@
      * [backgroundView].
      */
     private var backgroundDrawable: WrappedDrawable? = null
+    private val backgroundInsets by lazy { getBackground()?.opticalInsets ?: Insets.NONE }
     private var startBackgroundAlpha: Int = 0xFF
 
+    private val ghostedViewLocation = IntArray(2)
+    private val ghostedViewState = LaunchAnimator.State()
+
     /**
      * Return the background of the [ghostedView]. This background will be used to draw the
      * background of the background view that is expanding up to the final animation position. This
@@ -103,16 +125,24 @@
         return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
     }
 
-    override fun createAnimatorState(): ActivityLaunchAnimator.State {
-        val location = ghostedView.locationOnScreen
-        return ActivityLaunchAnimator.State(
-            top = location[1],
-            bottom = location[1] + ghostedView.height,
-            left = location[0],
-            right = location[0] + ghostedView.width,
+    override fun createAnimatorState(): LaunchAnimator.State {
+        val state = LaunchAnimator.State(
             topCornerRadius = getCurrentTopCornerRadius(),
             bottomCornerRadius = getCurrentBottomCornerRadius()
         )
+        fillGhostedViewState(state)
+        return state
+    }
+
+    fun fillGhostedViewState(state: LaunchAnimator.State) {
+        // For the animation we are interested in the area that has a non transparent background,
+        // so we have to take the optical insets into account.
+        ghostedView.getLocationOnScreen(ghostedViewLocation)
+        val insets = backgroundInsets
+        state.top = ghostedViewLocation[1] + insets.top
+        state.bottom = ghostedViewLocation[1] + ghostedView.height - insets.bottom
+        state.left = ghostedViewLocation[0] + insets.left
+        state.right = ghostedViewLocation[0] + ghostedView.width - insets.right
     }
 
     override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -144,7 +174,7 @@
     }
 
     override fun onLaunchAnimationProgress(
-        state: ActivityLaunchAnimator.State,
+        state: LaunchAnimator.State,
         progress: Float,
         linearProgress: Float
     ) {
@@ -162,19 +192,52 @@
             return
         }
 
-        val scale = min(state.widthRatio, state.heightRatio)
-        ghostViewMatrix.setValues(initialGhostViewMatrixValues)
-        ghostViewMatrix.postScale(scale, scale, state.startCenterX, state.startCenterY)
+        // The ghost and backgrounds views were made invisible earlier. That can for instance happen
+        // when animating a dialog into a view.
+        if (ghostView.visibility == View.INVISIBLE) {
+            ghostView.visibility = View.VISIBLE
+            backgroundView.visibility = View.VISIBLE
+        }
+
+        fillGhostedViewState(ghostedViewState)
+        val leftChange = state.left - ghostedViewState.left
+        val rightChange = state.right - ghostedViewState.right
+        val topChange = state.top - ghostedViewState.top
+        val bottomChange = state.bottom - ghostedViewState.bottom
+
+        val widthRatio = state.width.toFloat() / ghostedViewState.width
+        val heightRatio = state.height.toFloat() / ghostedViewState.height
+        val scale = min(widthRatio, heightRatio)
+
+        if (ghostedView.parent is ViewGroup) {
+            // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted
+            // view is still attached to a ViewGroup, otherwise calculateMatrix will throw.
+            GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix)
+        }
+
+        launchContainer.getLocationOnScreen(launchContainerLocation)
+        ghostViewMatrix.postScale(
+            scale, scale,
+            ghostedViewState.centerX - launchContainerLocation[0],
+            ghostedViewState.centerY - launchContainerLocation[1]
+        )
         ghostViewMatrix.postTranslate(
-                (state.leftChange + state.rightChange) / 2f,
-                (state.topChange + state.bottomChange) / 2f
+                (leftChange + rightChange) / 2f,
+                (topChange + bottomChange) / 2f
         )
         ghostView.animationMatrix = ghostViewMatrix
 
-        backgroundView.top = state.top
-        backgroundView.bottom = state.bottom
-        backgroundView.left = state.left
-        backgroundView.right = state.right
+        // We need to take into account the background insets for the background position.
+        val insets = backgroundInsets
+        val topWithInsets = state.top - insets.top
+        val leftWithInsets = state.left - insets.left
+        val rightWithInsets = state.right + insets.right
+        val bottomWithInsets = state.bottom + insets.bottom
+
+        backgroundView.top = topWithInsets - launchContainerLocation[1]
+        backgroundView.bottom = bottomWithInsets - launchContainerLocation[1]
+        backgroundView.left = leftWithInsets - launchContainerLocation[0]
+        backgroundView.right = rightWithInsets - launchContainerLocation[0]
 
         val backgroundDrawable = backgroundDrawable!!
         backgroundDrawable.wrapped?.let {
@@ -207,7 +270,7 @@
          * [drawable] is a [LayerDrawable], this will return the first layer that is a
          * [GradientDrawable].
          */
-        private fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
+        fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
             if (drawable is GradientDrawable) {
                 return drawable
             }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
index 659b9fe..8063483 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.animation;
 
+import android.graphics.Path;
 import android.util.MathUtils;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.AccelerateInterpolator;
@@ -29,7 +30,97 @@
  * Utility class to receive interpolators from
  */
 public class Interpolators {
-    public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+
+    /*
+     * ============================================================================================
+     * Emphasized interpolators.
+     * ============================================================================================
+     */
+
+    /**
+     * The default emphasized interpolator. Used for hero / emphasized movement of content.
+     */
+    public static final Interpolator EMPHASIZED = createEmphasizedInterpolator();
+
+    /**
+     * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
+     * is disappearing e.g. when moving off screen.
+     */
+    public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
+            0.3f, 0f, 0.8f, 0.15f);
+
+    /**
+     * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
+     * is appearing e.g. when coming from off screen
+     */
+    public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
+            0.05f, 0.7f, 0.1f, 1f);
+
+
+    /*
+     * ============================================================================================
+     * Standard interpolators.
+     * ============================================================================================
+     */
+
+    /**
+     * The standard interpolator that should be used on every normal animation
+     */
+    public static final Interpolator STANDARD = new PathInterpolator(
+            0.2f, 0f, 0f, 1f);
+
+    /**
+     * The standard accelerating interpolator that should be used on every regular movement of
+     * content that is disappearing e.g. when moving off screen.
+     */
+    public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(
+            0.3f, 0f, 1f, 1f);
+
+    /**
+     * The standard decelerating interpolator that should be used on every regular movement of
+     * content that is appearing e.g. when coming from off screen.
+     */
+    public static final Interpolator STANDARD_DECELERATE = new PathInterpolator(
+            0f, 0f, 0f, 1f);
+
+    /*
+     * ============================================================================================
+     * Legacy
+     * ============================================================================================
+     */
+
+    /**
+     * The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN.
+     */
+    public static final Interpolator LEGACY = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+
+    /**
+     * The default legacy accelerating interpolator as defined in Material 1.
+     * Also known as FAST_OUT_LINEAR_IN.
+     */
+    public static final Interpolator LEGACY_ACCELERATE = new PathInterpolator(0.4f, 0f, 1f, 1f);
+
+    /**
+     * The default legacy decelerating interpolator as defined in Material 1.
+     * Also known as LINEAR_OUT_SLOW_IN.
+     */
+    public static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f);
+
+    /**
+     * Linear interpolator. Often used if the interpolator is for different properties who need
+     * different interpolations.
+     */
+    public static final Interpolator LINEAR = new LinearInterpolator();
+
+    /*
+    * ============================================================================================
+    * Custom interpolators
+    * ============================================================================================
+    */
+
+    public static final Interpolator FAST_OUT_SLOW_IN = LEGACY;
+    public static final Interpolator FAST_OUT_LINEAR_IN = LEGACY_ACCELERATE;
+    public static final Interpolator LINEAR_OUT_SLOW_IN = LEGACY_DECELERATE;
 
     /**
      * Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t
@@ -37,12 +128,9 @@
      */
     public static final Interpolator FAST_OUT_SLOW_IN_REVERSE =
             new PathInterpolator(0.8f, 0f, 0.6f, 1f);
-    public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
-    public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
     public static final Interpolator SLOW_OUT_LINEAR_IN = new PathInterpolator(0.8f, 0f, 1f, 1f);
     public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
     public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
-    public static final Interpolator LINEAR = new LinearInterpolator();
     public static final Interpolator ACCELERATE = new AccelerateInterpolator();
     public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator();
     public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
@@ -72,6 +160,12 @@
     public static final Interpolator TOUCH_RESPONSE_REVERSE =
             new PathInterpolator(0.9f, 0f, 0.7f, 1f);
 
+    /*
+     * ============================================================================================
+     * Functions / Utilities
+     * ============================================================================================
+     */
+
     /**
      * Calculate the amount of overshoot using an exponential falloff function with desired
      * properties, where the overshoot smoothly transitions at the 1.0f boundary into the
@@ -103,23 +197,13 @@
         return MathUtils.max(0.0f, (float) (1.0f - Math.exp(-4 * progress)));
     }
 
-    /**
-     * Interpolate alpha for notifications background scrim during shade expansion.
-     * @param fraction Shade expansion fraction
-     * @param forNotification If we want the alpha of the notification shade or the scrim.
-     */
-    public static float getNotificationScrimAlpha(float fraction, boolean forNotification) {
-        if (forNotification) {
-            fraction = MathUtils.constrainedMap(0f, 1f, 0.3f, 1f, fraction);
-        } else {
-            fraction = MathUtils.constrainedMap(0f, 1f, 0f, 0.5f, fraction);
-        }
-        fraction = fraction * 1.2f - 0.2f;
-        if (fraction <= 0) {
-            return 0;
-        } else {
-            final float oneMinusFrac = 1f - fraction;
-            return (float) (1f - 0.5f * (1f - Math.cos(3.14159f * oneMinusFrac * oneMinusFrac)));
-        }
+    // Create the default emphasized interpolator
+    private static PathInterpolator createEmphasizedInterpolator() {
+        Path path = new Path();
+        // Doing the same as fast_out_extra_slow_in
+        path.moveTo(0f, 0f);
+        path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f);
+        path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f);
+        return new PathInterpolator(path);
     }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
new file mode 100644
index 0000000..3bf6c5e
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.drawable.GradientDrawable
+import android.util.Log
+import android.util.MathUtils
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.AnimationUtils
+import android.view.animation.PathInterpolator
+import kotlin.math.roundToInt
+
+private const val TAG = "LaunchAnimator"
+
+/** A base class to animate a window launch (activity or dialog) from a view . */
+class LaunchAnimator @JvmOverloads constructor(
+    context: Context,
+    private val isForTesting: Boolean = false
+) {
+    companion object {
+        internal const val DEBUG = false
+        const val ANIMATION_DURATION = 500L
+        private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
+        private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
+        private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
+
+        private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
+        private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
+
+        /**
+         * Given the [linearProgress] of a launch animation, return the linear progress of the
+         * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
+         */
+        @JvmStatic
+        fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
+            return MathUtils.constrain(
+                (linearProgress * ANIMATION_DURATION - delay) / duration,
+                0.0f,
+                1.0f
+            )
+        }
+    }
+
+    /** The interpolator used for the width, height, Y position and corner radius. */
+    private val animationInterpolator = AnimationUtils.loadInterpolator(context,
+        R.interpolator.launch_animation_interpolator_y)
+
+    /** The interpolator used for the X position. */
+    private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
+        R.interpolator.launch_animation_interpolator_x)
+
+    private val launchContainerLocation = IntArray(2)
+    private val cornerRadii = FloatArray(8)
+
+    /**
+     * A controller that takes care of applying the animation to an expanding view.
+     *
+     * Note that all callbacks (onXXX methods) are all called on the main thread.
+     */
+    interface Controller {
+        /**
+         * The container in which the view that started the animation will be animating together
+         * with the opening window.
+         *
+         * This will be used to:
+         *  - Get the associated [Context].
+         *  - Compute whether we are expanding fully above the launch container.
+         *  - Apply surface transactions in sync with RenderThread when animating an activity
+         *    launch.
+         *
+         * This container can be changed to force this [Controller] to animate the expanding view
+         * inside a different location, for instance to ensure correct layering during the
+         * animation.
+         */
+        var launchContainer: ViewGroup
+
+        /**
+         * Return the [State] of the view that will be animated. We will animate from this state to
+         * the final window state.
+         *
+         * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
+         * animation.
+         */
+        fun createAnimatorState(): State
+
+        /**
+         * The animation started. This is typically used to initialize any additional resource
+         * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
+         * fully above the [launchContainer].
+         */
+        fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
+
+        /** The animation made progress and the expandable view [state] should be updated. */
+        fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
+
+        /**
+         * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
+         * called previously. This is typically used to clean up the resources initialized when the
+         * animation was started.
+         */
+        fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
+    }
+
+    /** The state of an expandable view during a [LaunchAnimator] animation. */
+    open class State(
+        /** The position of the view in screen space coordinates. */
+        var top: Int = 0,
+        var bottom: Int = 0,
+        var left: Int = 0,
+        var right: Int = 0,
+
+        var topCornerRadius: Float = 0f,
+        var bottomCornerRadius: Float = 0f
+    ) {
+        private val startTop = top
+
+        val width: Int
+            get() = right - left
+
+        val height: Int
+            get() = bottom - top
+
+        open val topChange: Int
+            get() = top - startTop
+
+        val centerX: Float
+            get() = left + width / 2f
+
+        val centerY: Float
+            get() = top + height / 2f
+
+        /** Whether the expanding view should be visible or hidden. */
+        var visible: Boolean = true
+    }
+
+    interface Animation {
+        /** Cancel the animation. */
+        fun cancel()
+    }
+
+    /**
+     * Start a launch animation controlled by [controller] towards [endState]. An intermediary
+     * layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and
+     * should be the same background color as the opening (or closing) window. If [drawHole] is
+     * true, then this intermediary layer will be drawn with SRC blending mode while it fades out.
+     *
+     * TODO(b/184121838): Remove [drawHole] and instead make the StatusBar draw this hole instead.
+     */
+    fun startAnimation(
+        controller: Controller,
+        endState: State,
+        windowBackgroundColor: Int,
+        drawHole: Boolean = false
+    ): Animation {
+        val state = controller.createAnimatorState()
+
+        // Start state.
+        val startTop = state.top
+        val startBottom = state.bottom
+        val startLeft = state.left
+        val startRight = state.right
+        val startCenterX = (startLeft + startRight) / 2f
+        val startWidth = startRight - startLeft
+        val startTopCornerRadius = state.topCornerRadius
+        val startBottomCornerRadius = state.bottomCornerRadius
+
+        // End state.
+        var endTop = endState.top
+        var endBottom = endState.bottom
+        var endLeft = endState.left
+        var endRight = endState.right
+        var endCenterX = (endLeft + endRight) / 2f
+        var endWidth = endRight - endLeft
+        val endTopCornerRadius = endState.topCornerRadius
+        val endBottomCornerRadius = endState.bottomCornerRadius
+
+        fun maybeUpdateEndState() {
+            if (endTop != endState.top || endBottom != endState.bottom ||
+                endLeft != endState.left || endRight != endState.right) {
+                endTop = endState.top
+                endBottom = endState.bottom
+                endLeft = endState.left
+                endRight = endState.right
+                endCenterX = (endLeft + endRight) / 2f
+                endWidth = endRight - endLeft
+            }
+        }
+
+        val launchContainer = controller.launchContainer
+        val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState)
+
+        // We add an extra layer with the same color as the dialog/app splash screen background
+        // color, which is usually the same color of the app background. We first fade in this layer
+        // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
+        // launch container and reveal the opening window.
+        val windowBackgroundLayer = GradientDrawable().apply {
+            setColor(windowBackgroundColor)
+            alpha = 0
+        }
+
+        // Update state.
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = if (isForTesting) 0 else ANIMATION_DURATION
+        animator.interpolator = Interpolators.LINEAR
+
+        val launchContainerOverlay = launchContainer.overlay
+        var cancelled = false
+        animator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
+                if (DEBUG) {
+                    Log.d(TAG, "Animation started")
+                }
+                controller.onLaunchAnimationStart(isExpandingFullyAbove)
+
+                // Add the drawable to the launch container overlay. Overlays always draw
+                // drawables after views, so we know that it will be drawn above any view added
+                // by the controller.
+                launchContainerOverlay.add(windowBackgroundLayer)
+            }
+
+            override fun onAnimationEnd(animation: Animator?) {
+                if (DEBUG) {
+                    Log.d(TAG, "Animation ended")
+                }
+                controller.onLaunchAnimationEnd(isExpandingFullyAbove)
+                launchContainerOverlay.remove(windowBackgroundLayer)
+            }
+        })
+
+        animator.addUpdateListener { animation ->
+            if (cancelled) {
+                // TODO(b/184121838): Cancel the animator directly instead of just skipping the
+                // update.
+                return@addUpdateListener
+            }
+
+            maybeUpdateEndState()
+
+            // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non
+            // reversed animation.
+            val linearProgress = animation.animatedFraction
+            val progress = animationInterpolator.getInterpolation(linearProgress)
+            val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
+
+            val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress)
+            val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f
+
+            state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt()
+            state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt()
+            state.left = (xCenter - halfWidth).roundToInt()
+            state.right = (xCenter + halfWidth).roundToInt()
+
+            state.topCornerRadius =
+                MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress)
+            state.bottomCornerRadius =
+                MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
+
+            // The expanding view can/should be hidden once it is completely covered by the opening
+            // window.
+            state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
+
+            applyStateToWindowBackgroundLayer(
+                windowBackgroundLayer,
+                state,
+                linearProgress,
+                launchContainer,
+                drawHole
+            )
+            controller.onLaunchAnimationProgress(state, progress, linearProgress)
+        }
+
+        animator.start()
+        return object : Animation {
+            override fun cancel() {
+                cancelled = true
+                animator.cancel()
+            }
+        }
+    }
+
+    /** Return whether we are expanding fully above the [launchContainer]. */
+    internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean {
+        launchContainer.getLocationOnScreen(launchContainerLocation)
+        return endState.top <= launchContainerLocation[1] &&
+            endState.bottom >= launchContainerLocation[1] + launchContainer.height &&
+            endState.left <= launchContainerLocation[0] &&
+            endState.right >= launchContainerLocation[0] + launchContainer.width
+    }
+
+    private fun applyStateToWindowBackgroundLayer(
+        drawable: GradientDrawable,
+        state: State,
+        linearProgress: Float,
+        launchContainer: View,
+        drawHole: Boolean
+    ) {
+        // Update position.
+        launchContainer.getLocationOnScreen(launchContainerLocation)
+        drawable.setBounds(
+            state.left - launchContainerLocation[0],
+            state.top - launchContainerLocation[1],
+            state.right - launchContainerLocation[0],
+            state.bottom - launchContainerLocation[1]
+        )
+
+        // Update radius.
+        cornerRadii[0] = state.topCornerRadius
+        cornerRadii[1] = state.topCornerRadius
+        cornerRadii[2] = state.topCornerRadius
+        cornerRadii[3] = state.topCornerRadius
+        cornerRadii[4] = state.bottomCornerRadius
+        cornerRadii[5] = state.bottomCornerRadius
+        cornerRadii[6] = state.bottomCornerRadius
+        cornerRadii[7] = state.bottomCornerRadius
+        drawable.cornerRadii = cornerRadii
+
+        // We first fade in the background layer to hide the expanding view, then fade it out
+        // with SRC mode to draw a hole punch in the status bar and reveal the opening window.
+        val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
+        if (fadeInProgress < 1) {
+            val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress)
+            drawable.alpha = (alpha * 0xFF).roundToInt()
+        } else {
+            val fadeOutProgress = getProgress(
+                linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
+            val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
+            drawable.alpha = (alpha * 0xFF).roundToInt()
+
+            if (drawHole) {
+                drawable.setXfermode(SRC_MODE)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ShadeInterpolation.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ShadeInterpolation.kt
new file mode 100644
index 0000000..0ee2bfe
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ShadeInterpolation.kt
@@ -0,0 +1,37 @@
+package com.android.systemui.animation
+
+import android.util.MathUtils
+
+object ShadeInterpolation {
+
+    /**
+     * Interpolate alpha for notification background scrim during shade expansion.
+     * @param fraction Shade expansion fraction
+     */
+    @JvmStatic
+    fun getNotificationScrimAlpha(fraction: Float): Float {
+        val mappedFraction = MathUtils.constrainedMap(0f, 1f, 0f, 0.5f, fraction)
+        return interpolateEaseInOut(mappedFraction)
+    }
+
+    /**
+     * Interpolate alpha for shade content during shade expansion.
+     * @param fraction Shade expansion fraction
+     */
+    @JvmStatic
+    fun getContentAlpha(fraction: Float): Float {
+        val mappedFraction = MathUtils.constrainedMap(0f, 1f, 0.3f, 1f, fraction)
+        return interpolateEaseInOut(mappedFraction)
+    }
+
+    private fun interpolateEaseInOut(fraction: Float): Float {
+        val mappedFraction = fraction * 1.2f - 0.2f
+        return if (mappedFraction <= 0) {
+            0f
+        } else {
+            val oneMinusFrac = 1f - mappedFraction
+            (1f - 0.5f * (1f - Math.cos((3.14159f * oneMinusFrac * oneMinusFrac).toDouble())))
+                    .toFloat()
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/docs/keyguard.md b/packages/SystemUI/docs/keyguard.md
new file mode 100644
index 0000000..5e7bc1c
--- /dev/null
+++ b/packages/SystemUI/docs/keyguard.md
@@ -0,0 +1,46 @@
+# Keyguard (aka Lockscreen)
+
+Keyguard is responsible for:
+
+1. Handling authentication to allow the user to unlock the device, via biometrics or [KeyguardBouncer][1]
+2. Displaying informational content such as the time, notifications, and smartspace
+3. Always-on Display (AOD)
+
+Keyguard is the first screen available when turning on the device, as long as the user has not specified a security method of NONE.
+
+## Critical User Journeys
+
+The journeys below generally refer to Keyguard's portion of the overall flow, especially regarding use of the power button. Power button key interpretation (short press, long press, very long press, multi press) is done in [PhoneWindowManager][4], with calls to [PowerManagerService][2] to sleep or wake up, if needed.
+
+### Power On - AOD enabled or disabled
+
+Begins with the device in low power mode, with the display active for [AOD][3] or inactive. [PowerManagerService][2] can be directed to wake up on various user-configurable signals, such as lift to wake, screen taps, among others. [AOD][2], whether visibly enabled or not, handles these signals to transition AOD to full Lockscreen content. See more in [AOD][3].
+
+### Power Off
+
+An indication to power off the device most likely comes from one of two signals: the user presses the power button or the screen timeout has passed. This may [lock the device](#How-the-device-locks)
+
+#### On Lockscreen
+
+#### On Lockscreen, occluded by an activity
+
+#### Device unlocked, Keyguard has gone away
+
+### Pulsing (Incoming notifications while dozing)
+
+### How the device locks
+
+More coming
+* Screen timeout
+* Smart lock
+* Device policy
+* Power button instantly locks setting
+* Lock timeout after screen timeout setting
+
+
+[1]: /frameworks/base/packages/SystemUI/docs/keyguard/bouncer.md
+[2]: /frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
+[3]: /frameworks/base/packages/SystemUI/docs/keyguard/aod.md
+[4]: /frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
+
+
diff --git a/packages/SystemUI/docs/keyguard/aod.md b/packages/SystemUI/docs/keyguard/aod.md
new file mode 100644
index 0000000..6d76ed5
--- /dev/null
+++ b/packages/SystemUI/docs/keyguard/aod.md
@@ -0,0 +1 @@
+# Always-on Display (AOD)
diff --git a/packages/SystemUI/docs/keyguard/bouncer.md b/packages/SystemUI/docs/keyguard/bouncer.md
new file mode 100644
index 0000000..51f8516
--- /dev/null
+++ b/packages/SystemUI/docs/keyguard/bouncer.md
@@ -0,0 +1,18 @@
+# Bouncer
+
+[KeyguardBouncer][1] is the component responsible for displaying the security method set by the user (password, PIN, pattern) as well as SIM-related security methods, allowing the user to unlock the device or SIM.
+
+## Components
+
+The bouncer contains a hierarchy of controllers/views to render the user's security method and to manage the authentication attempts.
+
+1. [KeyguardBouncer][1] - Entrypoint for managing the bouncer visibility.
+    1. [KeyguardHostViewController][2] - Intercepts media keys. Can most likely be merged with the next item.
+        1. [KeyguardSecurityContainerController][3] - Manages unlock attempt responses, one-handed use
+            1. [KeyguardSecurityViewFlipperController][4] - Based upon the [KeyguardSecurityModel#SecurityMode][5], will instantiate the required view and controller. PIN, Pattern, etc.
+
+[1]: /frameworks/base/packages/SystemUI/com/android/systemui/statusbar/phone/KeyguardBouncer
+[2]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardHostViewController
+[3]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardSecurityContainerController
+[4]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardSecurityViewFlipperController
+[5]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardSecurityModel
diff --git a/packages/SystemUI/monet/Android.bp b/packages/SystemUI/monet/Android.bp
new file mode 100644
index 0000000..507ea25
--- /dev/null
+++ b/packages/SystemUI/monet/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "monet",
+    platform_apis: true,
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.core_core",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+}
diff --git a/packages/SystemUI/monet/AndroidManifest.xml b/packages/SystemUI/monet/AndroidManifest.xml
new file mode 100644
index 0000000..1fab528
--- /dev/null
+++ b/packages/SystemUI/monet/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.systemui.monet">
+</manifest>
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
new file mode 100644
index 0000000..1844288
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.monet
+
+import android.annotation.ColorInt
+import android.app.WallpaperColors
+import android.graphics.Color
+import com.android.internal.graphics.ColorUtils
+import com.android.internal.graphics.cam.Cam
+import com.android.internal.graphics.cam.CamUtils.lstarFromInt
+import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
+
+const val TAG = "ColorScheme"
+
+const val ACCENT1_CHROMA = 48.0f
+const val ACCENT2_CHROMA = 16.0f
+const val ACCENT3_CHROMA = 32.0f
+const val ACCENT3_HUE_SHIFT = 60.0f
+
+const val NEUTRAL1_CHROMA = 4.0f
+const val NEUTRAL2_CHROMA = 8.0f
+
+const val GOOGLE_BLUE = 0xFF1b6ef3.toInt()
+
+const val MIN_CHROMA = 5
+
+public class ColorScheme(@ColorInt seed: Int, val darkTheme: Boolean) {
+
+    val accent1: List<Int>
+    val accent2: List<Int>
+    val accent3: List<Int>
+    val neutral1: List<Int>
+    val neutral2: List<Int>
+
+    constructor(wallpaperColors: WallpaperColors, darkTheme: Boolean):
+            this(getSeedColor(wallpaperColors), darkTheme)
+
+    val allAccentColors: List<Int>
+        get() {
+            val allColors = mutableListOf<Int>()
+            allColors.addAll(accent1)
+            allColors.addAll(accent2)
+            allColors.addAll(accent3)
+            return allColors
+        }
+
+    val allNeutralColors: List<Int>
+        get() {
+            val allColors = mutableListOf<Int>()
+            allColors.addAll(neutral1)
+            allColors.addAll(neutral2)
+            return allColors
+        }
+
+    val backgroundColor
+        get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1[8] else neutral1[0], 0xFF)
+
+    val accentColor
+        get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1[2] else accent1[6], 0xFF)
+
+    init {
+        val proposedSeedCam = Cam.fromInt(seed)
+        val seedArgb = if (seed == Color.TRANSPARENT) {
+            GOOGLE_BLUE
+        } else if (proposedSeedCam.chroma < 5) {
+            GOOGLE_BLUE
+        } else {
+            seed
+        }
+        val camSeed = Cam.fromInt(seedArgb)
+        val hue = camSeed.hue
+        val chroma = camSeed.chroma.coerceAtLeast(ACCENT1_CHROMA)
+        accent1 = Shades.of(hue, chroma).toList()
+        accent2 = Shades.of(hue, ACCENT2_CHROMA).toList()
+        accent3 = Shades.of(hue + ACCENT3_HUE_SHIFT, ACCENT3_CHROMA).toList()
+        neutral1 = Shades.of(hue, NEUTRAL1_CHROMA).toList()
+        neutral2 = Shades.of(hue, NEUTRAL2_CHROMA).toList()
+    }
+
+    override fun toString(): String {
+        return "ColorScheme {\n" +
+                "  neutral1: ${humanReadable(neutral1)}\n" +
+                "  neutral2: ${humanReadable(neutral2)}\n" +
+                "  accent1: ${humanReadable(accent1)}\n" +
+                "  accent2: ${humanReadable(accent2)}\n" +
+                "  accent3: ${humanReadable(accent3)}\n" +
+                "}"
+    }
+
+    companion object {
+        /**
+         * Identifies a color to create a color scheme from.
+         *
+         * @param wallpaperColors Colors extracted from an image via quantization.
+         * @return ARGB int representing the color
+         */
+        @JvmStatic
+        @ColorInt
+        fun getSeedColor(wallpaperColors: WallpaperColors): Int {
+            return getSeedColors(wallpaperColors).first()
+        }
+
+        /**
+         * Filters and ranks colors from WallpaperColors.
+         *
+         * @param wallpaperColors Colors extracted from an image via quantization.
+         * @return List of ARGB ints, ordered from highest scoring to lowest.
+         */
+        @JvmStatic
+        fun getSeedColors(wallpaperColors: WallpaperColors): List<Int> {
+            val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b }
+                    .toDouble()
+            val totalPopulationMeaningless = (totalPopulation == 0.0)
+            if (totalPopulationMeaningless) {
+                // WallpaperColors with a population of 0 indicate the colors didn't come from
+                // quantization. Instead of scoring, trust the ordering of the provided primary
+                // secondary/tertiary colors.
+                //
+                // In this case, the colors are usually from a Live Wallpaper.
+                val distinctColors = wallpaperColors.mainColors.map {
+                    it.toArgb()
+                }.distinct().filter {
+                    Cam.fromInt(it).chroma >= MIN_CHROMA
+                }.toList()
+
+                if (distinctColors.isEmpty()) {
+                    return listOf(GOOGLE_BLUE)
+                }
+                return distinctColors
+            }
+
+            val intToProportion = wallpaperColors.allColors.mapValues {
+                it.value.toDouble() / totalPopulation
+            }
+            val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) }
+
+            // Get an array with 360 slots. A slot contains the percentage of colors with that hue.
+            val hueProportions = huePopulations(intToCam, intToProportion)
+            // Map each color to the percentage of the image with its hue.
+            val intToHueProportion = wallpaperColors.allColors.mapValues {
+                val cam = intToCam[it.key]!!
+                val hue = cam.hue.roundToInt()
+                var proportion = 0.0
+                for (i in hue - 15..hue + 15) {
+                    proportion += hueProportions[wrapDegrees(i)]
+                }
+                proportion
+            }
+            // Remove any inappropriate seed colors. For example, low chroma colors look grayscale
+            // raising their chroma will turn them to a much louder color that may not have been
+            // in the image.
+            val filteredIntToCam = intToCam.filter {
+                val cam = it.value
+                val lstar = lstarFromInt(it.key)
+                val proportion = intToHueProportion[it.key]!!
+                cam.chroma >= MIN_CHROMA &&
+                        (totalPopulationMeaningless || proportion > 0.01)
+            }
+            // Sort the colors by score, from high to low.
+            val intToScoreIntermediate = filteredIntToCam.mapValues {
+                score(it.value, intToHueProportion[it.key]!!)
+            }
+            val intToScore = intToScoreIntermediate.entries.toMutableList()
+            intToScore.sortByDescending { it.value }
+
+            // Go through the colors, from high score to low score.
+            // If the color is distinct in hue from colors picked so far, pick the color.
+            // Iteratively decrease the amount of hue distinctness required, thus ensuring we
+            // maximize difference between colors.
+            val minimumHueDistance = 15
+            val seeds = mutableListOf<Int>()
+            maximizeHueDistance@ for (i in 90 downTo minimumHueDistance step 1) {
+                seeds.clear()
+                for (entry in intToScore) {
+                    val int = entry.key
+                    val existingSeedNearby = seeds.find {
+                        val hueA = intToCam[int]!!.hue
+                        val hueB = intToCam[it]!!.hue
+                        hueDiff(hueA, hueB) < i } != null
+                    if (existingSeedNearby) {
+                        continue
+                    }
+                    seeds.add(int)
+                    if (seeds.size >= 4) {
+                        break@maximizeHueDistance
+                    }
+                }
+            }
+
+            if (seeds.isEmpty()) {
+                // Use gBlue 500 if there are 0 colors
+                seeds.add(GOOGLE_BLUE)
+            }
+
+            return seeds
+        }
+
+        private fun wrapDegrees(degrees: Int): Int {
+            return when {
+                degrees < 0 -> {
+                    (degrees % 360) + 360
+                }
+                degrees >= 360 -> {
+                    degrees % 360
+                }
+                else -> {
+                    degrees
+                }
+            }
+        }
+
+        private fun hueDiff(a: Float, b: Float): Float {
+            return 180f - ((a - b).absoluteValue - 180f).absoluteValue
+        }
+
+        private fun humanReadable(colors: List<Int>): String {
+            return colors.joinToString { "#" + Integer.toHexString(it) }
+        }
+
+        private fun score(cam: Cam, proportion: Double): Double {
+            val proportionScore = 0.7 * 100.0 * proportion
+            val chromaScore = if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA)
+            else 0.3 * (cam.chroma - ACCENT1_CHROMA)
+            return chromaScore + proportionScore
+        }
+
+        private fun huePopulations(
+            camByColor: Map<Int, Cam>,
+            populationByColor: Map<Int, Double>
+        ): List<Double> {
+            val huePopulation = List(size = 360, init = { 0.0 }).toMutableList()
+
+            for (entry in populationByColor.entries) {
+                val population = populationByColor[entry.key]!!
+                val cam = camByColor[entry.key]!!
+                val hue = cam.hue.roundToInt() % 360
+                if (cam.chroma <= MIN_CHROMA) {
+                    continue
+                }
+                huePopulation[hue] = huePopulation[hue] + population
+            }
+
+            return huePopulation
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java b/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java
new file mode 100644
index 0000000..aab3538
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.monet;
+
+
+import androidx.annotation.ColorInt;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.ColorUtils;
+
+
+/**
+ * Generate sets of colors that are shades of the same color
+ */
+@VisibleForTesting
+public class Shades {
+    /**
+     *  Combining the ability to convert between relative luminance and perceptual luminance with
+     *  contrast leads to a design system that can be based on a linear value to determine contrast,
+     *  rather than a ratio.
+     *
+     *  This codebase implements a design system that has that property, and as a result, we can
+     *  guarantee that any shades 5 steps from each other have a contrast ratio of at least 4.5.
+     *  4.5 is the requirement for smaller text contrast in WCAG 2.1 and earlier.
+     *
+     *  However, lstar 50 does _not_ have a contrast ratio >= 4.5 with lstar 100.
+     *  lstar 49.6 is the smallest lstar that will lead to a contrast ratio >= 4.5 with lstar 100,
+     *  and it also contrasts >= 4.5 with lstar 100.
+     */
+    public static final float MIDDLE_LSTAR = 49.6f;
+
+    /**
+     * Generate shades of a color. Ordered in lightness _descending_.
+     * <p>
+     * The first shade will be at 95% lightness, the next at 90, 80, etc. through 0.
+     *
+     * @param hue    hue in CAM16 color space
+     * @param chroma chroma in CAM16 color space
+     * @return shades of a color, as argb integers. Ordered by lightness descending.
+     */
+    public static @ColorInt int[] of(float hue, float chroma) {
+        int[] shades = new int[12];
+        // At tone 90 and above, blue and yellow hues can reach a much higher chroma.
+        // To preserve a consistent appearance across all hues, use a maximum chroma of 40.
+        shades[0] = ColorUtils.CAMToColor(hue, Math.min(40f, chroma), 99);
+        shades[1] = ColorUtils.CAMToColor(hue, Math.min(40f, chroma), 95);
+        for (int i = 2; i < 12; i++) {
+            float lStar = (i == 6) ? MIDDLE_LSTAR : 100 - 10 * (i - 1);
+            if (lStar >= 90) {
+                chroma = Math.min(40f, chroma);
+            }
+            shades[i] = ColorUtils.CAMToColor(hue, chroma, lStar);
+        }
+        return shades;
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/flags/Flag.kt b/packages/SystemUI/plugin/src/com/android/systemui/flags/Flag.kt
new file mode 100644
index 0000000..68834bc
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/flags/Flag.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags
+
+interface Flag<T> {
+    val id: Int
+    val default: T
+}
+
+data class BooleanFlag @JvmOverloads constructor(
+    override val id: Int,
+    override val default: Boolean = false
+) : Flag<Boolean>
+
+data class StringFlag @JvmOverloads constructor(
+    override val id: Int,
+    override val default: String = ""
+) : Flag<String>
+
+data class IntFlag @JvmOverloads constructor(
+    override val id: Int,
+    override val default: Int = 0
+) : Flag<Int>
+
+data class LongFlag @JvmOverloads constructor(
+    override val id: Int,
+    override val default: Long = 0
+) : Flag<Long>
+
+data class FloatFlag @JvmOverloads constructor(
+    override val id: Int,
+    override val default: Float = 0f
+) : Flag<Float>
+
+data class DoubleFlag @JvmOverloads constructor(
+    override val id: Int,
+    override val default: Double = 0.0
+) : Flag<Double>
\ No newline at end of file
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/plugin/src/com/android/systemui/flags/Flags.java
new file mode 100644
index 0000000..3761d42
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/flags/Flags.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * List of {@link Flag} objects for use in SystemUI.
+ *
+ * Flag Ids are integers.
+ * Ids must be unique. This is enforced in a unit test.
+ * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related featurs with
+ * a comment. This is purely for organizational purposes.
+ *
+ * On public release builds, flags will always return their default value. There is no way to
+ * change their value on release builds.
+ *
+ * See {@link FeatureFlagManager} for instructions on flipping the flags via adb.
+ */
+public class Flags {
+    public static final BooleanFlag TEAMFOOD = new BooleanFlag(1, false);
+
+    /***************************************/
+    // 100 - notification
+    public static final BooleanFlag NEW_NOTIFICATION_PIPELINE =
+            new BooleanFlag(100, true);
+
+    public static final BooleanFlag NEW_NOTIFICATION_PIPELINE_RENDERING =
+            new BooleanFlag(101, false);
+
+    public static final BooleanFlag NOTIFICATION_UPDATES =
+            new BooleanFlag(102, true);
+
+
+    /***************************************/
+    // 200 - keyguard/lockscreen
+    public static final BooleanFlag KEYGUARD_LAYOUT =
+            new BooleanFlag(200, true);
+
+    public static final BooleanFlag LOCKSCREEN_ANIMATIONS =
+            new BooleanFlag(201, true);
+
+    public static final BooleanFlag NEW_UNLOCK_SWIPE_ANIMATION =
+            new BooleanFlag(202, true);
+
+    /***************************************/
+    // 300 - power menu
+    public static final BooleanFlag POWER_MENU_LITE =
+            new BooleanFlag(300, true);
+
+    /***************************************/
+    // 400 - smartspace
+    public static final BooleanFlag SMARTSPACE_DEDUPING =
+            new BooleanFlag(400, true);
+
+    public static final BooleanFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
+            new BooleanFlag(401, false);
+
+    /***************************************/
+    // 500 - quick settings
+    public static final BooleanFlag NEW_USER_SWITCHER =
+            new BooleanFlag(500, true);
+
+    /***************************************/
+    // 600- status bar
+    public static final BooleanFlag COMBINED_STATUS_BAR_SIGNAL_ICONS =
+            new BooleanFlag(501, false);
+
+    /***************************************/
+    // 700 - dialer/calls
+    public static final BooleanFlag ONGOING_CALL_STATUS_BAR_CHIP =
+            new BooleanFlag(600, true);
+
+    public static final BooleanFlag ONGOING_CALL_IN_IMMERSIVE =
+            new BooleanFlag(601, true);
+
+    public static final BooleanFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP =
+            new BooleanFlag(602, true);
+
+    // Pay no attention to the reflection behind the curtain.
+    // ========================== Curtain ==========================
+    // |                                                           |
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    private static Map<Integer, Flag<?>> sFlagMap;
+    static Map<Integer, Flag<?>> collectFlags() {
+        if (sFlagMap != null) {
+            return sFlagMap;
+        }
+        Map<Integer, Flag<?>> flags = new HashMap<>();
+
+        Field[] fields = Flags.class.getFields();
+
+        for (Field field : fields) {
+            Class<?> t = field.getType();
+            if (Flag.class.isAssignableFrom(t)) {
+                try {
+                    Flag<?> flag = (Flag<?>) field.get(null);
+                    flags.put(flag.getId(), flag);
+                } catch (IllegalAccessException e) {
+                    // no-op
+                }
+            }
+        }
+
+        sFlagMap = flags;
+
+        return sFlagMap;
+    }
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    // |                                                           |
+    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
+
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 7c81325..6d088f0 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -60,8 +60,16 @@
      */
     void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags);
     void startActivity(Intent intent, boolean dismissShade);
+
+    default void startActivity(Intent intent, boolean dismissShade,
+            @Nullable ActivityLaunchAnimator.Controller animationController) {
+        startActivity(intent, dismissShade, animationController,
+                false /* showOverLockscreenWhenLocked */);
+    }
+
     void startActivity(Intent intent, boolean dismissShade,
-            @Nullable ActivityLaunchAnimator.Controller animationController);
+            @Nullable ActivityLaunchAnimator.Controller animationController,
+            boolean showOverLockscreenWhenLocked);
     void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade);
     void startActivity(Intent intent, boolean dismissShade, Callback callback);
     void postStartActivityDismissingKeyguard(Intent intent, int delay);
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index 0424382..757dc2e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -16,7 +16,6 @@
 
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.ViewGroup;
 
 import com.android.systemui.plugins.FragmentBase;
 import com.android.systemui.plugins.annotations.DependsOn;
@@ -53,10 +52,19 @@
     boolean isShowingDetail();
     void closeDetail();
     void animateHeaderSlidingOut();
-    void setQsExpansion(float qsExpansionFraction, float headerTranslation);
+
+    /**
+     * Asks QS to update its presentation, according to {@code NotificationPanelViewController}.
+     *
+     * @param qsExpansionFraction How much each UI element in QS should be expanded (QQS to QS.)
+     * @param panelExpansionFraction Whats the expansion of the whole shade.
+     * @param headerTranslation How much we should vertically translate QS.
+     */
+    void setQsExpansion(float qsExpansionFraction, float panelExpansionFraction,
+            float headerTranslation);
     void setHeaderListening(boolean listening);
     void notifyCustomizeChanged();
-    void setContainer(ViewGroup container);
+    void setContainerController(QSContainerController controller);
     void setExpandClickListener(OnClickListener onClickListener);
 
     View getHeader();
@@ -76,13 +84,13 @@
     /**
      * If QS should translate as we pull it down, or if it should be static.
      */
-    void setTranslateWhileExpanding(boolean shouldTranslate);
+    void setInSplitShade(boolean shouldTranslate);
 
     /**
      * Set the amount of pixels we have currently dragged down if we're transitioning to the full
      * shade. 0.0f means we're not transitioning yet.
      */
-    default void setTransitionToFullShadeAmount(float pxAmount, boolean animated) {}
+    default void setTransitionToFullShadeAmount(float pxAmount, float progress) {}
 
     /**
      * A rounded corner clipping that makes QS feel as if it were behind everything.
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt
new file mode 100644
index 0000000..8bf982d
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt
@@ -0,0 +1,9 @@
+package com.android.systemui.plugins.qs
+
+interface QSContainerController {
+    fun setCustomizerAnimating(animating: Boolean)
+
+    fun setCustomizerShowing(showing: Boolean)
+
+    fun setDetailShowing(showing: Boolean)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
index 6c5c4ef..9829918 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
@@ -109,9 +109,8 @@
          * Callback to be notified when the fullscreen or immersive state changes.
          *
          * @param isFullscreen if any of the system bar is hidden by the focused window.
-         * @param isImmersive if the navigation bar can stay hidden when the display gets tapped.
          */
-        default void onFullscreenStateChanged(boolean isFullscreen, boolean isImmersive) {}
+        default void onFullscreenStateChanged(boolean isFullscreen) {}
 
         /**
          * Callback to be notified when the pulsing state changes
diff --git a/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml b/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml
new file mode 100644
index 0000000..08c66a2
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:color="?androidprv:attr/colorAccentSecondaryVariant"/>
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/anim/fp_to_unlock.xml b/packages/SystemUI/res-keyguard/drawable/fp_to_unlock.xml
similarity index 87%
rename from packages/SystemUI/res/anim/fp_to_unlock.xml
rename to packages/SystemUI/res-keyguard/drawable/fp_to_unlock.xml
index a5f75b6..b93ccc6 100644
--- a/packages/SystemUI/res/anim/fp_to_unlock.xml
+++ b/packages/SystemUI/res-keyguard/drawable/fp_to_unlock.xml
@@ -19,15 +19,15 @@
       <group android:name="_R_G">
         <group android:name="_R_G_L_1_G_N_7_T_0" android:translateX="-27" android:translateY="-17.5">
           <group android:name="_R_G_L_1_G" android:translateX="30.75" android:translateY="25.75">
-            <path android:name="_R_G_L_1_G_D_0_P_0" android:strokeColor="#b7f29f" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 " />
-            <path android:name="_R_G_L_1_G_D_1_P_0" android:strokeColor="#b7f29f" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 " />
-            <path android:name="_R_G_L_1_G_D_2_P_0" android:strokeColor="#b7f29f" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 " />
-            <path android:name="_R_G_L_1_G_D_3_P_0" android:strokeColor="#b7f29f" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 " />
+            <path android:name="_R_G_L_1_G_D_0_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 " />
+            <path android:name="_R_G_L_1_G_D_1_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 " />
+            <path android:name="_R_G_L_1_G_D_2_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 " />
+            <path android:name="_R_G_L_1_G_D_3_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 " />
           </group>
         </group>
         <group android:name="_R_G_L_0_G_N_7_T_0" android:translateX="-27" android:translateY="-17.5">
           <group android:name="_R_G_L_0_G" android:translateX="47.357" android:translateY="53.25" android:pivotX="2.75" android:pivotY="2.75" android:scaleX="1.41866" android:scaleY="1.41866">
-            <path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#b7f29f" android:fillAlpha="0" android:fillType="nonZero" android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c " />
+            <path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#FF000000" android:fillAlpha="0" android:fillType="nonZero" android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c " />
           </group>
         </group>
       </group>
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml b/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml
index dd35dd9..1eec820 100644
--- a/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml
+++ b/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml
@@ -20,6 +20,6 @@
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
-        android:fillColor="?android:attr/colorBackground"
+        android:fillColor="?android:attr/textColorPrimaryInverse"
         android:pathData="M9,15.59L12.59,12L9,8.41L10.41,7L14,10.59L17.59,7L19,8.41L15.41,12L19,15.59L17.59,17L14,13.41L10.41,17L9,15.59zM21,6H8l-4.5,6L8,18h13V6M21,4c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2H8c-0.63,0 -1.22,-0.3 -1.6,-0.8L1,12l5.4,-7.2C6.78,4.3 7.37,4 8,4H21L21,4z"/>
 </vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_fingerprint.xml b/packages/SystemUI/res-keyguard/drawable/ic_fingerprint.xml
new file mode 100644
index 0000000..2063d21
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_fingerprint.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:height="65dp"
+        android:width="46dp"
+        android:viewportHeight="65"
+        android:viewportWidth="46">
+    <group android:name="_R_G_L_0_G" android:translateX="3.75" android:translateY="8.25">
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="2.5"
+            android:pathData="M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 " />
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="2.5"
+            android:pathData="M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 " />
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="2.5"
+            android:pathData="M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 " />
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="2.5"
+            android:pathData="M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 " />
+    </group>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml b/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml
index b844515..2ad5e54 100644
--- a/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml
+++ b/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml
@@ -19,7 +19,8 @@
      android:viewportHeight="36"
      android:viewportWidth="36"
      android:width="36sp">
-  <path android:fillColor="?android:attr/colorBackground"
+
+  <path android:fillColor="?android:attr/textColorPrimaryInverse"
         android:pathData="M17.59,13.41L21.17,17H7v2h14.17l-3.59,3.59L19,24l6,-6l-6,-6L17.59,
                           13.41zM26,12v12h2V12H26z"/>
 </vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_lock.xml b/packages/SystemUI/res-keyguard/drawable/ic_lock.xml
new file mode 100644
index 0000000..14a8d0b
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_lock.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:height="65dp"
+        android:width="46dp"
+        android:viewportHeight="65"
+        android:viewportWidth="46">
+    <group android:name="_R_G">
+        <group android:name="_R_G_L_2_G_N_10_N_11_T_0"
+               android:translateX="-27.5"
+               android:translateY="-17.5">
+            <group android:name="_R_G_L_2_G_N_10_T_1"
+                   android:translateX="50.25"
+                   android:translateY="61">
+                <group android:name="_R_G_L_2_G_N_10_T_0"
+                       android:translateX="-13.75"
+                       android:translateY="-7.5">
+                    <group android:name="_R_G_L_2_G"
+                           android:translateX="-0.375"
+                           android:translateY="-22.375">
+                        <path android:name="_R_G_L_2_G_D_0_P_0"
+                              android:strokeColor="#FF000000"
+                              android:strokeLineCap="round"
+                              android:strokeLineJoin="round"
+                              android:strokeWidth="2"
+                              android:strokeAlpha="1"
+                              android:pathData=" M4.75 15 C4.75,15 23.25,15 23.25,15 C24.35,15 25.25,15.9 25.25,17 C25.25,17 25.25,33 25.25,33 C25.25,34.1 24.35,35 23.25,35 C23.25,35 4.75,35 4.75,35 C3.65,35 2.75,34.1 2.75,33 C2.75,33 2.75,17 2.75,17 C2.75,15.9 3.65,15 4.75,15c "/>
+                    </group>
+                </group>
+            </group>
+        </group>
+        <group android:name="_R_G_L_1_G_N_10_N_11_T_0"
+               android:translateX="-27.5"
+               android:translateY="-17.5">
+            <group android:name="_R_G_L_1_G_N_10_T_1"
+                   android:translateX="50.25"
+                   android:translateY="61">
+                <group android:name="_R_G_L_1_G_N_10_T_0"
+                       android:translateX="-13.75"
+                       android:translateY="-7.5">
+                    <group android:name="_R_G_L_1_G"
+                           android:translateX="5"
+                           android:translateY="-22.5">
+                        <path android:name="_R_G_L_1_G_D_0_P_0"
+                              android:strokeColor="#FF000000"
+                              android:strokeLineCap="round"
+                              android:strokeLineJoin="round"
+                              android:strokeWidth="2"
+                              android:strokeAlpha="1"
+                              android:pathData=" M2.5 15 C2.5,15 2.5,8.61 2.5,8.61 C2.5,5.24 5.3,2.5 8.75,2.5 C12.2,2.5 15,5.24 15,8.61 C15,8.61 15,15 15,15 "/>
+                    </group>
+                </group>
+            </group>
+        </group>
+        <group android:name="_R_G_L_0_G_N_10_N_11_T_0"
+               android:translateX="-27.5"
+               android:translateY="-17.5">
+            <group android:name="_R_G_L_0_G_N_10_T_1"
+                   android:translateX="50.25"
+                   android:translateY="61">
+                <group android:name="_R_G_L_0_G_N_10_T_0"
+                       android:translateX="-13.75"
+                       android:translateY="-7.5">
+                    <group android:name="_R_G_L_0_G"
+                           android:translateX="11"
+                           android:translateY="-0.25"
+                           android:pivotX="2.75"
+                           android:pivotY="2.75"
+                           android:scaleX="1"
+                           android:scaleY="1">
+                        <path android:name="_R_G_L_0_G_D_0_P_0"
+                              android:fillColor="#FF000000"
+                              android:fillAlpha="1"
+                              android:fillType="nonZero"
+                              android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c "/>
+                    </group>
+                </group>
+            </group>
+        </group>
+    </group>
+</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_lock_aod.xml b/packages/SystemUI/res-keyguard/drawable/ic_lock_aod.xml
new file mode 100644
index 0000000..cdae306
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_lock_aod.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:height="65dp"
+        android:width="46dp"
+        android:viewportHeight="65"
+        android:viewportWidth="46">
+    <group android:name="_R_G_L_2_G" android:translateX="23" android:translateY="32.125">
+        <path
+            android:fillColor="#FF000000"
+            android:fillAlpha="1"
+            android:fillType="nonZero"
+            android:pathData=" M0 6.13 C0.97,6.13 1.75,5.34 1.75,4.38 C1.75,3.41 0.97,2.63 0,2.63 C-0.97,2.63 -1.75,3.41 -1.75,4.38 C-1.75,5.34 -0.97,6.13 0,6.13c " />
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="1.5"
+            android:pathData=" M7.88 -0.62 C7.88,-0.62 7.88,9.38 7.88,9.38 C7.88,10.48 6.98,11.38 5.88,11.38 C5.88,11.38 -5.87,11.38 -5.87,11.38 C-6.98,11.38 -7.87,10.48 -7.87,9.38 C-7.87,9.38 -7.87,-0.62 -7.87,-0.62 C-7.87,-1.73 -6.98,-2.62 -5.87,-2.62 C-5.87,-2.62 5.88,-2.62 5.88,-2.62 C6.98,-2.62 7.88,-1.73 7.88,-0.62c " />
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="1.5"
+            android:pathData=" M4.38 -2.62 C4.38,-2.62 4.38,-7.1 4.38,-7.1 C4.38,-9.46 2.42,-11.37 0,-11.37 C-2.42,-11.37 -4.37,-9.46 -4.37,-7.1 C-4.37,-7.1 -4.37,-2.62 -4.37,-2.62 " />
+    </group>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_unlocked.xml b/packages/SystemUI/res-keyguard/drawable/ic_unlocked.xml
new file mode 100644
index 0000000..54242781
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_unlocked.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:height="65dp"
+        android:width="46dp"
+        android:viewportHeight="65"
+        android:viewportWidth="46">
+    <group android:translateX="8.625"
+           android:translateY="13.625">
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="2.5"
+            android:pathData="M4.75 15 C4.75,15 23.25,15 23.25,15 C24.35,15 25.25,15.9 25.25,17 C25.25,17 25.25,33 25.25,33 C25.25,34.1 24.35,35 23.25,35 C23.25,35 4.75,35 4.75,35 C3.65,35 2.75,34.1 2.75,33 C2.75,33 2.75,17 2.75,17 C2.75,15.9 3.65,15 4.75,15c "/>
+    </group>
+    <group android:translateX="14"
+           android:translateY="13.5">
+        <path
+            android:strokeColor="#FF000000"
+            android:strokeLineCap="round"
+            android:strokeLineJoin="round"
+            android:strokeWidth="2.5"
+            android:pathData="M27.19 14.81 C27.19,14.81 27.19,8.3 27.19,8.3 C27.19,4.92 24.44,2.88 21.19,2.75 C17.74,2.62 15,4.74 15,8.11 C15,8.11 15,15 15,15 "/>
+    </group>
+    <group android:translateX="20"
+           android:translateY="35.75">
+        <path
+            android:fillColor="#FF000000"
+            android:fillAlpha="1"
+            android:fillType="nonZero"
+            android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c "/>
+    </group>
+</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/lock_aod_to_ls.xml b/packages/SystemUI/res-keyguard/drawable/lock_aod_to_ls.xml
new file mode 100644
index 0000000..d35f695
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/lock_aod_to_ls.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<animated-vector xmlns:aapt="http://schemas.android.com/aapt"
+                 xmlns:android="http://schemas.android.com/apk/res/android">
+    <aapt:attr name="android:drawable">
+        <vector android:height="65dp"
+                android:width="46dp"
+                android:viewportHeight="65"
+                android:viewportWidth="46">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_2_G"
+                       android:translateX="23"
+                       android:translateY="32.125">
+                    <path android:name="_R_G_L_2_G_D_0_P_0"
+                          android:fillColor="#FF000000"
+                          android:fillAlpha="1"
+                          android:fillType="nonZero"
+                          android:pathData=" M0 6.13 C0.97,6.13 1.75,5.34 1.75,4.38 C1.75,3.41 0.97,2.63 0,2.63 C-0.97,2.63 -1.75,3.41 -1.75,4.38 C-1.75,5.34 -0.97,6.13 0,6.13c "/>
+                </group>
+                <group android:name="_R_G_L_1_G"
+                       android:translateX="23"
+                       android:translateY="32.125">
+                    <path android:name="_R_G_L_1_G_D_0_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="1.5"
+                          android:strokeAlpha="1"
+                          android:pathData=" M7.88 -0.62 C7.88,-0.62 7.88,9.38 7.88,9.38 C7.88,10.48 6.98,11.38 5.88,11.38 C5.88,11.38 -5.87,11.38 -5.87,11.38 C-6.98,11.38 -7.87,10.48 -7.87,9.38 C-7.87,9.38 -7.87,-0.62 -7.87,-0.62 C-7.87,-1.73 -6.98,-2.62 -5.87,-2.62 C-5.87,-2.62 5.88,-2.62 5.88,-2.62 C6.98,-2.62 7.88,-1.73 7.88,-0.62c "/>
+                </group>
+                <group android:name="_R_G_L_0_G"
+                       android:translateX="23"
+                       android:translateY="32.125">
+                    <path android:name="_R_G_L_0_G_D_0_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="1.5"
+                          android:strokeAlpha="1"
+                          android:pathData=" M4.38 -2.62 C4.38,-2.62 4.38,-7.1 4.38,-7.1 C4.38,-9.46 2.42,-11.37 0,-11.37 C-2.42,-11.37 -4.37,-9.46 -4.37,-7.1 C-4.37,-7.1 -4.37,-2.62 -4.37,-2.62 "/>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_2_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="M0 6.13 C0.97,6.13 1.75,5.34 1.75,4.38 C1.75,3.41 0.97,2.63 0,2.63 C-0.97,2.63 -1.75,3.41 -1.75,4.38 C-1.75,5.34 -0.97,6.13 0,6.13c "
+                                android:valueTo="M-0.09 8.63 C1.2,8.63 2.25,7.57 2.25,6.28 C2.25,4.99 1.2,3.94 -0.09,3.94 C-1.39,3.94 -2.44,4.99 -2.44,6.28 C-2.44,7.57 -1.39,8.63 -0.09,8.63c "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.372,0 0.203,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="strokeWidth"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="1.5"
+                                android:valueTo="2"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.386,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="M7.88 -0.62 C7.88,-0.62 7.88,9.38 7.88,9.38 C7.88,10.48 6.98,11.38 5.88,11.38 C5.88,11.38 -5.87,11.38 -5.87,11.38 C-6.98,11.38 -7.87,10.48 -7.87,9.38 C-7.87,9.38 -7.87,-0.62 -7.87,-0.62 C-7.87,-1.73 -6.98,-2.62 -5.87,-2.62 C-5.87,-2.62 5.88,-2.62 5.88,-2.62 C6.98,-2.62 7.88,-1.73 7.88,-0.62c "
+                                android:valueTo="M11.25 -0.64 C11.25,-0.64 11.25,13.64 11.25,13.64 C11.25,15.22 9.97,16.5 8.39,16.5 C8.39,16.5 -8.39,16.5 -8.39,16.5 C-9.97,16.5 -11.25,15.22 -11.25,13.64 C-11.25,13.64 -11.25,-0.64 -11.25,-0.64 C-11.25,-2.22 -9.97,-3.5 -8.39,-3.5 C-8.39,-3.5 8.39,-3.5 8.39,-3.5 C9.97,-3.5 11.25,-2.22 11.25,-0.64c "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.372,0 0.203,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="strokeWidth"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="1.5"
+                                android:valueTo="2"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.386,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="M4.38 -2.62 C4.38,-2.62 4.38,-7.1 4.38,-7.1 C4.38,-9.46 2.42,-11.37 0,-11.37 C-2.42,-11.37 -4.37,-9.46 -4.37,-7.1 C-4.37,-7.1 -4.37,-2.62 -4.37,-2.62 "
+                                android:valueTo="M5.88 -3.87 C5.88,-3.87 5.88,-10.2 5.88,-10.2 C5.88,-13.54 3.08,-16.25 -0.37,-16.25 C-3.83,-16.25 -6.62,-13.54 -6.62,-10.2 C-6.62,-10.2 -6.62,-3.87 -6.62,-3.87 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.372,0 0.203,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="translateX"
+                                android:duration="517"
+                                android:startOffset="0"
+                                android:valueFrom="0"
+                                android:valueTo="1"
+                                android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/lock_ls_to_aod.xml b/packages/SystemUI/res-keyguard/drawable/lock_ls_to_aod.xml
new file mode 100644
index 0000000..8a728ee
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/lock_ls_to_aod.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<animated-vector xmlns:aapt="http://schemas.android.com/aapt"
+                 xmlns:android="http://schemas.android.com/apk/res/android">
+    <aapt:attr name="android:drawable">
+        <vector android:height="65dp"
+                android:width="46dp"
+                android:viewportHeight="65"
+                android:viewportWidth="46">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_2_G"
+                       android:translateX="23"
+                       android:translateY="32.125">
+                    <path android:name="_R_G_L_2_G_D_0_P_0"
+                          android:fillColor="#FF000000"
+                          android:fillAlpha="1"
+                          android:fillType="nonZero"
+                          android:pathData=" M-0.09 8.63 C1.2,8.63 2.25,7.57 2.25,6.28 C2.25,4.99 1.2,3.94 -0.09,3.94 C-1.39,3.94 -2.44,4.99 -2.44,6.28 C-2.44,7.57 -1.39,8.63 -0.09,8.63c "/>
+                </group>
+                <group android:name="_R_G_L_1_G"
+                       android:translateX="23"
+                       android:translateY="32.125">
+                    <path android:name="_R_G_L_1_G_D_0_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="2"
+                          android:strokeAlpha="1"
+                          android:pathData=" M11.25 -0.64 C11.25,-0.64 11.25,13.64 11.25,13.64 C11.25,15.22 9.97,16.5 8.39,16.5 C8.39,16.5 -8.39,16.5 -8.39,16.5 C-9.97,16.5 -11.25,15.22 -11.25,13.64 C-11.25,13.64 -11.25,-0.64 -11.25,-0.64 C-11.25,-2.22 -9.97,-3.5 -8.39,-3.5 C-8.39,-3.5 8.39,-3.5 8.39,-3.5 C9.97,-3.5 11.25,-2.22 11.25,-0.64c "/>
+                </group>
+                <group android:name="_R_G_L_0_G"
+                       android:translateX="23"
+                       android:translateY="32.125">
+                    <path android:name="_R_G_L_0_G_D_0_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="2"
+                          android:strokeAlpha="1"
+                          android:pathData=" M5.88 -3.87 C5.88,-3.87 5.88,-10.2 5.88,-10.2 C5.88,-13.54 3.08,-16.25 -0.37,-16.25 C-3.83,-16.25 -6.62,-13.54 -6.62,-10.2 C-6.62,-10.2 -6.62,-3.87 -6.62,-3.87 "/>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_2_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="M-0.09 8.63 C1.2,8.63 2.25,7.57 2.25,6.28 C2.25,4.99 1.2,3.94 -0.09,3.94 C-1.39,3.94 -2.44,4.99 -2.44,6.28 C-2.44,7.57 -1.39,8.63 -0.09,8.63c "
+                                android:valueTo="M0 6.13 C0.97,6.13 1.75,5.34 1.75,4.38 C1.75,3.41 0.97,2.63 0,2.63 C-0.97,2.63 -1.75,3.41 -1.75,4.38 C-1.75,5.34 -0.97,6.13 0,6.13c "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.431,0 0.133,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="strokeWidth"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="2"
+                                android:valueTo="1.5"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.38,0 0.274,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="M11.25 -0.64 C11.25,-0.64 11.25,13.64 11.25,13.64 C11.25,15.22 9.97,16.5 8.39,16.5 C8.39,16.5 -8.39,16.5 -8.39,16.5 C-9.97,16.5 -11.25,15.22 -11.25,13.64 C-11.25,13.64 -11.25,-0.64 -11.25,-0.64 C-11.25,-2.22 -9.97,-3.5 -8.39,-3.5 C-8.39,-3.5 8.39,-3.5 8.39,-3.5 C9.97,-3.5 11.25,-2.22 11.25,-0.64c "
+                                android:valueTo="M7.88 -0.62 C7.88,-0.62 7.88,9.38 7.88,9.38 C7.88,10.48 6.98,11.38 5.88,11.38 C5.88,11.38 -5.87,11.38 -5.87,11.38 C-6.98,11.38 -7.87,10.48 -7.87,9.38 C-7.87,9.38 -7.87,-0.62 -7.87,-0.62 C-7.87,-1.73 -6.98,-2.62 -5.87,-2.62 C-5.87,-2.62 5.88,-2.62 5.88,-2.62 C6.98,-2.62 7.88,-1.73 7.88,-0.62c "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.431,0 0.133,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="strokeWidth"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="2"
+                                android:valueTo="1.5"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.38,0 0.274,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="333"
+                                android:startOffset="0"
+                                android:valueFrom="M5.88 -3.87 C5.88,-3.87 5.88,-10.2 5.88,-10.2 C5.88,-13.54 3.08,-16.25 -0.37,-16.25 C-3.83,-16.25 -6.62,-13.54 -6.62,-10.2 C-6.62,-10.2 -6.62,-3.87 -6.62,-3.87 "
+                                android:valueTo="M4.38 -2.62 C4.38,-2.62 4.38,-7.1 4.38,-7.1 C4.38,-9.46 2.42,-11.37 0,-11.37 C-2.42,-11.37 -4.37,-9.46 -4.37,-7.1 C-4.37,-7.1 -4.37,-2.62 -4.37,-2.62 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.431,0 0.133,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="translateX"
+                                android:duration="517"
+                                android:startOffset="0"
+                                android:valueFrom="0"
+                                android:valueTo="1"
+                                android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
diff --git a/packages/SystemUI/res/anim/lock_to_unlock.xml b/packages/SystemUI/res-keyguard/drawable/lock_to_unlock.xml
similarity index 91%
rename from packages/SystemUI/res/anim/lock_to_unlock.xml
rename to packages/SystemUI/res-keyguard/drawable/lock_to_unlock.xml
index 76f7a05..ab7e9d9 100644
--- a/packages/SystemUI/res/anim/lock_to_unlock.xml
+++ b/packages/SystemUI/res-keyguard/drawable/lock_to_unlock.xml
@@ -21,7 +21,7 @@
           <group android:name="_R_G_L_2_G_N_10_T_1" android:translateX="50.25" android:translateY="61">
             <group android:name="_R_G_L_2_G_N_10_T_0" android:translateX="-13.75" android:translateY="-7.5">
               <group android:name="_R_G_L_2_G" android:translateX="-0.375" android:translateY="-22.375">
-                <path android:name="_R_G_L_2_G_D_0_P_0" android:strokeColor="#b7f29f" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M4.75 15 C4.75,15 23.25,15 23.25,15 C24.35,15 25.25,15.9 25.25,17 C25.25,17 25.25,33 25.25,33 C25.25,34.1 24.35,35 23.25,35 C23.25,35 4.75,35 4.75,35 C3.65,35 2.75,34.1 2.75,33 C2.75,33 2.75,17 2.75,17 C2.75,15.9 3.65,15 4.75,15c " />
+                <path android:name="_R_G_L_2_G_D_0_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M4.75 15 C4.75,15 23.25,15 23.25,15 C24.35,15 25.25,15.9 25.25,17 C25.25,17 25.25,33 25.25,33 C25.25,34.1 24.35,35 23.25,35 C23.25,35 4.75,35 4.75,35 C3.65,35 2.75,34.1 2.75,33 C2.75,33 2.75,17 2.75,17 C2.75,15.9 3.65,15 4.75,15c " />
               </group>
             </group>
           </group>
@@ -30,7 +30,7 @@
           <group android:name="_R_G_L_1_G_N_10_T_1" android:translateX="50.25" android:translateY="61">
             <group android:name="_R_G_L_1_G_N_10_T_0" android:translateX="-13.75" android:translateY="-7.5">
               <group android:name="_R_G_L_1_G" android:translateX="5" android:translateY="-22.5">
-                <path android:name="_R_G_L_1_G_D_0_P_0" android:strokeColor="#b7f29f" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M2.5 15 C2.5,15 2.5,8.61 2.5,8.61 C2.5,5.24 5.3,2.5 8.75,2.5 C12.2,2.5 15,5.24 15,8.61 C15,8.61 15,15 15,15 " />
+                <path android:name="_R_G_L_1_G_D_0_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M2.5 15 C2.5,15 2.5,8.61 2.5,8.61 C2.5,5.24 5.3,2.5 8.75,2.5 C12.2,2.5 15,5.24 15,8.61 C15,8.61 15,15 15,15 " />
               </group>
             </group>
           </group>
@@ -39,7 +39,7 @@
           <group android:name="_R_G_L_0_G_N_10_T_1" android:translateX="50.25" android:translateY="61">
             <group android:name="_R_G_L_0_G_N_10_T_0" android:translateX="-13.75" android:translateY="-7.5">
               <group android:name="_R_G_L_0_G" android:translateX="11" android:translateY="-0.25" android:pivotX="2.75" android:pivotY="2.75" android:scaleX="1" android:scaleY="1">
-                <path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#b7f29f" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c " />
+                <path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#FF000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c " />
               </group>
             </group>
           </group>
diff --git a/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml b/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml
new file mode 100644
index 0000000..7f0f68f
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<animated-selector
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <!--
+        State corresponds with the following icons:
+            state_first => lock icon
+            state_middle => fingerprint icon
+            state_last => unlocked icon
+
+        state_single
+            = true => AOD
+            = false => LS
+    -->
+
+    <item
+        android:id="@+id/locked"
+        android:drawable="@drawable/ic_lock"
+        android:state_first="true"
+        android:state_single="false"/>
+
+    <item
+        android:id="@+id/locked_fp"
+        android:state_middle="true"
+        android:state_single="false"
+        android:drawable="@drawable/ic_fingerprint" />
+
+    <item
+        android:id="@+id/unlocked"
+        android:state_last="true"
+        android:state_single="false"
+        android:drawable="@drawable/ic_unlocked" />
+
+    <item
+        android:id="@+id/locked_aod"
+        android:state_first="true"
+        android:state_single="true"
+        android:drawable="@drawable/ic_lock_aod" />
+
+    <item
+        android:id="@+id/no_icon"
+        android:drawable="@color/transparent" />
+
+    <transition
+        android:fromId="@id/locked"
+        android:toId="@id/unlocked"
+        android:drawable="@drawable/lock_to_unlock" />
+
+    <transition
+        android:fromId="@id/locked_fp"
+        android:toId="@id/unlocked"
+        android:drawable="@drawable/fp_to_unlock" />
+
+    <transition
+        android:fromId="@id/unlocked"
+        android:toId="@id/locked_fp"
+        android:drawable="@drawable/unlock_to_fp" />
+
+    <transition
+        android:fromId="@id/locked_aod"
+        android:toId="@id/locked"
+        android:drawable="@drawable/lock_aod_to_ls" />
+
+    <transition
+        android:fromId="@id/locked"
+        android:toId="@id/locked_aod"
+        android:drawable="@drawable/lock_ls_to_aod" />
+</animated-selector>
diff --git a/packages/SystemUI/res-keyguard/drawable/unlock_to_fp.xml b/packages/SystemUI/res-keyguard/drawable/unlock_to_fp.xml
new file mode 100644
index 0000000..620c71a
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/unlock_to_fp.xml
@@ -0,0 +1,298 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<animated-vector xmlns:aapt="http://schemas.android.com/aapt"
+                 xmlns:android="http://schemas.android.com/apk/res/android">
+    <aapt:attr name="android:drawable">
+        <vector android:height="65dp"
+                android:width="46dp"
+                android:viewportHeight="65"
+                android:viewportWidth="46">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_1_G"
+                       android:translateX="3.75"
+                       android:translateY="8.25">
+                    <path android:name="_R_G_L_1_G_D_0_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="2"
+                          android:strokeAlpha="1"
+                          android:pathData=" M30.64 30.14 C30.64,30.14 30.64,38.14 30.64,38.14 C30.64,38.77 30.36,39.32 29.91,39.69 C29.57,39.97 29.12,40.14 28.64,40.14 C28.64,40.14 10.14,40.14 10.14,40.14 C9.04,40.14 8.14,39.25 8.14,38.14 C8.14,38.14 8.14,30.14 8.14,30.14 "/>
+                    <path android:name="_R_G_L_1_G_D_1_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="2"
+                          android:strokeAlpha="0"
+                          android:pathData=" M19.42 31.53 C18.15,31.52 18.11,30.33 18.11,30.33 C18.11,29.59 18.66,28.98 19.4,28.98 C20.13,28.98 20.69,29.59 20.69,30.33 C20.69,30.33 20.69,30.37 20.69,30.37 C20.69,30.64 20.49,30.87 20.25,30.87 C20.07,30.87 19.91,30.74 19.84,30.55 C19.84,30.55 19.69,30.14 19.69,30.14 C19.63,29.94 19.46,29.82 19.28,29.82 C19.04,29.82 18.61,30.02 18.61,30.29 C18.61,30.43 18.6,30.75 18.76,31.03 C18.87,31.21 19.21,31.77 19.96,31.41 C20.69,31.01 20.69,30.34 20.69,30.34 "/>
+                    <path android:name="_R_G_L_1_G_D_2_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="2"
+                          android:strokeAlpha="1"
+                          android:pathData=" M8.14 30.22 C8.14,30.22 8.14,22.22 8.14,22.22 C8.14,21.71 8.33,21.25 8.64,20.9 C9,20.48 9.54,20.22 10.14,20.22 C10.14,20.22 28.64,20.22 28.64,20.22 C29.75,20.22 30.64,21.11 30.64,22.22 C30.64,22.22 30.64,30.14 30.64,30.14 "/>
+                    <path android:name="_R_G_L_1_G_D_3_P_0"
+                          android:strokeColor="#FF000000"
+                          android:strokeLineCap="round"
+                          android:strokeLineJoin="round"
+                          android:strokeWidth="2"
+                          android:strokeAlpha="1"
+                          android:pathData=" M37.91 20.05 C37.91,20.05 37.89,14.16 37.89,14.16 C37.89,10.79 35.15,8.05 31.86,8.03 C28.46,8.01 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 "/>
+                </group>
+                <group android:name="_R_G_L_0_G"
+                       android:translateX="20.357"
+                       android:translateY="35.75"
+                       android:pivotX="2.75"
+                       android:pivotY="2.75"
+                       android:scaleX="1"
+                       android:scaleY="1">
+                    <path android:name="_R_G_L_0_G_D_0_P_0"
+                          android:fillColor="#FF000000"
+                          android:fillAlpha="1"
+                          android:fillType="nonZero"
+                          android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c "/>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="183"
+                                android:startOffset="0"
+                                android:valueFrom="M30.64 30.14 C30.64,30.14 30.64,38.14 30.64,38.14 C30.64,38.77 30.36,39.32 29.91,39.69 C29.57,39.97 29.12,40.14 28.64,40.14 C28.64,40.14 10.14,40.14 10.14,40.14 C9.04,40.14 8.14,39.25 8.14,38.14 C8.14,38.14 8.14,30.14 8.14,30.14 "
+                                android:valueTo="M30.64 30.14 C30.64,30.14 30.64,38.14 30.64,38.14 C30.64,38.77 30.36,39.32 29.91,39.69 C29.57,39.97 29.12,40.14 28.64,40.14 C28.64,40.14 10.14,40.14 10.14,40.14 C9.04,40.14 8.14,39.25 8.14,38.14 C8.14,38.14 8.14,30.14 8.14,30.14 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.15,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="133"
+                                android:startOffset="183"
+                                android:valueFrom="M30.64 30.14 C30.64,30.14 30.64,38.14 30.64,38.14 C30.64,38.77 30.36,39.32 29.91,39.69 C29.57,39.97 29.12,40.14 28.64,40.14 C28.64,40.14 10.14,40.14 10.14,40.14 C9.04,40.14 8.14,39.25 8.14,38.14 C8.14,38.14 8.14,30.14 8.14,30.14 "
+                                android:valueTo="M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.15,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="strokeAlpha"
+                                android:duration="183"
+                                android:startOffset="0"
+                                android:valueFrom="0"
+                                android:valueTo="0"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="strokeAlpha"
+                                android:duration="33"
+                                android:startOffset="183"
+                                android:valueFrom="0"
+                                android:valueTo="1"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="183"
+                                android:startOffset="0"
+                                android:valueFrom="M19.42 31.53 C18.15,31.52 18.11,30.33 18.11,30.33 C18.11,29.59 18.66,28.98 19.4,28.98 C20.13,28.98 20.69,29.59 20.69,30.33 C20.69,30.33 20.69,30.37 20.69,30.37 C20.69,30.64 20.49,30.87 20.25,30.87 C20.07,30.87 19.91,30.74 19.84,30.55 C19.84,30.55 19.69,30.14 19.69,30.14 C19.63,29.94 19.46,29.82 19.28,29.82 C19.04,29.82 18.61,30.02 18.61,30.29 C18.61,30.43 18.6,30.75 18.76,31.03 C18.87,31.21 19.21,31.77 19.96,31.41 C20.69,31.01 20.69,30.34 20.69,30.34 "
+                                android:valueTo="M19.42 31.53 C18.15,31.52 18.11,30.33 18.11,30.33 C18.11,29.59 18.66,28.98 19.4,28.98 C20.13,28.98 20.69,29.59 20.69,30.33 C20.69,30.33 20.69,30.37 20.69,30.37 C20.69,30.64 20.49,30.87 20.25,30.87 C20.07,30.87 19.91,30.74 19.84,30.55 C19.84,30.55 19.69,30.14 19.69,30.14 C19.63,29.94 19.46,29.82 19.28,29.82 C19.04,29.82 18.61,30.02 18.61,30.29 C18.61,30.43 18.6,30.75 18.76,31.03 C18.87,31.21 19.21,31.77 19.96,31.41 C20.69,31.01 20.69,30.34 20.69,30.34 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.15,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="133"
+                                android:startOffset="183"
+                                android:valueFrom="M19.42 31.53 C18.15,31.52 18.11,30.33 18.11,30.33 C18.11,29.59 18.66,28.98 19.4,28.98 C20.13,28.98 20.69,29.59 20.69,30.33 C20.69,30.33 20.69,30.37 20.69,30.37 C20.69,30.64 20.49,30.87 20.25,30.87 C20.07,30.87 19.91,30.74 19.84,30.55 C19.84,30.55 19.69,30.14 19.69,30.14 C19.63,29.94 19.46,29.82 19.28,29.82 C19.04,29.82 18.61,30.02 18.61,30.29 C18.61,30.43 18.6,30.75 18.76,31.03 C18.87,31.21 19.21,31.77 19.96,31.41 C20.69,31.01 20.69,30.34 20.69,30.34 "
+                                android:valueTo="M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.15,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_2_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="183"
+                                android:startOffset="0"
+                                android:valueFrom="M8.14 30.22 C8.14,30.22 8.14,22.22 8.14,22.22 C8.14,21.71 8.33,21.25 8.64,20.9 C9,20.48 9.54,20.22 10.14,20.22 C10.14,20.22 28.64,20.22 28.64,20.22 C29.75,20.22 30.64,21.11 30.64,22.22 C30.64,22.22 30.64,30.14 30.64,30.14 "
+                                android:valueTo="M8.14 30.22 C8.14,30.22 8.14,22.22 8.14,22.22 C8.14,21.71 8.33,21.25 8.64,20.9 C9,20.48 9.54,20.22 10.14,20.22 C10.14,20.22 28.64,20.22 28.64,20.22 C29.75,20.22 30.64,21.11 30.64,22.22 C30.64,22.22 30.64,30.14 30.64,30.14 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.15,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="133"
+                                android:startOffset="183"
+                                android:valueFrom="M8.14 30.22 C8.14,30.22 8.14,22.22 8.14,22.22 C8.14,21.71 8.33,21.25 8.64,20.9 C9,20.48 9.54,20.22 10.14,20.22 C10.14,20.22 28.64,20.22 28.64,20.22 C29.75,20.22 30.64,21.11 30.64,22.22 C30.64,22.22 30.64,30.14 30.64,30.14 "
+                                android:valueTo="M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.15,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_3_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="150"
+                                android:startOffset="0"
+                                android:valueFrom="M37.91 20.05 C37.91,20.05 37.89,14.16 37.89,14.16 C37.89,10.79 35.15,8.05 31.86,8.03 C28.46,8.01 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 "
+                                android:valueTo="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.261,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="33"
+                                android:startOffset="150"
+                                android:valueFrom="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 "
+                                android:valueTo="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.123,0 0.261,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="133"
+                                android:startOffset="183"
+                                android:valueFrom="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 "
+                                android:valueTo="M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 "
+                                android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.123,0 0.15,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="fillAlpha"
+                                android:duration="200"
+                                android:startOffset="0"
+                                android:valueFrom="1"
+                                android:valueTo="1"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="fillAlpha"
+                                android:duration="17"
+                                android:startOffset="200"
+                                android:valueFrom="1"
+                                android:valueTo="0"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="scaleX"
+                                android:duration="183"
+                                android:startOffset="0"
+                                android:valueFrom="1"
+                                android:valueTo="1"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.596,0 0.018,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="scaleY"
+                                android:duration="183"
+                                android:startOffset="0"
+                                android:valueFrom="1"
+                                android:valueTo="1"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.596,0 0.018,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="scaleX"
+                                android:duration="67"
+                                android:startOffset="183"
+                                android:valueFrom="1"
+                                android:valueTo="1.4186600000000003"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.596,0 0.018,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="scaleY"
+                                android:duration="67"
+                                android:startOffset="183"
+                                android:valueFrom="1"
+                                android:valueTo="1.4186600000000003"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.596,0 0.018,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="translateX"
+                                android:duration="433"
+                                android:startOffset="0"
+                                android:valueFrom="0"
+                                android:valueTo="1"
+                                android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
new file mode 100644
index 0000000..dfc3e63
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+-->
+
+<!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc -->
+<com.android.systemui.qs.FooterActionsView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"
+    android:gravity="center_vertical">
+
+    <com.android.systemui.statusbar.AlphaOptimizedImageView
+        android:id="@android:id/edit"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/qs_footer_action_button_size"
+        android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
+        android:layout_weight="1"
+        android:background="@drawable/qs_footer_action_chip_background"
+        android:clickable="true"
+        android:clipToPadding="false"
+        android:contentDescription="@string/accessibility_quick_settings_edit"
+        android:focusable="true"
+        android:padding="@dimen/qs_footer_icon_padding"
+        android:src="@*android:drawable/ic_mode_edit"
+        android:tint="?android:attr/textColorPrimary" />
+
+    <com.android.systemui.statusbar.phone.MultiUserSwitch
+        android:id="@+id/multi_user_switch"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/qs_footer_action_button_size"
+        android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
+        android:layout_weight="1"
+        android:background="@drawable/qs_footer_action_chip_background"
+        android:focusable="true">
+
+        <ImageView
+            android:id="@+id/multi_user_avatar"
+            android:layout_width="@dimen/multi_user_avatar_expanded_size"
+            android:layout_height="@dimen/multi_user_avatar_expanded_size"
+            android:layout_gravity="center"
+            android:scaleType="centerInside" />
+    </com.android.systemui.statusbar.phone.MultiUserSwitch>
+
+    <com.android.systemui.statusbar.AlphaOptimizedImageView
+        android:id="@+id/pm_lite"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/qs_footer_action_button_size"
+        android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
+        android:layout_weight="1"
+        android:background="@drawable/qs_footer_action_chip_background"
+        android:clickable="true"
+        android:clipToPadding="false"
+        android:focusable="true"
+        android:padding="@dimen/qs_footer_icon_padding"
+        android:src="@*android:drawable/ic_lock_power_off"
+        android:contentDescription="@string/accessibility_quick_settings_power_menu"
+        android:tint="?android:attr/textColorPrimary" />
+
+    <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
+        android:id="@+id/settings_button_container"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/qs_footer_action_button_size"
+        android:background="@drawable/qs_footer_action_chip_background"
+        android:layout_weight="1"
+        android:clipChildren="false"
+        android:clipToPadding="false">
+
+        <com.android.systemui.statusbar.phone.SettingsButton
+            android:id="@+id/settings_button"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/qs_footer_action_button_size"
+            android:layout_gravity="center"
+            android:contentDescription="@string/accessibility_quick_settings_settings"
+            android:background="@drawable/qs_footer_action_chip_background_borderless"
+            android:padding="@dimen/qs_footer_icon_padding"
+            android:scaleType="centerInside"
+            android:src="@drawable/ic_settings"
+            android:tint="?android:attr/textColorPrimary" />
+
+        <com.android.systemui.statusbar.AlphaOptimizedImageView
+            android:id="@+id/tuner_icon"
+            android:layout_width="8dp"
+            android:layout_height="8dp"
+            android:layout_gravity="center_horizontal|bottom"
+            android:layout_marginBottom="@dimen/qs_footer_icon_padding"
+            android:src="@drawable/tuner"
+            android:tint="?android:attr/textColorTertiary"
+            android:visibility="invisible" />
+
+    </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
+
+</com.android.systemui.qs.FooterActionsView>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 28c6166..87a9825 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -51,7 +51,7 @@
         android:id="@+id/lockscreen_clock_view_large"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_below="@id/keyguard_status_area"
+        android:layout_below="@id/keyguard_slice_view"
         android:visibility="gone">
         <com.android.keyguard.AnimatableClockView
             android:id="@+id/animatable_clock_view_large"
@@ -68,19 +68,28 @@
             lockScreenWeight="400"
         />
     </FrameLayout>
-    <include layout="@layout/keyguard_status_area"
+
+    <!-- Not quite optimal but needed to translate these items as a group. The
+         NotificationIconContainer has its own logic for translation. -->
+    <LinearLayout
         android:id="@+id/keyguard_status_area"
+        android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alignParentStart="true"
-        android:layout_below="@id/lockscreen_clock_view" />
+        android:layout_below="@id/lockscreen_clock_view">
 
-    <com.android.systemui.statusbar.phone.NotificationIconContainer
-        android:id="@+id/left_aligned_notification_icon_container"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/notification_shelf_height"
-        android:layout_below="@id/keyguard_status_area"
-        android:paddingStart="@dimen/below_clock_padding_start_icons"
-        android:visibility="invisible"
-    />
+      <include layout="@layout/keyguard_slice_view"
+               android:id="@+id/keyguard_slice_view"
+               android:layout_width="match_parent"
+               android:layout_height="wrap_content" />
+
+      <com.android.systemui.statusbar.phone.NotificationIconContainer
+          android:id="@+id/left_aligned_notification_icon_container"
+          android:layout_width="match_parent"
+          android:layout_height="@dimen/notification_shelf_height"
+          android:paddingStart="@dimen/below_clock_padding_start_icons"
+          android:visibility="invisible"
+          />
+    </LinearLayout>
 </com.android.keyguard.KeyguardClockSwitch>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
index ce63082..f613a19 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
@@ -27,49 +27,44 @@
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:clipChildren="false"
-    android:clipToPadding="false"
     androidprv:layout_maxWidth="@dimen/keyguard_security_width"
-    androidprv:layout_maxHeight="@dimen/keyguard_security_height"
-    android:gravity="center_horizontal">
+    android:clipChildren="false"
+    android:clipToPadding="false">
 
-    <FrameLayout
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/pattern_container"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:clipChildren="false"
-        android:clipToPadding="false">
-
-        <LinearLayout
-            android:id="@+id/pattern_container"
-            android:layout_height="wrap_content"
+        android:layout_height="0dp"
+        android:layout_marginBottom="8dp"
+        android:layout_weight="1"
+        android:layoutDirection="ltr">
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/pattern_top_guideline"
             android:layout_width="wrap_content"
-            android:orientation="vertical"
-            android:layout_gravity="center_horizontal|bottom"
-            android:clipChildren="false"
-            android:clipToPadding="false">
+            android:layout_height="wrap_content"
+            androidprv:layout_constraintGuide_percent="0"
+            android:orientation="horizontal" />
 
-            <com.android.internal.widget.LockPatternView
-                android:id="@+id/lockPatternView"
-                android:layout_width="match_parent"
-                android:layout_height="0dp"
-                android:layout_weight="1"
-                android:layout_marginEnd="8dip"
-                android:layout_marginBottom="4dip"
-                android:layout_marginStart="8dip"
-                android:layout_gravity="center_horizontal"
-                android:gravity="center"
-                android:clipChildren="false"
-                android:clipToPadding="false" />
+        <com.android.internal.widget.LockPatternView
+            android:id="@+id/lockPatternView"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:layout_constraintTop_toBottomOf="@id/pattern_top_guideline"
+            androidprv:layout_constraintBottom_toBottomOf="parent"
+            androidprv:layout_constraintLeft_toLeftOf="parent"
+            androidprv:layout_constraintRight_toRightOf="parent"
+            androidprv:layout_constraintDimensionRatio="1.0"
+            androidprv:layout_constraintVertical_bias="1.0"
+            />
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
-          <include layout="@layout/keyguard_eca"
-              android:id="@+id/keyguard_selector_fade_container"
-              android:layout_width="match_parent"
-              android:layout_height="wrap_content"
-              android:orientation="vertical"
-              android:layout_gravity="bottom|center_horizontal"
-              android:layout_marginTop="@dimen/keyguard_eca_top_margin"
-              android:gravity="center_horizontal" />
-        </LinearLayout>
-    </FrameLayout>
+    <include layout="@layout/keyguard_eca"
+        android:id="@+id/keyguard_selector_fade_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_gravity="bottom|center_horizontal"
+        android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+        android:gravity="center_horizontal" />
 
 </com.android.keyguard.KeyguardPatternView>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index 02cb2bc..a946318 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -20,171 +20,174 @@
 <com.android.keyguard.KeyguardPINView
         xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:androidprv="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
         android:id="@+id/keyguard_pin_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         androidprv:layout_maxWidth="@dimen/keyguard_security_width"
         android:orientation="vertical"
         >
-    <LinearLayout
-            android:id="@+id/pin_container"
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/pin_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginBottom="8dp"
+        android:layout_weight="1"
+        android:layoutDirection="ltr"
+        android:orientation="vertical">
+
+        <!-- Set this to be just above key1. It would be better to introduce a barrier above
+             key1/key2/key3, then place this View above that. Sadly, that doesn't work (the Barrier
+             drops to the bottom of the page, and key1/2/3 all shoot up to the top-left). In any
+             case, the Flow should ensure that key1/2/3 all have the same top, so this should be
+             fine. -->
+        <com.android.keyguard.AlphaOptimizedRelativeLayout
+            android:id="@+id/row0"
             android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:orientation="vertical"
-            android:layout_weight="1"
-            android:layoutDirection="ltr"
-            android:layout_marginBottom="8dp"
-            >
-      <Space
-          android:layout_width="match_parent"
-          android:layout_height="0dp"
-          android:layout_weight="1"
-          />
-      <com.android.keyguard.AlphaOptimizedRelativeLayout
-          android:id="@+id/row0"
-          android:layout_width="match_parent"
-          android:layout_height="wrap_content"
-          android:paddingBottom="@dimen/num_pad_entry_row_margin_bottom"
-          >
+            android:layout_height="wrap_content"
+            android:paddingBottom="@dimen/num_pad_entry_row_margin_bottom"
+            androidprv:layout_constraintEnd_toEndOf="parent"
+            androidprv:layout_constraintStart_toStartOf="parent"
+
+            androidprv:layout_constraintTop_toTopOf="parent"
+            androidprv:layout_constraintBottom_toTopOf="@id/key1"
+            androidprv:layout_constraintVertical_bias="0.0">
+
             <com.android.keyguard.PasswordTextView
-                    android:id="@+id/pinEntry"
-                    android:layout_width="@dimen/keyguard_security_width"
-                    android:layout_height="@dimen/keyguard_password_height"
-                    style="@style/Widget.TextView.Password"
-                    android:layout_centerHorizontal="true"
-                    android:layout_marginRight="72dp"
-                    androidprv:scaledTextSize="@integer/scaled_password_text_size"
-                    android:contentDescription="@string/keyguard_accessibility_pin_area"
-                    />
+                android:id="@+id/pinEntry"
+                style="@style/Widget.TextView.Password"
+                android:layout_width="@dimen/keyguard_security_width"
+                android:layout_height="@dimen/keyguard_password_height"
+                android:layout_centerHorizontal="true"
+                android:layout_marginRight="72dp"
+                android:contentDescription="@string/keyguard_accessibility_pin_area"
+                androidprv:scaledTextSize="@integer/scaled_password_text_size" />
         </com.android.keyguard.AlphaOptimizedRelativeLayout>
-        <LinearLayout
-                android:id="@+id/row1"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key1"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="1"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key2"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="2"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key3"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="3"
-                    />
-        </LinearLayout>
-        <LinearLayout
-                android:id="@+id/row2"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key4"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="4"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key5"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="5"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key6"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="6"
-                    />
-        </LinearLayout>
-        <LinearLayout
-                android:id="@+id/row3"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="@dimen/num_pad_row_margin_bottom"
-                >
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key7"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="7"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key8"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="8"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key9"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="9"
-                    />
-        </LinearLayout>
-        <LinearLayout
-                android:id="@+id/row4"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:layout_gravity="center_horizontal"
-                >
-            <com.android.keyguard.NumPadButton
-                    android:id="@+id/delete_button"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    android:contentDescription="@string/keyboardview_keycode_delete"
-                    style="@style/NumPadKey.Delete"
-                    />
-            <com.android.keyguard.NumPadKey
-                    android:id="@+id/key0"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    android:layout_marginEnd="@dimen/num_pad_key_margin_end"
-                    androidprv:textView="@+id/pinEntry"
-                    androidprv:digit="0"
-                    />
-            <com.android.keyguard.NumPadButton
-                    android:id="@+id/key_enter"
-                    android:layout_width="@dimen/num_pad_key_width"
-                    android:layout_height="match_parent"
-                    style="@style/NumPadKey.Enter"
-                    android:contentDescription="@string/keyboardview_keycode_enter"
-                    />
-        </LinearLayout>
-    </LinearLayout>
+
+        <!-- Guideline used to place the top row of keys relative to the screen height. This will be
+             updated in KeyguardPINView to reduce the height of the PIN pad. -->
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/pin_pad_top_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            androidprv:layout_constraintGuide_percent="0"
+            android:orientation="horizontal" />
+
+        <androidx.constraintlayout.helper.widget.Flow
+            android:id="@+id/flow1"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:orientation="horizontal"
+
+            androidprv:constraint_referenced_ids="key1,key2,key3,key4,key5,key6,key7,key8,key9,delete_button,key0,key_enter"
+
+            androidprv:flow_horizontalGap="@dimen/num_pad_key_margin_end"
+
+            androidprv:flow_horizontalStyle="packed"
+            androidprv:flow_maxElementsWrap="3"
+
+            androidprv:flow_verticalBias="1.0"
+            androidprv:flow_verticalGap="@dimen/num_pad_entry_row_margin_bottom"
+            androidprv:flow_verticalStyle="packed"
+
+            androidprv:flow_wrapMode="aligned"
+            androidprv:layout_constraintBottom_toBottomOf="parent"
+            androidprv:layout_constraintEnd_toEndOf="parent"
+            androidprv:layout_constraintStart_toStartOf="parent"
+            androidprv:layout_constraintTop_toBottomOf="@id/pin_pad_top_guideline" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key1"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="1"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key2"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="2"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key3"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="3"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key4"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="4"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key5"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="5"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key6"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="6"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key7"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="7"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key8"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="8"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key9"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="9"
+            androidprv:textView="@+id/pinEntry" />
+
+
+        <com.android.keyguard.NumPadButton
+            android:id="@+id/delete_button"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            style="@style/NumPadKey.Delete"
+            android:contentDescription="@string/keyboardview_keycode_delete"
+            />
+
+        <com.android.keyguard.NumPadKey
+            android:id="@+id/key0"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            androidprv:digit="0"
+            androidprv:textView="@+id/pinEntry" />
+
+        <com.android.keyguard.NumPadButton
+            android:id="@+id/key_enter"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            style="@style/NumPadKey.Enter"
+            android:contentDescription="@string/keyboardview_keycode_enter"
+            />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+
     <include layout="@layout/keyguard_eca"
              android:id="@+id/keyguard_selector_fade_container"
              android:layout_width="match_parent"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml
similarity index 89%
rename from packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml
rename to packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml
index 95eb5c1..1863d11 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml
@@ -22,11 +22,10 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_gravity="center_horizontal"
+    android:layout_gravity="start"
     android:clipToPadding="false"
     android:orientation="vertical"
-    android:paddingStart="@dimen/below_clock_padding_start"
-    android:layout_centerHorizontal="true">
+    android:paddingStart="@dimen/below_clock_padding_start">
     <TextView
               android:id="@+id/title"
               android:layout_width="match_parent"
@@ -42,6 +41,6 @@
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:orientation="horizontal"
-              android:gravity="center"
+              android:gravity="start"
     />
 </com.android.keyguard.KeyguardSliceView>
diff --git a/packages/SystemUI/res-keyguard/values-land/donottranslate.xml b/packages/SystemUI/res-keyguard/values-land/donottranslate.xml
new file mode 100644
index 0000000..9912b69
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/values-land/donottranslate.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="num_pad_key_ratio">1.51</string>
+</resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml
index c34012d..d816b3a 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml
@@ -21,4 +21,8 @@
 
     <!-- Overload default clock widget parameters -->
     <dimen name="widget_big_font_size">88dp</dimen>
+
+    <dimen name="qs_header_system_icons_area_height">0dp</dimen>
+    <dimen name="qs_panel_padding_top">0dp</dimen>
+
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res-keyguard/values-sw600dp-land/donottranslate.xml
similarity index 67%
copy from packages/SystemUI/res/drawable/media_output_dialog_background.xml
copy to packages/SystemUI/res-keyguard/values-sw600dp-land/donottranslate.xml
index 3ceb0f6..1a52e93 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_background.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp-land/donottranslate.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
+  ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -15,9 +15,7 @@
   ~ limitations under the License.
   -->
 
-<inset xmlns:android="http://schemas.android.com/apk/res/android">
-    <shape android:shape="rectangle">
-        <corners android:radius="8dp" />
-        <solid android:color="?android:attr/colorBackground" />
-    </shape>
-</inset>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Don't use the smaller PIN pad keys if we have the screen space to support it. -->
+    <string name="num_pad_key_ratio">1.0</string>
+</resources>
diff --git a/packages/SystemUI/res/values-h560dp-xhdpi/config.xml b/packages/SystemUI/res-keyguard/values-sw720dp/bools.xml
similarity index 69%
rename from packages/SystemUI/res/values-h560dp-xhdpi/config.xml
rename to packages/SystemUI/res-keyguard/values-sw720dp/bools.xml
index cf2017f..e09bf7e 100644
--- a/packages/SystemUI/res/values-h560dp-xhdpi/config.xml
+++ b/packages/SystemUI/res-keyguard/values-sw720dp/bools.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
-  ~ Copyright (C) 2014 The Android Open Source Project
+  ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -13,11 +12,9 @@
   ~ distributed under the License is distributed on an "AS IS" BASIS,
   ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
+  ~ limitations under the License.
   -->
-<resources>
-    <!-- The maximum count of notifications on Keyguard. The rest will be collapsed in an overflow
-     card. -->
-    <integer name="keyguard_max_notification_count">3</integer>
-</resources>
 
+<resources>
+    <bool name="can_use_one_handed_bouncer">true</bool>
+</resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 7e3c87b..e854b02 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -98,4 +98,17 @@
     <dimen name="below_clock_padding_start">32dp</dimen>
     <dimen name="below_clock_padding_end">16dp</dimen>
     <dimen name="below_clock_padding_start_icons">28dp</dimen>
+
+    <!-- Proportion of the screen height to use to set the maximum height of the bouncer to when
+         the device is in the DEVICE_POSTURE_HALF_OPENED posture, for the PIN/pattern entry. 0 will
+         allow it to use the whole screen space, 0.6 will allow it to use just under half of the
+         screen. -->
+    <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
+
+    <!-- The actual amount of translation that is applied to the bouncer when it animates from one
+         side of the screen to the other in one-handed mode. Note that it will always translate from
+         the side of the screen to the other (it will "jump" closer to the destination while the
+         opacity is zero), but this controls how much motion will actually be applied to it while
+         animating. Larger values will cause it to move "faster" while fading out/in. -->
+    <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/donottranslate.xml b/packages/SystemUI/res-keyguard/values/donottranslate.xml
index 1934457..052d329 100644
--- a/packages/SystemUI/res-keyguard/values/donottranslate.xml
+++ b/packages/SystemUI/res-keyguard/values/donottranslate.xml
@@ -29,4 +29,6 @@
 
     <!-- Skeleton string format for displaying the time in 24-hour format. -->
     <string name="clock_24hr_format">Hm</string>
+
+    <string name="num_pad_key_ratio">1</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index 098b7e8..6e89fb0 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -17,7 +17,7 @@
 */
 -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- Keyguard PIN pad styles -->
     <style name="Keyguard.TextView" parent="@android:style/Widget.DeviceDefault.TextView">
         <item name="android:textSize">@dimen/kg_status_line_font_size</item>
@@ -37,6 +37,10 @@
       <item name="android:colorControlNormal">@null</item>
       <item name="android:colorControlHighlight">?android:attr/colorAccent</item>
       <item name="android:background">@drawable/num_pad_key_background</item>
+
+      <!-- Default values for NumPadKey used in a ConstraintLayout. -->
+      <item name="layout_constraintDimensionRatio">@string/num_pad_key_ratio</item>
+      <item name="layout_constraintWidth_max">@dimen/num_pad_key_width</item>
     </style>
     <style name="Widget.TextView.NumPadKey.Digit"
            parent="@android:style/Widget.DeviceDefault.TextView">
@@ -54,12 +58,12 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
     <style name="NumPadKey.Delete">
-        <item name="android:colorControlNormal">?android:attr/textColorSecondary</item>
+        <item name="android:colorControlNormal">@color/numpad_key_color_secondary</item>
         <item name="android:src">@drawable/ic_backspace_24dp</item>
     </style>
     <style name="NumPadKey.Enter">
-      <item name="android:colorControlNormal">?android:attr/textColorSecondary</item>
-      <item name="android:src">@drawable/ic_keyboard_tab_36dp</item>
+        <item name="android:colorControlNormal">@color/numpad_key_color_secondary</item>
+        <item name="android:src">@drawable/ic_keyboard_tab_36dp</item>
     </style>
     <style name="Widget.TextView.NumPadKey.Klondike"
            parent="@android:style/Widget.DeviceDefault.TextView">
diff --git a/packages/SystemUI/res/color/docked_divider_background.xml b/packages/SystemUI/res/color/docked_divider_background.xml
new file mode 100644
index 0000000..2ab8ecd
--- /dev/null
+++ b/packages/SystemUI/res/color/docked_divider_background.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:color="@android:color/system_neutral1_500" android:lStar="35" />
+</selector>
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res/color/prv_color_surface.xml
similarity index 67%
copy from packages/SystemUI/res/drawable/media_output_dialog_background.xml
copy to packages/SystemUI/res/color/prv_color_surface.xml
index 3ceb0f6..b9d016c 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_background.xml
+++ b/packages/SystemUI/res/color/prv_color_surface.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
+  ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,10 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android">
-    <shape android:shape="rectangle">
-        <corners android:radius="8dp" />
-        <solid android:color="?android:attr/colorBackground" />
-    </shape>
-</inset>
+<selector xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+          xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?androidprv:attr/colorSurface" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res/color/prv_text_color_on_accent.xml
similarity index 67%
copy from packages/SystemUI/res/drawable/media_output_dialog_background.xml
copy to packages/SystemUI/res/color/prv_text_color_on_accent.xml
index 3ceb0f6..9f44aca 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_background.xml
+++ b/packages/SystemUI/res/color/prv_text_color_on_accent.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2020 The Android Open Source Project
+  ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,10 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android">
-    <shape android:shape="rectangle">
-        <corners android:radius="8dp" />
-        <solid android:color="?android:attr/colorBackground" />
-    </shape>
-</inset>
+<selector xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+          xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?androidprv:attr/textColorOnAccent" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_qs_drag_handle.xml b/packages/SystemUI/res/drawable/ic_qs_drag_handle.xml
new file mode 100644
index 0000000..9a69b33
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_drag_handle.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="36dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="?android:attr/textColorPrimary"
+        android:pathData="M5.41,7.59L4,9l8,8 8,-8 -1.41,-1.41L12,14.17" />
+</vector>
diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
index 1535e72..3a08a71 100644
--- a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
+++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
@@ -22,11 +22,12 @@
     android:color="?android:attr/textColorPrimary">
   <item>
     <shape
-        android:shape="oval">
+        android:shape="rectangle">
       <solid android:color="?androidprv:attr/colorSurface"/>
       <size
           android:width="@dimen/keyguard_affordance_width"
           android:height="@dimen/keyguard_affordance_height"/>
+      <corners android:radius="@dimen/keyguard_affordance_fixed_radius"/>
     </shape>
   </item>
 </ripple>
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_button_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_button_background.xml
new file mode 100644
index 0000000..363a022
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_button_background.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:shape="rectangle">
+    <stroke
+        android:color="?androidprv:attr/colorAccentPrimaryVariant"
+        android:width="1dp"/>
+    <corners android:radius="20dp"/>
+    <padding
+        android:left="16dp"
+        android:right="16dp"
+        android:top="8dp"
+        android:bottom="8dp" />
+    <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
new file mode 100644
index 0000000..1a128df
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:insetTop="@dimen/qs_dialog_button_vertical_inset"
+       android:insetBottom="@dimen/qs_dialog_button_vertical_inset">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/white"/>
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <solid android:color="?androidprv:attr/colorAccentPrimary"/>
+                <padding android:left="@dimen/qs_dialog_button_horizontal_padding"
+                         android:top="@dimen/qs_dialog_button_vertical_padding"
+                         android:right="@dimen/qs_dialog_button_horizontal_padding"
+                         android:bottom="@dimen/qs_dialog_button_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
new file mode 100644
index 0000000..467c20f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:insetTop="@dimen/qs_dialog_button_vertical_inset"
+       android:insetBottom="@dimen/qs_dialog_button_vertical_inset">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/white"/>
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <solid android:color="@android:color/transparent"/>
+                <stroke android:color="?androidprv:attr/colorAccentPrimary"
+                        android:width="1dp"
+                />
+                <padding android:left="@dimen/qs_dialog_button_horizontal_padding"
+                         android:top="@dimen/qs_dialog_button_vertical_padding"
+                         android:right="@dimen/qs_dialog_button_horizontal_padding"
+                         android:bottom="@dimen/qs_dialog_button_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/packages/SystemUI/res/drawable/rounded_corner_bottom_secondary.xml b/packages/SystemUI/res/drawable/rounded_corner_bottom_secondary.xml
new file mode 100644
index 0000000..5cc8d6a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/rounded_corner_bottom_secondary.xml
@@ -0,0 +1,16 @@
+<!--
+    Copyright (C) 2021 The Android Open Source Project
+
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<!-- Overlay this resource to change rounded_corners_bottom -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/rounded_secondary"/>
diff --git a/packages/SystemUI/res/drawable/rounded_corner_top_secondary.xml b/packages/SystemUI/res/drawable/rounded_corner_top_secondary.xml
new file mode 100644
index 0000000..724e3ef
--- /dev/null
+++ b/packages/SystemUI/res/drawable/rounded_corner_top_secondary.xml
@@ -0,0 +1,16 @@
+<!--
+    Copyright (C) 2021 The Android Open Source Project
+
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<!-- Overlay this resource to change rounded_corners_top -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/rounded_secondary"/>
diff --git a/packages/SystemUI/res/drawable/rounded_secondary.xml b/packages/SystemUI/res/drawable/rounded_secondary.xml
new file mode 100644
index 0000000..eb72fa1
--- /dev/null
+++ b/packages/SystemUI/res/drawable/rounded_secondary.xml
@@ -0,0 +1,24 @@
+<!--
+    Copyright (C) 2021 The Android Open Source Project
+
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="8dp"
+    android:height="8dp"
+    android:viewportWidth="8"
+    android:viewportHeight="8">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M8,0H0v8C0,3.6,3.6,0,8,0z" />
+
+</vector>
diff --git a/packages/SystemUI/res/drawable/stat_sys_managed_profile_status.xml b/packages/SystemUI/res/drawable/stat_sys_managed_profile_status.xml
index 3761a40..c415ecd 100644
--- a/packages/SystemUI/res/drawable/stat_sys_managed_profile_status.xml
+++ b/packages/SystemUI/res/drawable/stat_sys_managed_profile_status.xml
@@ -21,7 +21,4 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,6h-4V4c0-1.1-0.9-2-2-2h-4C8.9,2,8,2.9,8,4v2H4C2.9,6,2,6.9,2,8l0,11c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V8 C22,6.9,21.1,6,20,6z M10,4h4v2h-4V4z M20,19H4V8h16V19z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 12 12 C 12.8284271247 12 13.5 12.6715728753 13.5 13.5 C 13.5 14.3284271247 12.8284271247 15 12 15 C 11.1715728753 15 10.5 14.3284271247 10.5 13.5 C 10.5 12.6715728753 11.1715728753 12 12 12 Z" />
 </vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/global_actions_change_panel.xml b/packages/SystemUI/res/layout/global_actions_change_panel.xml
deleted file mode 100644
index bc9c203..0000000
--- a/packages/SystemUI/res/layout/global_actions_change_panel.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <TextView
-        android:id="@+id/global_actions_change_message"
-        android:layout_width="wrap_content"
-        android:visibility="gone"
-        android:layout_height="wrap_content"
-        android:text="@string/global_actions_change_description" />
-    <ImageView
-        android:id="@+id/global_actions_change_button"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/global_screenshot_static.xml b/packages/SystemUI/res/layout/global_screenshot_static.xml
index 6a9254c..21c5ab0 100644
--- a/packages/SystemUI/res/layout/global_screenshot_static.xml
+++ b/packages/SystemUI/res/layout/global_screenshot_static.xml
@@ -130,13 +130,4 @@
         app:layout_constraintStart_toStartOf="@id/global_screenshot_preview"
         app:layout_constraintTop_toTopOf="@id/global_screenshot_preview"
         android:elevation="@dimen/screenshot_preview_elevation"/>
-    <View
-        android:id="@+id/screenshot_transition_view"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:visibility="invisible"
-        app:layout_constraintStart_toStartOf="@id/global_screenshot_preview"
-        app:layout_constraintTop_toTopOf="@id/global_screenshot_preview"
-        app:layout_constraintEnd_toEndOf="@id/global_screenshot_preview"
-        app:layout_constraintBottom_toBottomOf="@id/global_screenshot_preview"/>
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index c88e95f..c1d7308 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -19,7 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/internet_connectivity_dialog"
-    android:layout_width="@dimen/internet_dialog_list_max_width"
+    android:layout_width="@dimen/large_dialog_width"
     android:layout_height="@dimen/internet_dialog_list_max_height"
     android:background="@drawable/internet_dialog_rounded_top_corner_background"
     android:orientation="vertical">
@@ -323,46 +323,6 @@
                 </FrameLayout>
             </LinearLayout>
 
-            <LinearLayout
-                android:id="@+id/wifi_scan_notify_layout"
-                style="@style/InternetDialog.Network"
-                android:orientation="vertical"
-                android:layout_height="wrap_content"
-                android:paddingBottom="4dp"
-                android:clickable="false"
-                android:focusable="false">
-
-                <LinearLayout
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:minWidth="56dp"
-                    android:gravity="start|top"
-                    android:orientation="horizontal"
-                    android:paddingEnd="12dp"
-                    android:paddingTop="16dp"
-                    android:paddingBottom="4dp">
-                    <ImageView
-                        android:src="@drawable/ic_info_outline"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:tint="?android:attr/textColorTertiary"/>
-                </LinearLayout>
-
-                <LinearLayout
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:orientation="vertical">
-                    <TextView
-                        android:id="@+id/wifi_scan_notify_text"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:paddingTop="16dp"
-                        android:paddingBottom="8dp"
-                        android:textColor="?android:attr/textColorSecondary"
-                        android:clickable="true"/>
-                </LinearLayout>
-            </LinearLayout>
-
             <FrameLayout
                 android:id="@+id/done_layout"
                 android:layout_width="67dp"
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index d996cee..cd6bc11 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -18,47 +18,50 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/media_output_dialog"
-    android:layout_width="match_parent"
+    android:layout_width="@dimen/large_dialog_width"
     android:layout_height="wrap_content"
     android:orientation="vertical">
 
     <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="94dp"
+        android:layout_height="96dp"
         android:gravity="start|center_vertical"
         android:paddingStart="16dp"
         android:orientation="horizontal">
         <ImageView
             android:id="@+id/header_icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:paddingEnd="@dimen/media_output_dialog_header_icon_padding"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
             android:importantForAccessibility="no"/>
 
         <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="16dp"
+            android:layout_height="match_parent"
+            android:paddingStart="16dp"
+            android:paddingTop="20dp"
+            android:paddingBottom="24dp"
+            android:paddingEnd="24dp"
             android:orientation="vertical">
             <TextView
                 android:id="@+id/header_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:ellipsize="end"
+                android:gravity="center_vertical"
                 android:maxLines="1"
                 android:textColor="?android:attr/textColorPrimary"
                 android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
                 android:textSize="20sp"/>
-
             <TextView
                 android:id="@+id/header_subtitle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:gravity="center_vertical"
                 android:ellipsize="end"
                 android:maxLines="1"
+                android:textColor="?android:attr/textColorTertiary"
                 android:fontFamily="roboto-regular"
-                android:textSize="14sp"/>
-
+                android:textSize="16sp"/>
         </LinearLayout>
     </LinearLayout>
 
@@ -81,21 +84,21 @@
             android:overScrollMode="never"/>
     </LinearLayout>
 
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:background="?android:attr/listDivider"/>
-
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginBottom="18dp"
+        android:layout_marginEnd="24dp"
         android:orientation="horizontal">
 
         <Button
             android:id="@+id/stop"
-            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            style="@style/MediaOutputRoundedOutlinedButton"
             android:layout_width="wrap_content"
-            android:layout_height="64dp"
+            android:layout_height="36dp"
+            android:minWidth="0dp"
             android:text="@string/keyboard_key_media_stop"
             android:visibility="gone"/>
 
@@ -106,10 +109,10 @@
 
         <Button
             android:id="@+id/done"
-            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            style="@style/MediaOutputRoundedOutlinedButton"
             android:layout_width="wrap_content"
-            android:layout_height="64dp"
-            android:layout_marginEnd="0dp"
+            android:layout_height="36dp"
+            android:minWidth="0dp"
             android:text="@string/inline_done_button"/>
     </LinearLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
index 16c03e1..a5a7efa 100644
--- a/packages/SystemUI/res/layout/media_output_list_item.xml
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -23,17 +23,20 @@
     android:orientation="vertical">
     <FrameLayout
         android:layout_width="match_parent"
-        android:layout_height="64dp">
+        android:layout_height="88dp"
+        android:paddingTop="24dp"
+        android:paddingBottom="16dp"
+        android:paddingStart="24dp"
+        android:paddingEnd="8dp">
 
         <FrameLayout
-            android:layout_width="36dp"
-            android:layout_height="36dp"
-            android:layout_gravity="center_vertical|start"
-            android:layout_marginStart="16dp">
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_gravity="center_vertical|start">
             <ImageView
                 android:id="@+id/title_icon"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
                 android:layout_gravity="center"/>
         </FrameLayout>
 
@@ -42,49 +45,69 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical|start"
-            android:layout_marginStart="68dp"
+            android:layout_marginStart="64dp"
             android:ellipsize="end"
             android:maxLines="1"
             android:textColor="?android:attr/textColorPrimary"
-            android:textSize="14sp"/>
+            android:textSize="16sp"/>
 
         <RelativeLayout
             android:id="@+id/two_line_layout"
             android:layout_width="wrap_content"
             android:layout_height="48dp"
-            android:layout_marginStart="52dp"
-            android:layout_marginEnd="69dp"
-            android:layout_marginTop="10dp">
+            android:layout_marginStart="48dp">
             <TextView
                 android:id="@+id/two_line_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginStart="16dp"
-                android:layout_marginEnd="15dp"
+                android:layout_marginEnd="48dp"
                 android:ellipsize="end"
                 android:maxLines="1"
                 android:textColor="?android:attr/textColorPrimary"
-                android:textSize="14sp"/>
+                android:textSize="16sp"/>
             <TextView
                 android:id="@+id/subtitle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginStart="16dp"
                 android:layout_marginEnd="15dp"
-                android:layout_marginBottom="7dp"
+                android:layout_marginTop="4dp"
                 android:layout_alignParentBottom="true"
                 android:ellipsize="end"
                 android:maxLines="1"
                 android:textColor="?android:attr/textColorSecondary"
-                android:textSize="12sp"
+                android:textSize="14sp"
                 android:fontFamily="roboto-regular"
                 android:visibility="gone"/>
             <SeekBar
                 android:id="@+id/volume_seekbar"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="8dp"
                 style="@*android:style/Widget.DeviceDefault.SeekBar"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_alignParentBottom="true"/>
+            <ImageView
+                android:id="@+id/add_icon"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_gravity="right"
+                android:layout_marginEnd="24dp"
+                android:layout_alignParentRight="true"
+                android:src="@drawable/ic_add"
+                android:tint="?android:attr/colorAccent"
+            />
+            <CheckBox
+                android:id="@+id/check_box"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_gravity="right"
+                android:layout_marginEnd="24dp"
+                android:layout_alignParentRight="true"
+                android:button="@drawable/ic_check_box"
+                android:visibility="gone"
+                />
         </RelativeLayout>
 
         <ProgressBar
@@ -92,47 +115,17 @@
             style="@*android:style/Widget.Material.ProgressBar.Horizontal"
             android:layout_width="258dp"
             android:layout_height="18dp"
-            android:layout_marginStart="68dp"
-            android:layout_marginTop="40dp"
+            android:layout_marginStart="64dp"
+            android:layout_marginTop="28dp"
             android:indeterminate="true"
             android:indeterminateOnly="true"
             android:visibility="gone"/>
-
-        <View
-            android:id="@+id/end_divider"
-            android:layout_width="1dp"
-            android:layout_height="36dp"
-            android:layout_marginEnd="68dp"
-            android:layout_gravity="right|center_vertical"
-            android:background="?android:attr/listDivider"
-            android:visibility="gone"/>
-
-        <ImageView
-            android:id="@+id/add_icon"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_gravity="right|center_vertical"
-            android:layout_marginEnd="24dp"
-            android:src="@drawable/ic_add"
-            android:tint="?android:attr/colorAccent"
-            android:visibility="gone"/>
-
-        <CheckBox
-            android:id="@+id/check_box"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_gravity="right|center_vertical"
-            android:layout_marginEnd="24dp"
-            android:button="@drawable/ic_check_box"
-            android:visibility="gone"/>
     </FrameLayout>
 
     <View
         android:id="@+id/bottom_divider"
         android:layout_width="match_parent"
         android:layout_height="1dp"
-        android:layout_marginTop="12dp"
-        android:layout_marginBottom="12dp"
         android:layout_gravity="bottom"
         android:background="?android:attr/listDivider"
         android:visibility="gone"/>
diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml
index 566cd25..b546a9c 100644
--- a/packages/SystemUI/res/layout/media_view.xml
+++ b/packages/SystemUI/res/layout/media_view.xml
@@ -134,6 +134,7 @@
         android:background="@drawable/qs_media_light_source"
         android:forceHasOverlappingRendering="false">
         <LinearLayout
+            android:id="@+id/media_seamless_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:minHeight="@dimen/qs_seamless_height"
diff --git a/packages/SystemUI/res/layout/qs_customize_panel_content.xml b/packages/SystemUI/res/layout/qs_customize_panel_content.xml
index 8ca1b8e..3be9993 100644
--- a/packages/SystemUI/res/layout/qs_customize_panel_content.xml
+++ b/packages/SystemUI/res/layout/qs_customize_panel_content.xml
@@ -19,7 +19,7 @@
     <View
         android:id="@+id/customizer_transparent_view"
         android:layout_width="match_parent"
-        android:layout_height="@*android:dimen/quick_qs_offset_height"
+        android:layout_height="@dimen/qs_header_system_icons_area_height"
         android:background="@android:color/transparent" />
 
     <com.android.keyguard.AlphaOptimizedLinearLayout
diff --git a/packages/SystemUI/res/layout/qs_detail.xml b/packages/SystemUI/res/layout/qs_detail.xml
index 59e1a75..78655c0 100644
--- a/packages/SystemUI/res/layout/qs_detail.xml
+++ b/packages/SystemUI/res/layout/qs_detail.xml
@@ -20,9 +20,9 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@drawable/qs_detail_background"
+    android:layout_marginTop="@dimen/qs_detail_margin_top"
     android:clickable="true"
     android:orientation="vertical"
-    android:layout_marginTop="@*android:dimen/quick_qs_offset_height"
     android:paddingBottom="8dp"
     android:visibility="invisible"
     android:elevation="4dp"
diff --git a/packages/SystemUI/res/layout/qs_detail_header.xml b/packages/SystemUI/res/layout/qs_detail_header.xml
index da80633..d1ab054 100644
--- a/packages/SystemUI/res/layout/qs_detail_header.xml
+++ b/packages/SystemUI/res/layout/qs_detail_header.xml
@@ -28,7 +28,7 @@
 
     <com.android.systemui.ResizingSpace
         android:layout_width="match_parent"
-        android:layout_height="@dimen/qs_detail_margin_top" />
+        android:layout_height="@dimen/qs_detail_header_margin_top" />
 
     <LinearLayout
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index 317dbc0..e70084b 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -68,93 +68,9 @@
 
         </LinearLayout>
 
-        <LinearLayout
-            android:id="@+id/qs_footer_actions_container"
-            android:layout_width="match_parent"
-            android:layout_height="48dp"
-            android:gravity="center_vertical">
+        <include layout="@layout/footer_actions"
+            android:id="@+id/qs_footer_actions"/>
 
-            <com.android.systemui.statusbar.AlphaOptimizedImageView
-                android:id="@android:id/edit"
-                android:layout_width="0dp"
-                android:layout_height="@dimen/qs_footer_action_button_size"
-                android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
-                android:layout_weight="1"
-                android:background="@drawable/qs_footer_action_chip_background"
-                android:clickable="true"
-                android:clipToPadding="false"
-                android:contentDescription="@string/accessibility_quick_settings_edit"
-                android:focusable="true"
-                android:padding="@dimen/qs_footer_icon_padding"
-                android:src="@*android:drawable/ic_mode_edit"
-                android:tint="?android:attr/textColorPrimary" />
-
-            <com.android.systemui.statusbar.phone.MultiUserSwitch
-                android:id="@+id/multi_user_switch"
-                android:layout_width="0dp"
-                android:layout_height="@dimen/qs_footer_action_button_size"
-                android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
-                android:layout_weight="1"
-                android:background="@drawable/qs_footer_action_chip_background"
-                android:focusable="true">
-
-                <ImageView
-                    android:id="@+id/multi_user_avatar"
-                    android:layout_width="@dimen/multi_user_avatar_expanded_size"
-                    android:layout_height="@dimen/multi_user_avatar_expanded_size"
-                    android:layout_gravity="center"
-                    android:scaleType="centerInside" />
-            </com.android.systemui.statusbar.phone.MultiUserSwitch>
-
-            <com.android.systemui.statusbar.AlphaOptimizedImageView
-                android:id="@+id/pm_lite"
-                android:layout_width="0dp"
-                android:layout_height="@dimen/qs_footer_action_button_size"
-                android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
-                android:layout_weight="1"
-                android:background="@drawable/qs_footer_action_chip_background"
-                android:clickable="true"
-                android:clipToPadding="false"
-                android:focusable="true"
-                android:padding="@dimen/qs_footer_icon_padding"
-                android:src="@*android:drawable/ic_lock_power_off"
-                android:contentDescription="@string/accessibility_quick_settings_power_menu"
-                android:tint="?android:attr/textColorPrimary" />
-
-            <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-                android:id="@+id/settings_button_container"
-                android:layout_width="0dp"
-                android:layout_height="@dimen/qs_footer_action_button_size"
-                android:background="@drawable/qs_footer_action_chip_background"
-                android:layout_weight="1"
-                android:clipChildren="false"
-                android:clipToPadding="false">
-
-                <com.android.systemui.statusbar.phone.SettingsButton
-                    android:id="@+id/settings_button"
-                    android:layout_width="match_parent"
-                    android:layout_height="@dimen/qs_footer_action_button_size"
-                    android:layout_gravity="center"
-                    android:contentDescription="@string/accessibility_quick_settings_settings"
-                    android:background="@drawable/qs_footer_action_chip_background_borderless"
-                    android:padding="@dimen/qs_footer_icon_padding"
-                    android:scaleType="centerInside"
-                    android:src="@drawable/ic_settings"
-                    android:tint="?android:attr/textColorPrimary" />
-
-                <com.android.systemui.statusbar.AlphaOptimizedImageView
-                    android:id="@+id/tuner_icon"
-                    android:layout_width="8dp"
-                    android:layout_height="8dp"
-                    android:layout_gravity="center_horizontal|bottom"
-                    android:layout_marginBottom="@dimen/qs_footer_icon_padding"
-                    android:src="@drawable/tuner"
-                    android:tint="?android:attr/textColorTertiary"
-                    android:visibility="invisible" />
-
-            </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
-
-        </LinearLayout>
     </LinearLayout>
 
 </com.android.systemui.qs.QSFooterView>
diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml
index 4c6418a..2ac03c2 100644
--- a/packages/SystemUI/res/layout/qs_panel.xml
+++ b/packages/SystemUI/res/layout/qs_panel.xml
@@ -13,13 +13,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.systemui.qs.QSContainerImpl
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.systemui.qs.QSContainerImpl xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/quick_settings_container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipToPadding="false"
-    android:clipChildren="false" >
+    android:clipChildren="false">
 
     <com.android.systemui.qs.NonInterceptingScrollView
         android:id="@+id/expanded_qs_scroll_view"
@@ -40,15 +40,33 @@
             android:accessibilityTraversalBefore="@android:id/edit"
             android:clipToPadding="false"
             android:clipChildren="false">
+
             <include layout="@layout/qs_footer_impl" />
         </com.android.systemui.qs.QSPanel>
     </com.android.systemui.qs.NonInterceptingScrollView>
 
     <include layout="@layout/quick_status_bar_expanded_header" />
 
-    <include android:id="@+id/qs_detail" layout="@layout/qs_detail" />
+    <include
+        android:id="@+id/qs_detail"
+        layout="@layout/qs_detail" />
 
-    <include android:id="@+id/qs_customize" layout="@layout/qs_customize_panel"
+    <include
+        android:id="@+id/qs_customize"
+        layout="@layout/qs_customize_panel"
         android:visibility="gone" />
 
+    <ImageView
+        android:id="@+id/qs_drag_handle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginTop="24dp"
+        android:elevation="4dp"
+        android:importantForAccessibility="no"
+        android:scaleType="center"
+        android:src="@drawable/ic_qs_drag_handle"
+        android:tint="@color/qs_detail_button_white"
+        tools:ignore="UseAppTint" />
+
 </com.android.systemui.qs.QSContainerImpl>
diff --git a/packages/SystemUI/res/layout/qs_user_dialog_content.xml b/packages/SystemUI/res/layout/qs_user_dialog_content.xml
new file mode 100644
index 0000000..543b7d7
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_user_dialog_content.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:sysui="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="24dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginEnd="16dp"
+    >
+        <TextView
+            android:id="@+id/title"
+            android:layout_height="wrap_content"
+            android:layout_width="0dp"
+            android:textAlignment="center"
+            android:text="@string/qs_user_switch_dialog_title"
+            android:textAppearance="@style/TextAppearance.QSDialog.Title"
+            android:layout_marginBottom="32dp"
+            sysui:layout_constraintTop_toTopOf="parent"
+            sysui:layout_constraintStart_toStartOf="parent"
+            sysui:layout_constraintEnd_toEndOf="parent"
+            sysui:layout_constraintBottom_toTopOf="@id/grid"
+            />
+
+        <com.android.systemui.qs.PseudoGridView
+            android:id="@+id/grid"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="28dp"
+            sysui:verticalSpacing="4dp"
+            sysui:horizontalSpacing="4dp"
+            sysui:fixedChildWidth="80dp"
+            sysui:layout_constraintTop_toBottomOf="@id/title"
+            sysui:layout_constraintStart_toStartOf="parent"
+            sysui:layout_constraintEnd_toEndOf="parent"
+            sysui:layout_constraintBottom_toTopOf="@id/barrier"
+        />
+
+        <androidx.constraintlayout.widget.Barrier
+            android:id="@+id/barrier"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            sysui:barrierDirection="top"
+            sysui:constraint_referenced_ids="settings,done"
+            />
+
+        <Button
+            android:id="@+id/settings"
+            android:layout_width="wrap_content"
+            android:layout_height="48dp"
+            android:text="@string/quick_settings_more_user_settings"
+            sysui:layout_constraintTop_toBottomOf="@id/barrier"
+            sysui:layout_constraintBottom_toBottomOf="parent"
+            sysui:layout_constraintStart_toStartOf="parent"
+            sysui:layout_constraintEnd_toStartOf="@id/done"
+            sysui:layout_constraintHorizontal_chainStyle="spread_inside"
+            style="@style/Widget.QSDialog.Button.BorderButton"
+            />
+
+        <Button
+            android:id="@+id/done"
+            android:layout_width="wrap_content"
+            android:layout_height="48dp"
+            android:text="@string/quick_settings_done"
+            sysui:layout_constraintTop_toBottomOf="@id/barrier"
+            sysui:layout_constraintBottom_toBottomOf="parent"
+            sysui:layout_constraintStart_toEndOf="@id/settings"
+            sysui:layout_constraintEnd_toEndOf="parent"
+            style="@style/Widget.QSDialog.Button"
+            />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/quick_qs_status_icons.xml b/packages/SystemUI/res/layout/quick_qs_status_icons.xml
index 42a7c89..542a1c9 100644
--- a/packages/SystemUI/res/layout/quick_qs_status_icons.xml
+++ b/packages/SystemUI/res/layout/quick_qs_status_icons.xml
@@ -21,7 +21,7 @@
     android:layout_height="@*android:dimen/quick_qs_offset_height"
     android:clipChildren="false"
     android:clipToPadding="false"
-    android:minHeight="48dp"
+    android:minHeight="@dimen/qs_header_row_min_height"
     android:clickable="false"
     android:focusable="true"
     android:theme="@style/Theme.SystemUI.QuickSettings.Header">
@@ -39,7 +39,7 @@
             android:id="@+id/clock"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
-            android:minHeight="48dp"
+            android:minHeight="@dimen/qs_header_row_min_height"
             android:gravity="center_vertical|start"
             android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
             android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
@@ -64,7 +64,7 @@
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_weight="1"
-        android:minHeight="48dp"
+        android:minHeight="@dimen/qs_header_row_min_height"
         android:minWidth="48dp"
         android:layout_marginStart="8dp"
         android:layout_gravity="end|center_vertical"
@@ -97,7 +97,7 @@
             android:layout_height="match_parent"
             android:paddingEnd="@dimen/signal_cluster_battery_padding" />
 
-        <com.android.systemui.BatteryMeterView
+        <com.android.systemui.battery.BatteryMeterView
             android:id="@+id/batteryRemainingIcon"
             android:layout_height="match_parent"
             android:layout_width="0dp"
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index f3b8b0b..6b14c96 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -32,7 +32,7 @@
     android:paddingStart="0dp"
     android:elevation="4dp" >
 
-    <!-- Date and privacy. Only visible in QS -->
+    <!-- Date and privacy. Only visible in QS when not in split shade -->
     <include layout="@layout/quick_status_bar_header_date_privacy"/>
 
     <RelativeLayout
@@ -42,21 +42,32 @@
         android:layout_gravity="top"
         android:clipChildren="false"
         android:clipToPadding="false">
-    <!-- Time, icons and Carrier (only in QS) -->
-    <include layout="@layout/quick_qs_status_icons"/>
+        <!-- Time, icons and Carrier (only in QS when not in split shade) -->
+        <include layout="@layout/quick_qs_status_icons"/>
 
-    <com.android.systemui.qs.QuickQSPanel
-        android:id="@+id/quick_qs_panel"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_below="@id/quick_qs_status_icons"
-        android:layout_marginTop="@dimen/qqs_layout_margin_top"
-        android:accessibilityTraversalAfter="@id/quick_qs_status_icons"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:focusable="true"
-        android:paddingBottom="24dp"
-        android:importantForAccessibility="yes" />
+        <com.android.systemui.qs.QuickQSPanel
+            android:id="@+id/quick_qs_panel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/quick_qs_status_icons"
+            android:layout_marginTop="@dimen/qqs_layout_margin_top"
+            android:accessibilityTraversalAfter="@id/quick_qs_status_icons"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:focusable="true"
+            android:paddingBottom="24dp"
+            android:importantForAccessibility="yes">
+
+            <include
+                layout="@layout/footer_actions"
+                android:id="@+id/qqs_footer_actions"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="16dp"
+                android:layout_marginStart="@dimen/qs_footer_margin"
+                android:layout_marginEnd="@dimen/qs_footer_margin"
+                />
+        </com.android.systemui.qs.QuickQSPanel>
     </RelativeLayout>
 
 </com.android.systemui.qs.QuickStatusBarHeader>
diff --git a/packages/SystemUI/res/layout/rotate_suggestion.xml b/packages/SystemUI/res/layout/rotate_suggestion.xml
index 194d2e0..1c3eedb 100644
--- a/packages/SystemUI/res/layout/rotate_suggestion.xml
+++ b/packages/SystemUI/res/layout/rotate_suggestion.xml
@@ -14,16 +14,19 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-
-<com.android.systemui.navigationbar.buttons.KeyButtonView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/rotate_suggestion"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_weight="0"
-    android:scaleType="center"
-    android:visibility="invisible"
-    android:contentDescription="@string/accessibility_rotate_button"
-    android:paddingStart="@dimen/navigation_key_padding"
-    android:paddingEnd="@dimen/navigation_key_padding"
-/>
\ No newline at end of file
+    >
+
+    <com.android.systemui.navigationbar.buttons.KeyButtonView
+        android:id="@+id/rotate_suggestion"
+        android:layout_width="@dimen/floating_rotation_button_diameter"
+        android:layout_height="@dimen/floating_rotation_button_diameter"
+        android:contentDescription="@string/accessibility_rotate_button"
+        android:paddingStart="@dimen/navigation_key_padding"
+        android:paddingEnd="@dimen/navigation_key_padding"
+        android:layout_gravity="bottom|left"
+        android:scaleType="center"
+        android:visibility="invisible" />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/rounded_corners_bottom.xml b/packages/SystemUI/res/layout/rounded_corners_bottom.xml
index 720e47b..f91ab6f 100644
--- a/packages/SystemUI/res/layout/rounded_corners_bottom.xml
+++ b/packages/SystemUI/res/layout/rounded_corners_bottom.xml
@@ -31,8 +31,7 @@
         android:id="@+id/privacy_dot_left_container"
         android:layout_height="@dimen/status_bar_height"
         android:layout_width="wrap_content"
-        android:layout_marginTop="@dimen/status_bar_padding_top"
-        android:layout_marginLeft="0dp"
+        android:paddingTop="@dimen/status_bar_padding_top"
         android:layout_gravity="left|bottom"
         android:visibility="invisible" >
         <ImageView
@@ -51,12 +50,12 @@
         android:tint="#ff000000"
         android:layout_gravity="right|bottom"
         android:src="@drawable/rounded_corner_bottom"/>
+
     <FrameLayout
         android:id="@+id/privacy_dot_right_container"
         android:layout_height="@dimen/status_bar_height"
         android:layout_width="wrap_content"
-        android:layout_marginTop="@dimen/status_bar_padding_top"
-        android:layout_marginRight="0dp"
+        android:paddingTop="@dimen/status_bar_padding_top"
         android:layout_gravity="right|bottom"
         android:visibility="invisible" >
         <ImageView
diff --git a/packages/SystemUI/res/layout/rounded_corners_top.xml b/packages/SystemUI/res/layout/rounded_corners_top.xml
index 6abe406..819a9a4 100644
--- a/packages/SystemUI/res/layout/rounded_corners_top.xml
+++ b/packages/SystemUI/res/layout/rounded_corners_top.xml
@@ -29,10 +29,9 @@
 
     <FrameLayout
         android:id="@+id/privacy_dot_left_container"
-        android:layout_height="@*android:dimen/status_bar_height_portrait"
+        android:layout_height="@dimen/status_bar_height"
         android:layout_width="wrap_content"
-        android:layout_marginTop="@dimen/status_bar_padding_top"
-        android:layout_marginLeft="0dp"
+        android:paddingTop="@dimen/status_bar_padding_top"
         android:layout_gravity="left|top"
         android:visibility="invisible" >
         <ImageView
@@ -54,10 +53,9 @@
 
     <FrameLayout
         android:id="@+id/privacy_dot_right_container"
-        android:layout_height="@*android:dimen/status_bar_height_portrait"
+        android:layout_height="@dimen/status_bar_height"
         android:layout_width="wrap_content"
-        android:layout_marginTop="@dimen/status_bar_padding_top"
-        android:layout_marginRight="0dp"
+        android:paddingTop="@dimen/status_bar_padding_top"
         android:layout_gravity="right|top"
         android:visibility="invisible" >
         <ImageView
@@ -67,8 +65,6 @@
             android:layout_gravity="center_vertical|left"
             android:src="@drawable/system_animation_ongoing_dot"
             android:visibility="visible" />
-
     </FrameLayout>
 
-
 </com.android.systemui.RegionInterceptingFrameLayout>
diff --git a/packages/SystemUI/res/layout/sidefps_view.xml b/packages/SystemUI/res/layout/sidefps_view.xml
index 3977635..921f788 100644
--- a/packages/SystemUI/res/layout/sidefps_view.xml
+++ b/packages/SystemUI/res/layout/sidefps_view.xml
@@ -14,11 +14,13 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.systemui.biometrics.SidefpsView
+<com.airbnb.lottie.LottieAnimationView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/sidefps_view"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:contentDescription="@string/accessibility_fingerprint_label">
-</com.android.systemui.biometrics.SidefpsView>
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/sidefps_animation"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    app:lottie_autoPlay="true"
+    app:lottie_loop="true"
+    app:lottie_rawRes="@raw/sfps_pulse"
+    android:contentDescription="@string/accessibility_fingerprint_label"/>
diff --git a/packages/SystemUI/res/layout/split_shade_header.xml b/packages/SystemUI/res/layout/split_shade_header.xml
new file mode 100644
index 0000000..f2c5b7b
--- /dev/null
+++ b/packages/SystemUI/res/layout/split_shade_header.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/split_shade_status_bar"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/split_shade_header_height"
+    android:minHeight="@dimen/split_shade_header_min_height"
+    android:clickable="false"
+    android:focusable="true"
+    android:paddingLeft="@dimen/qs_panel_padding"
+    android:paddingRight="@dimen/qs_panel_padding"
+    android:visibility="gone"
+    android:theme="@style/Theme.SystemUI.QuickSettings.Header">
+
+    <com.android.systemui.statusbar.policy.Clock
+        android:id="@+id/clock"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:minWidth="48dp"
+        android:minHeight="@dimen/split_shade_header_min_height"
+        android:gravity="start|center_vertical"
+        android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
+        android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
+        android:singleLine="true"
+        android:textAppearance="@style/TextAppearance.QS.Status" />
+
+    <com.android.systemui.statusbar.policy.DateView
+        android:id="@+id/date"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start|center_vertical"
+        android:gravity="center_vertical"
+        android:singleLine="true"
+        android:textAppearance="@style/TextAppearance.QS.Status"
+        systemui:datePattern="@string/abbrev_wday_month_day_no_year_alarm" />
+
+    <FrameLayout
+        android:id="@+id/rightLayout"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:gravity="end">
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_gravity="end|center_vertical">
+
+            <include
+                android:id="@+id/carrier_group"
+                layout="@layout/qs_carrier_group"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_gravity="end|center_vertical"
+                android:layout_marginStart="8dp"
+                android:focusable="false"
+                android:minHeight="@dimen/split_shade_header_min_height"
+                android:minWidth="48dp" />
+
+            <com.android.systemui.statusbar.phone.StatusIconContainer
+                android:id="@+id/statusIcons"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:paddingEnd="@dimen/signal_cluster_battery_padding" />
+
+            <com.android.systemui.battery.BatteryMeterView
+                android:id="@+id/batteryRemainingIcon"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                systemui:textAppearance="@style/TextAppearance.QS.Status" />
+        </LinearLayout>
+    </FrameLayout>
+
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 8b78732..cc1af87 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -25,12 +25,6 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@android:color/transparent">
-    <FrameLayout
-        android:id="@+id/big_clock_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="gone" />
-
     <ViewStub
         android:id="@+id/keyguard_qs_user_switch_stub"
         android:layout="@layout/keyguard_qs_user_switch"
@@ -71,18 +65,6 @@
             android:layout_gravity="center"
             android:scaleType="centerCrop"/>
 
-        <!-- Fingerprint -->
-        <!-- AOD dashed fingerprint icon with moving dashes -->
-        <com.airbnb.lottie.LottieAnimationView
-            android:id="@+id/lock_udfps_aod_fp"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:padding="@dimen/lock_icon_padding"
-            android:layout_gravity="center"
-            android:scaleType="centerCrop"
-            systemui:lottie_autoPlay="false"
-            systemui:lottie_loop="true"
-            systemui:lottie_rawRes="@raw/udfps_aod_fp"/>
     </com.android.keyguard.LockIconView>
 
     <com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer
@@ -93,11 +75,13 @@
         android:clipToPadding="false"
         android:clipChildren="false">
 
+        <include layout="@layout/split_shade_header"/>
+
         <include
             layout="@layout/keyguard_status_view"
             android:visibility="gone"/>
 
-        <include layout="@layout/dock_info_overlay" />
+        <include layout="@layout/dock_info_overlay"/>
 
         <FrameLayout
             android:id="@+id/qs_frame"
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index 71e8fc9..e02a1767 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -80,9 +80,10 @@
     />
 
     <!-- Keyguard messages -->
-    <FrameLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
         android:layout_marginTop="@dimen/status_bar_height"
         android:layout_gravity="top|center_horizontal"
         android:gravity="center_horizontal">
@@ -96,7 +97,11 @@
             android:singleLine="true"
             android:ellipsize="marquee"
             android:focusable="true" />
-    </FrameLayout>
+        <FrameLayout android:id="@+id/keyguard_bouncer_container"
+                     android:layout_height="0dp"
+                     android:layout_width="match_parent"
+                     android:layout_weight="1" />
+    </LinearLayout>
 
     <com.android.systemui.biometrics.AuthRippleView
         android:id="@+id/auth_ripple"
diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml
index 818d1d7..6d5c7d4 100644
--- a/packages/SystemUI/res/layout/system_icons.xml
+++ b/packages/SystemUI/res/layout/system_icons.xml
@@ -29,7 +29,7 @@
         android:gravity="center_vertical"
         android:orientation="horizontal"/>
 
-    <com.android.systemui.BatteryMeterView android:id="@+id/battery"
+    <com.android.systemui.battery.BatteryMeterView android:id="@+id/battery"
         android:layout_height="match_parent"
         android:layout_width="wrap_content"
         android:clipToPadding="false"
diff --git a/packages/SystemUI/res/layout/text_toast.xml b/packages/SystemUI/res/layout/text_toast.xml
index 49b182a..a3fe8ef 100644
--- a/packages/SystemUI/res/layout/text_toast.xml
+++ b/packages/SystemUI/res/layout/text_toast.xml
@@ -45,6 +45,5 @@
         android:maxLines="2"
         android:paddingTop="12dp"
         android:paddingBottom="12dp"
-        android:lineHeight="20sp"
         android:textAppearance="@*android:style/TextAppearance.Toast"/>
 </LinearLayout>
diff --git a/packages/SystemUI/res/layout/udfps_aod_lock_icon.xml b/packages/SystemUI/res/layout/udfps_aod_lock_icon.xml
new file mode 100644
index 0000000..f5bfa49
--- /dev/null
+++ b/packages/SystemUI/res/layout/udfps_aod_lock_icon.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.airbnb.lottie.LottieAnimationView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/lock_udfps_aod_fp"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="@dimen/lock_icon_padding"
+    android:layout_gravity="center"
+    android:scaleType="centerCrop"
+    systemui:lottie_autoPlay="false"
+    systemui:lottie_loop="true"
+    systemui:lottie_rawRes="@raw/udfps_aod_fp"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/raw/sfps_pulse.json b/packages/SystemUI/res/raw/sfps_pulse.json
new file mode 100644
index 0000000..c4903a2
--- /dev/null
+++ b/packages/SystemUI/res/raw/sfps_pulse.json
@@ -0,0 +1 @@
+{"v":"5.7.6","fr":60,"ip":0,"op":300,"w":42,"h":80,"nm":"Fingerprint Pulse Motion","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[28,40,0],"to":[0.751,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":30,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":60,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":90,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":120,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":150,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":180,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":210,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":240,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":270,"s":[32.503,40,0],"to":[0,0,0],"ti":[0.751,0,0]},{"t":300,"s":[28,40,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.566,0],[-3.929,-5.503],[-2.751,-6.68],[3.929,0],[-2.751,6.68],[-3.929,5.503]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30.218,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".blue600","cl":"blue600","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[41.878,40,0],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[55,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[28.253,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[55,55]},{"t":30,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":30,"st":-30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[20]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[55,55]},{"t":60,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[20]},{"t":90,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[55,55]},{"t":90,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":90,"st":30,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[20]},{"t":120,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[55,55]},{"t":120,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[20]},{"t":150,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[55,55]},{"t":150,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":150,"st":90,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":160,"s":[20]},{"t":180,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":120,"s":[55,55]},{"t":180,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":120,"op":180,"st":120,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":190,"s":[20]},{"t":210,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":150,"s":[55,55]},{"t":210,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":150,"op":210,"st":150,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":220,"s":[20]},{"t":240,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":180,"s":[55,55]},{"t":240,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":240,"st":180,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[20]},{"t":270,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":210,"s":[55,55]},{"t":270,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":210,"op":270,"st":210,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[20]},{"t":300,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":240,"s":[55,55]},{"t":300,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":300,"st":240,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":310,"s":[20]},{"t":330,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":270,"s":[55,55]},{"t":330,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":330,"st":270,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/packages/SystemUI/res/raw/sfps_pulse_landscape.json b/packages/SystemUI/res/raw/sfps_pulse_landscape.json
new file mode 100644
index 0000000..8c91762
--- /dev/null
+++ b/packages/SystemUI/res/raw/sfps_pulse_landscape.json
@@ -0,0 +1 @@
+{"v":"5.7.6","fr":60,"ip":0,"op":300,"w":80,"h":42,"nm":"Fingerprint Pulse Motion Portrait","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[40,14,0],"to":[0,-0.751,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":30,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":60,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":90,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":120,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":150,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":180,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":210,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":240,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":270,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,-0.751,0]},{"t":300,"s":[40,14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.566,0],[-3.929,-5.503],[-2.751,-6.68],[3.929,0],[-2.751,6.68],[-3.929,5.503]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30.218,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".blue600","cl":"blue600","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,0.122,0],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[55,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[28.253,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[55,55]},{"t":30,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":30,"st":-30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[20]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[55,55]},{"t":60,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[20]},{"t":90,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[55,55]},{"t":90,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":90,"st":30,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[20]},{"t":120,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[55,55]},{"t":120,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[20]},{"t":150,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[55,55]},{"t":150,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":150,"st":90,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":160,"s":[20]},{"t":180,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":120,"s":[55,55]},{"t":180,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":120,"op":180,"st":120,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":190,"s":[20]},{"t":210,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":150,"s":[55,55]},{"t":210,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":150,"op":210,"st":150,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":220,"s":[20]},{"t":240,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":180,"s":[55,55]},{"t":240,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":240,"st":180,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[20]},{"t":270,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":210,"s":[55,55]},{"t":270,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":210,"op":270,"st":210,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[20]},{"t":300,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":240,"s":[55,55]},{"t":300,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":300,"st":240,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":310,"s":[20]},{"t":330,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":270,"s":[55,55]},{"t":330,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":330,"st":270,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-land/config.xml b/packages/SystemUI/res/values-land/config.xml
index ea456d8..ac4dfd2 100644
--- a/packages/SystemUI/res/values-land/config.xml
+++ b/packages/SystemUI/res/values-land/config.xml
@@ -28,9 +28,6 @@
     <!-- The number of columns that the top level tiles span in the QuickSettings -->
     <integer name="quick_settings_user_time_settings_tile_span">2</integer>
 
-    <!-- We have only space for one notification on phone landscape layouts. -->
-    <integer name="keyguard_max_notification_count">1</integer>
-
     <!-- orientation of the dead zone when touches have recently occurred elsewhere on screen -->
     <integer name="navigation_bar_deadzone_orientation">1</integer>
 
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 34bf28a..fc5edf3 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -32,16 +32,16 @@
     -->
     <dimen name="qs_customize_header_min_height">48dp</dimen>
 
+    <!--  In landscape the security footer is actually part of the header,
+    and needs to be as short as the header  -->
     <dimen name="qs_security_footer_single_line_height">@*android:dimen/quick_qs_offset_height</dimen>
     <dimen name="qs_footer_padding">14dp</dimen>
-    <dimen name="qs_footers_margin_bottom">0dp</dimen>
     <dimen name="qs_security_footer_background_inset">12dp</dimen>
-    <dimen name="qs_security_footer_corner_radius">28dp</dimen>
 
     <dimen name="battery_detail_graph_space_top">9dp</dimen>
     <dimen name="battery_detail_graph_space_bottom">9dp</dimen>
 
-    <dimen name="qs_detail_margin_top">14dp</dimen>
+    <dimen name="qs_detail_header_margin_top">14dp</dimen>
 
     <dimen name="volume_tool_tip_top_margin">12dp</dimen>
     <dimen name="volume_row_slider_height">128dp</dimen>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index ffcc3a8..07e28b6 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -16,7 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Dialog" />
+    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Dialog">
+        <item name="android:buttonCornerRadius">28dp</item>
+    </style>
 
     <style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert" />
 
diff --git a/packages/SystemUI/res/values-sw600dp-land/config.xml b/packages/SystemUI/res/values-sw600dp-land/config.xml
index e2b2e25..ab159e1 100644
--- a/packages/SystemUI/res/values-sw600dp-land/config.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/config.xml
@@ -18,6 +18,18 @@
     <!-- Max number of columns for quick controls area -->
     <integer name="controls_max_columns">2</integer>
 
+    <!-- The maximum number of rows in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_rows">4</integer>
+
+    <!-- The maximum number of tiles in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_tiles">8</integer>
+
     <!-- Whether to use the split 2-column notification shade -->
     <bool name="config_use_split_notification_shade">true</bool>
+
+    <!-- The number of columns in the QuickSettings -->
+    <integer name="quick_settings_num_columns">2</integer>
+
+    <!-- Notifications are sized to match the width of two (of 4) qs tiles in landscape. -->
+    <bool name="config_skinnyNotifsInLandscape">false</bool>
 </resources>
diff --git a/packages/SystemUI/res/values-h560dp-xhdpi/config.xml b/packages/SystemUI/res/values-sw600dp-port/config.xml
similarity index 61%
copy from packages/SystemUI/res/values-h560dp-xhdpi/config.xml
copy to packages/SystemUI/res/values-sw600dp-port/config.xml
index cf2017f..02fd25b 100644
--- a/packages/SystemUI/res/values-h560dp-xhdpi/config.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/config.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <!--
-  ~ Copyright (C) 2014 The Android Open Source Project
+  ~ Copyright (C) 2021 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -16,8 +15,13 @@
   ~ limitations under the License
   -->
 <resources>
-    <!-- The maximum count of notifications on Keyguard. The rest will be collapsed in an overflow
-     card. -->
-    <integer name="keyguard_max_notification_count">3</integer>
-</resources>
 
+    <!-- The maximum number of tiles in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_tiles">6</integer>
+
+    <!-- The maximum number of rows in the QuickSettings -->
+    <integer name="quick_settings_max_rows">3</integer>
+
+    <!-- The number of columns in the QuickSettings -->
+    <integer name="quick_settings_num_columns">3</integer>
+</resources>
diff --git a/packages/SystemUI/res/values-sw600dp-port/dimens.xml b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
index 40838f3..da2403a 100644
--- a/packages/SystemUI/res/values-sw600dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
@@ -16,6 +16,6 @@
   -->
 <resources>
     <!-- Size of the panel of large phones on portrait. This shouldn't fill, but have some padding on the side -->
-    <dimen name="notification_panel_width">416dp</dimen>
+    <dimen name="notification_panel_width">504dp</dimen>
 
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index 2f5e8ea..45b5afa 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -38,4 +38,10 @@
     <!-- Max number of columns for quick controls area -->
     <integer name="controls_max_columns">4</integer>
 
+    <!-- How many lines to show in the security footer -->
+    <integer name="qs_security_footer_maxLines">1</integer>
+
+    <!-- Determines whether to allow the nav bar handle to be forced to be opaque. -->
+    <bool name="allow_force_nav_bar_handle_opaque">false</bool>
+
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 0a34dfd..85f8f09 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -96,6 +96,14 @@
 
     <dimen name="global_actions_grid_item_layout_height">80dp</dimen>
 
-    <!-- Internet panel related dimensions -->
-    <dimen name="internet_dialog_list_max_width">624dp</dimen>
+    <!--  For large screens the security footer appears below the footer,
+    same as phones in portrait  -->
+    <dimen name="qs_security_footer_single_line_height">48dp</dimen>
+    <dimen name="qs_security_footer_background_inset">0dp</dimen>
+
+    <!-- When split shade is used, this panel should be aligned to the top -->
+    <dimen name="qs_detail_margin_top">0dp</dimen>
+
+    <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
+    <dimen name="large_dialog_width">624dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-sw720dp-land/config.xml b/packages/SystemUI/res/values-sw720dp-land/config.xml
new file mode 100644
index 0000000..e4573c6
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-land/config.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+    <!-- Max number of columns for quick controls area -->
+    <integer name="controls_max_columns">2</integer>
+
+    <!-- The maximum number of rows in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_rows">4</integer>
+
+    <!-- The maximum number of tiles in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_tiles">8</integer>
+
+    <!-- Whether to use the split 2-column notification shade -->
+    <bool name="config_use_split_notification_shade">true</bool>
+
+    <!-- The number of columns in the QuickSettings -->
+    <integer name="quick_settings_num_columns">2</integer>
+
+    <!-- Notifications are sized to match the width of two (of 4) qs tiles in landscape. -->
+    <bool name="config_skinnyNotifsInLandscape">false</bool>
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/config.xml b/packages/SystemUI/res/values-sw720dp-port/config.xml
new file mode 100644
index 0000000..1225086
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-port/config.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds. -->
+<resources>
+    <!-- The maximum number of tiles in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_tiles">6</integer>
+
+    <!-- The number of columns in the QuickSettings -->
+    <integer name="quick_settings_num_columns">3</integer>
+
+    <!-- The maximum number of rows in the QuickSettings -->
+    <integer name="quick_settings_max_rows">3</integer>
+
+</resources>
+
diff --git a/packages/SystemUI/res/values-sw720dp/config.xml b/packages/SystemUI/res/values-sw720dp/config.xml
index 64e2760..436f8d0 100644
--- a/packages/SystemUI/res/values-sw720dp/config.xml
+++ b/packages/SystemUI/res/values-sw720dp/config.xml
@@ -22,8 +22,5 @@
 <resources>
     <integer name="status_bar_config_maxNotificationIcons">5</integer>
 
-    <!-- The maximum count of notifications on Keyguard. The rest will be collapsed in an overflow
-         card. -->
-    <integer name="keyguard_max_notification_count">5</integer>
 </resources>
 
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 3121ce3..db69924 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -77,6 +77,7 @@
         <attr name="numColumns" format="integer" />
         <attr name="verticalSpacing" format="dimension" />
         <attr name="horizontalSpacing" format="dimension" />
+        <attr name="fixedChildWidth" format="dimension" />
     </declare-styleable>
 
     <!-- Theme for icons in the status/nav bar (light/dark). background/fillColor is used for dual
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index f539b0c..03c6fdd 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -69,6 +69,9 @@
     <!-- Shadows under the clock, date and other keyguard text fields -->
     <color name="keyguard_shadow_color">#B2000000</color>
 
+    <!-- Color for the images in keyguard number pad buttons   -->
+    <color name="keyguard_keypad_image_color">?android:attr/textColorPrimaryInverse</color>
+
     <!-- Color for rounded background for activated user in keyguard user switcher -->
     <color name="kg_user_switcher_activated_background_color">#26000000</color>
     <!-- Icon color for user avatars in keyguard user switcher -->
@@ -149,7 +152,6 @@
     <!-- Chosen so fill over background matches single tone -->
     <color name="dark_mode_qs_icon_color_dual_tone_fill">#99000000</color>
 
-    <color name="docked_divider_background">#ff000000</color>
     <color name="docked_divider_handle">#ffffff</color>
     <drawable name="forced_resizable_background">#59000000</drawable>
     <color name="minimize_dock_shadow_start">#60000000</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index d274c91..d0de876 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -86,7 +86,10 @@
     <bool name="config_navigation_bar_enable_auto_dim_no_visible_wallpaper">true</bool>
 
     <!-- The maximum number of tiles in the QuickQSPanel -->
-    <integer name="quick_qs_panel_max_columns">4</integer>
+    <integer name="quick_qs_panel_max_tiles">4</integer>
+
+    <!-- The maximum number of rows in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_rows">2</integer>
 
     <!-- The number of columns in the QuickSettings -->
     <integer name="quick_settings_num_columns">2</integer>
@@ -159,7 +162,7 @@
 
     <!-- The maximum count of notifications on Keyguard. The rest will be collapsed in an overflow
      card. -->
-    <integer name="keyguard_max_notification_count">3</integer>
+    <integer name="keyguard_max_notification_count">-1</integer>
 
     <!-- Defines the implementation of the velocity tracker to be used for the panel expansion. Can
          be 'platform' or 'noisy' (i.e. for noisy touch screens). -->
@@ -188,6 +191,15 @@
      low powered state yet. -->
     <bool name="doze_single_tap_uses_prox">true</bool>
 
+    <!-- Doze: whether the single tap sensor uses the proximity sensor in the given posture.
+        See doze_single_tap_uses_prox for usage. -->
+    <integer-array name="doze_single_tap_uses_prox_posture_mapping">
+        <item>1</item> <!-- UNKNOWN -->
+        <item>1</item> <!-- CLOSED -->
+        <item>1</item> <!-- HALF_OPENED -->
+        <item>1</item> <!-- OPENED -->
+    </integer-array>
+
     <!-- Doze: whether the long press sensor uses the proximity sensor.
      If both this parameter and doze_selectively_register_prox are true, registration for the
      sensor will be delayed when the device first enters dozing but the device has not entered its
@@ -208,9 +220,27 @@
          always-on display) -->
     <string name="doze_brightness_sensor_type" translatable="false"></string>
 
+    <!-- Name of a sensor per posture state that provides a low-power estimate of the desired
+         display brightness, suitable to listen to while the device is asleep (e.g. during
+         always-on display) -->
+    <string-array name="doze_brightness_sensor_name_posture_mapping" translatable="false">
+        <!-- UNKNOWN -->
+        <!-- CLOSED -->
+        <!-- HALF_OPENED -->
+        <!-- OPENED -->
+    </string-array>
+
     <!-- Override value to use for proximity sensor.  -->
     <string name="proximity_sensor_type" translatable="false"></string>
 
+    <!-- Sensor type per posture state to use for proximity sensor -->
+    <string-array name="proximity_sensor_posture_mapping" translatable="false">
+        <!-- UNKNOWN -->
+        <!-- CLOSED -->
+        <!-- HALF_OPENED -->
+        <!-- OPENED -->
+    </string-array>
+
     <!-- If using proximity_sensor_type, specifies a threshold value to distinguish near and
          far break points. A sensor value less than this is considered "near". -->
     <item name="proximity_sensor_threshold" translatable="false" format="float" type="dimen"></item>
@@ -224,6 +254,15 @@
     <!-- Override value to use for proximity sensor as confirmation for proximity_sensor_type. -->
     <string name="proximity_sensor_secondary_type" translatable="false"></string>
 
+    <!-- Sensor type per posture state to use for proximity sensor as a confirmation for
+        proximity_sensor_type. -->
+    <string-array name="proximity_sensor_secondary_posture_mapping" translatable="false">
+        <!-- UNKNOWN -->
+        <!-- CLOSED -->
+        <!-- HALF_OPENED -->
+        <!-- OPENED -->
+    </string-array>
+
     <!-- If using proximity_sensor_secondary_type, specifies a threshold value to distinguish
          near and far break points. A sensor value less than this is considered "near". -->
     <item name="proximity_sensor_secondary_threshold" translatable="false" format="float"
@@ -608,8 +647,6 @@
 
     <!-- Whether wallet view is shown in landscape / seascape orientations -->
     <bool name="global_actions_show_landscape_wallet_view">false</bool>
-    <!-- Whether global actions should show an informational message about changes in S -->
-    <bool name="global_actions_show_change_info">false</bool>
 
     <!-- Package name of the preferred system app to perform eSOS action -->
     <string name="config_preferredEmergencySosPackage" translatable="false"></string>
@@ -670,4 +707,43 @@
          1 - Override the setting to always bypass keyguard
          2 - Override the setting to never bypass keyguard -->
     <integer name="config_face_unlock_bypass_override">0</integer>
+
+    <!-- Flag to activate notification to contents feature -->
+    <bool name="config_notificationToContents">false</bool>
+
+    <!-- Respect drawable/rounded_secondary.xml intrinsic size for multiple radius corner path
+         customization for secondary display-->
+    <bool name="config_roundedCornerMultipleRadiusSecondary">false</bool>
+
+    <!-- Whether the rounded corners are multiple radius for each display in a multi-display device.
+         {@see com.android.internal.R.array#config_displayUniqueIdArray} -->
+    <array name="config_roundedCornerMultipleRadiusArray">
+        <item>@bool/config_roundedCornerMultipleRadius</item>
+        <item>@bool/config_roundedCornerMultipleRadiusSecondary</item>
+    </array>
+
+    <!-- The rounded corner drawable for each display in a multi-display device.
+         {@see com.android.internal.R.array#config_displayUniqueIdArray} -->
+    <array name="config_roundedCornerDrawableArray">
+        <item>@drawable/rounded</item>
+        <item>@drawable/rounded_secondary</item>
+    </array>
+
+    <!-- The top rounded corner drawable for each display in a multi-display device.
+         {@see com.android.internal.R.array#config_displayUniqueIdArray} -->
+    <array name="config_roundedCornerTopDrawableArray">
+        <item>@drawable/rounded_corner_top</item>
+        <item>@drawable/rounded_corner_top_secondary</item>
+    </array>
+
+    <!-- The bottom rounded corner drawable for each display in a multi-display device.
+         {@see com.android.internal.R.array#config_displayUniqueIdArray} -->
+    <array name="config_roundedCornerBottomDrawableArray">
+        <item>@drawable/rounded_corner_bottom</item>
+        <item>@drawable/rounded_corner_bottom_secondary</item>
+    </array>
+
+    <!-- Flag to enable privacy dot views, it shall be true for normal case -->
+    <bool name="config_enablePrivacyDot">true</bool>
+
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c231afc..7293f31 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -56,13 +56,10 @@
     <!-- The amount by which the arrow is shifted to avoid the finger-->
     <dimen name="navigation_edge_finger_offset">48dp</dimen>
 
-    <!-- Luminance threshold to determine black/white contrast for the navigation affordances -->
-    <item name="navigation_luminance_threshold" type="dimen" format="float">0.5</item>
-    <!-- Luminance change threshold that allows applying new value if difference was exceeded -->
-    <item name="navigation_luminance_change_threshold" type="dimen" format="float">0.05</item>
-
     <dimen name="floating_rotation_button_diameter">40dp</dimen>
-    <dimen name="floating_rotation_button_min_margin">4dp</dimen>
+    <dimen name="floating_rotation_button_min_margin">20dp</dimen>
+    <dimen name="floating_rotation_button_taskbar_left_margin">20dp</dimen>
+    <dimen name="floating_rotation_button_taskbar_bottom_margin">10dp</dimen>
 
     <!-- Height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
@@ -401,8 +398,11 @@
     <dimen name="status_bar_header_padding_bottom">48dp</dimen>
 
     <!-- The height of the container that holds the battery and time in the quick settings header.
+    Preferred over using "@*android:dimen/quick_qs_offset_height" as system icons are not always
+    present in quick settings (e.g. in split shade) and it's useful to be able to override this
+    value in such cases.
          -->
-    <dimen name="qs_header_system_icons_area_height">48dp</dimen>
+    <dimen name="qs_header_system_icons_area_height">@*android:dimen/quick_qs_offset_height</dimen>
 
     <!-- How far the quick-quick settings panel extends below the status bar -->
     <dimen name="qs_quick_header_panel_height">128dp</dimen>
@@ -454,6 +454,10 @@
     <!-- Width for the notification panel and related windows -->
     <dimen name="match_parent">-1px</dimen>
 
+    <!-- Height of status bar in split shade mode - visible only on large screens -->
+    <dimen name="split_shade_header_height">@*android:dimen/quick_qs_offset_height</dimen>
+    <dimen name="split_shade_header_min_height">@dimen/qs_header_row_min_height</dimen>
+
     <!-- The top margin of the panel that holds the list of notifications. -->
     <dimen name="notification_panel_margin_top">0dp</dimen>
 
@@ -603,7 +607,7 @@
     <dimen name="qs_detail_item_primary_text_size">16sp</dimen>
     <dimen name="qs_detail_item_secondary_text_size">14sp</dimen>
     <dimen name="qs_detail_empty_text_size">14sp</dimen>
-    <dimen name="qs_detail_margin_top">28dp</dimen>
+    <dimen name="qs_detail_header_margin_top">28dp</dimen>
     <dimen name="qs_detail_back_margin_end">16dp</dimen>
     <dimen name="qs_detail_header_text_padding">16dp</dimen>
     <dimen name="qs_data_usage_text_size">14sp</dimen>
@@ -625,6 +629,7 @@
     <dimen name="qs_footer_icon_size">20dp</dimen>
     <dimen name="qs_header_top_padding">15dp</dimen>
     <dimen name="qs_header_bottom_padding">14dp</dimen>
+    <dimen name="qs_header_row_min_height">48dp</dimen>
 
     <dimen name="qs_footer_padding">20dp</dimen>
     <dimen name="qs_security_footer_height">88dp</dimen>
@@ -653,6 +658,8 @@
     <!-- Padding between subtitles and the following text in the QSFooter dialog -->
     <dimen name="qs_footer_dialog_subtitle_padding">20dp</dimen>
 
+    <dimen name="qs_detail_margin_top">@*android:dimen/quick_qs_offset_height</dimen>
+
     <dimen name="seek_bar_height">3dp</dimen>
     <dimen name="seek_bar_corner_radius">3dp</dimen>
 
@@ -908,6 +915,7 @@
 
     <dimen name="keyguard_affordance_fixed_height">48dp</dimen>
     <dimen name="keyguard_affordance_fixed_width">48dp</dimen>
+    <dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
 
     <dimen name="keyguard_affordance_horizontal_offset">32dp</dimen>
     <dimen name="keyguard_affordance_vertical_offset">32dp</dimen>
@@ -1137,7 +1145,7 @@
 
     <!-- The maximum offset in either direction that elements are moved vertically to prevent
          burn-in on AOD. -->
-    <dimen name="burn_in_prevention_offset_y_large_clock">42dp</dimen>
+    <dimen name="burn_in_prevention_offset_y_clock">42dp</dimen>
 
     <!-- Clock maximum font size (dp is intentional, to prevent any further scaling) -->
     <dimen name="large_clock_text_size">150dp</dimen>
@@ -1324,6 +1332,7 @@
     <dimen name="magnifier_up_down_controls_height">40dp</dimen>
     <!-- The extra padding to show the whole outer border -->
     <dimen name="magnifier_drag_handle_padding">3dp</dimen>
+    <dimen name="magnification_max_frame_size">300dp</dimen>
 
     <!-- Home Controls -->
     <dimen name="controls_header_side_margin">4dp</dimen>
@@ -1430,10 +1439,10 @@
     <!-- Output switcher panel related dimensions -->
     <dimen name="media_output_dialog_list_margin">12dp</dimen>
     <dimen name="media_output_dialog_list_max_height">364dp</dimen>
-    <dimen name="media_output_dialog_header_album_icon_size">52dp</dimen>
-    <dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
+    <dimen name="media_output_dialog_header_album_icon_size">48dp</dimen>
+    <dimen name="media_output_dialog_header_back_icon_size">32dp</dimen>
     <dimen name="media_output_dialog_header_icon_padding">16dp</dimen>
-    <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
+    <dimen name="media_output_dialog_icon_corner_radius">8dp</dimen>
     <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
 
     <!-- Distance that the full shade transition takes in order for qs to fully transition to the
@@ -1550,6 +1559,13 @@
     <!-- Location on the screen of the center of the fingerprint sensor. For devices with under
      display fingerprint sensors, this directly corresponds to the fingerprint sensor's location.
      For devices with sensors on the back of the device, this corresponds to the location on the
+     screen directly in front of the sensor.
+     By default, this is set to @null to use the horizontal center of the screen. -->
+    <dimen name="physical_fingerprint_sensor_center_screen_location_x">@null</dimen>
+
+    <!-- Location on the screen of the center of the fingerprint sensor. For devices with under
+     display fingerprint sensors, this directly corresponds to the fingerprint sensor's location.
+     For devices with sensors on the back of the device, this corresponds to the location on the
      screen directly in front of the sensor. -->
     <dimen name="physical_fingerprint_sensor_center_screen_location_y">610px</dimen>
 
@@ -1586,8 +1602,10 @@
 
     <!-- Internet panel related dimensions -->
     <dimen name="internet_dialog_list_margin">12dp</dimen>
-    <dimen name="internet_dialog_list_max_height">646dp</dimen>
-    <dimen name="internet_dialog_list_max_width">@dimen/match_parent</dimen>
+    <dimen name="internet_dialog_list_max_height">662dp</dimen>
+
+    <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
+    <dimen name="large_dialog_width">@dimen/match_parent</dimen>
 
     <!-- Signal icon in internet dialog -->
     <dimen name="signal_strength_icon_size">24dp</dimen>
@@ -1621,4 +1639,9 @@
     <item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
 
     <dimen name="drag_and_drop_icon_size">70dp</dimen>
+
+    <dimen name="qs_dialog_button_horizontal_padding">16dp</dimen>
+    <dimen name="qs_dialog_button_vertical_padding">8dp</dimen>
+    <!-- The button will be 48dp tall, but the background needs to be 36dp tall -->
+    <dimen name="qs_dialog_button_vertical_inset">6dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index efa8754..c598097 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -18,40 +18,12 @@
 <resources>
     <bool name="are_flags_overrideable">false</bool>
 
-    <bool name="flag_notification_pipeline2">true</bool>
-    <bool name="flag_notification_pipeline2_rendering">false</bool>
-    <bool name="flag_notif_updates">true</bool>
-
-    <bool name="flag_monet">false</bool>
-
-    <!-- b/171917882 -->
-    <bool name="flag_notification_twocolumn">false</bool>
-
-    <!-- AOD/Lockscreen alternate layout -->
-    <bool name="flag_keyguard_layout">true</bool>
+    <bool name="flag_monet">true</bool>
 
     <!-- People Tile flag -->
     <bool name="flag_conversations">false</bool>
 
-    <!-- The new animations to/from lockscreen and AOD! -->
-    <bool name="flag_lockscreen_animations">true</bool>
-
-    <!-- The new swipe to unlock animation, which shows the app/launcher behind the keyguard during
-    the swipe. -->
-    <bool name="flag_new_unlock_swipe_animation">true</bool>
-
-    <!-- The shared-element transition between lockscreen smartspace and launcher smartspace. -->
-    <bool name="flag_smartspace_shared_element_transition">false</bool>
-
-    <bool name="flag_pm_lite">true</bool>
-
     <bool name="flag_charging_ripple">false</bool>
 
-    <bool name="flag_ongoing_call_status_bar_chip">true</bool>
-
     <bool name="flag_smartspace">false</bool>
-
-    <bool name="flag_smartspace_deduping">true</bool>
-
-    <bool name="flag_combined_status_bar_signal_icons">false</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4ad4fa9..523b9c8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1779,8 +1779,6 @@
     <string name="tuner_full_importance_settings">Power notification controls</string>
     <string name="tuner_full_importance_settings_on">On</string>
     <string name="tuner_full_importance_settings_off">Off</string>
-    <!-- [CHAR LIMIT=NONE] Notification camera based rotation enabled description -->
-    <string name="rotation_lock_camera_rotation_on">On - Face-based</string>
     <string name="power_notification_controls_description">With power notification controls, you can set an importance level from 0 to 5 for an app\'s notifications.
         \n\n<b>Level 5</b>
         \n- Show at the top of the notification list
@@ -2832,6 +2830,8 @@
     <string name="controls_media_settings_button">Settings</string>
     <!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
     <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
+    <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
+    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
 
     <!-- Title for Smartspace recommendation card within media controls. The "Play" means the action to play a media [CHAR_LIMIT=10] -->
     <string name="controls_media_smartspace_rec_title">Play</string>
@@ -2992,11 +2992,6 @@
     <!-- Content description for a chip in the status bar showing that the user is currently on a phone call. [CHAR LIMIT=NONE] -->
     <string name="ongoing_phone_call_content_description">Ongoing phone call</string>
 
-    <!-- Placeholder for string describing changes in global actions -->
-    <string name="global_actions_change_description" translatable="false"><xliff:g>%1$s</xliff:g></string>
-    <!-- URL for more information about changes in global actions -->
-    <string name="global_actions_change_url" translatable="false"></string>
-
     <!-- Provider Model: Default title of the mobile network in the mobile layout. [CHAR LIMIT=50] -->
     <string name="mobile_data_settings_title">Mobile data</string>
     <!-- Provider Model: Summary text separator for preferences including a short description
@@ -3033,6 +3028,7 @@
     <string name="see_all_networks">See all</string>
     <!-- Summary for warning to disconnect ethernet first then switch to other networks. [CHAR LIMIT=60] -->
     <string name="to_switch_networks_disconnect_ethernet">To switch networks, disconnect ethernet</string>
-    <!-- Message to describe "Wi-Fi scan always available feature" when Wi-Fi is off and Wi-Fi scanning is on. [CHAR LIMIT=NONE] -->
-    <string name="wifi_scan_notify_message">To improve device experience, apps and services can still scan for Wi\u2011Fi networks at any time, even when Wi\u2011Fi is off. You can change this in Wi\u2011Fi scanning settings. <annotation id="link">Change</annotation></string>
+
+    <!-- Title for User Switch dialog. [CHAR LIMIT=20] -->
+    <string name="qs_user_switch_dialog_title">Select user</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 93d60cc..ff299ea 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -331,9 +331,6 @@
     <style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast">
     </style>
 
-    <style name="Animation.MediaOutputDialog" parent="@android:style/Animation.InputMethod">
-    </style>
-
     <!-- Standard animations for hiding and showing the status bar. -->
     <style name="Animation.StatusBar">
     </style>
@@ -420,7 +417,9 @@
         <item name="android:windowIsFloating">true</item>
     </style>
 
-    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog" />
+    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog">
+        <item name="android:buttonCornerRadius">28dp</item>
+    </style>
 
     <style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
 
@@ -436,10 +435,6 @@
         <item name="android:windowCloseOnTouchOutside">true</item>
     </style>
 
-    <style name="Theme.SystemUI.Dialog.MediaOutput">
-        <item name="android:windowBackground">@drawable/media_output_dialog_background</item>
-    </style>
-
     <style name="QSBorderlessButton">
         <item name="android:padding">12dp</item>
         <item name="android:background">@drawable/qs_btn_borderless_rect</item>
@@ -520,6 +515,10 @@
         <item name="android:background">@drawable/btn_borderless_rect</item>
     </style>
 
+    <style name="MediaOutputRoundedOutlinedButton" parent="@android:style/Widget.Material.Button">
+        <item name="android:background">@drawable/media_output_dialog_button_background</item>
+    </style>
+
     <style name="TunerSettings" parent="@android:style/Theme.DeviceDefault.Settings">
         <item name="android:windowActionBar">false</item>
         <item name="preferenceTheme">@style/TunerPreferenceTheme</item>
@@ -936,6 +935,27 @@
         <item name="actionDividerHeight">32dp</item>
     </style>
 
+    <style name="TextAppearance.QSDialog.Title" parent="Theme.SystemUI.Dialog">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textSize">24sp</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+        <item name="android:lineHeight">32sp</item>
+    </style>
+
+    <style name="Widget.QSDialog.Button" parent = "Theme.SystemUI.Dialog">
+        <item name="android:background">@drawable/qs_dialog_btn_filled</item>
+        <item name="android:textColor">@color/prv_text_color_on_accent</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:lineHeight">20sp</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+        <item name="android:stateListAnimator">@null</item>
+    </style>
+
+    <style name="Widget.QSDialog.Button.BorderButton">
+        <item name="android:background">@drawable/qs_dialog_btn_outline</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
     <style name="Theme.SystemUI.Dialog.Internet">
         <item name="android:windowBackground">@drawable/internet_dialog_background</item>
     </style>
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index b2ae2a0..4880b12 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -43,11 +43,13 @@
         "src/**/*.kt",
         "src/**/*.aidl",
         ":wm_shell-aidls",
+        ":wm_shell_util-sources",
     ],
 
     static_libs: [
         "PluginCoreLib",
+        "androidx.dynamicanimation_dynamicanimation",
     ],
     java_version: "1.8",
-    min_sdk_version: "26",
+    min_sdk_version: "current",
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
new file mode 100644
index 0000000..9010d51
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.shared.animation
+
+import android.graphics.Point
+import android.view.Surface
+import android.view.View
+import android.view.WindowManager
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import java.lang.ref.WeakReference
+
+/**
+ * Creates an animation where all registered views are moved into their final location
+ * by moving from the center of the screen to the sides
+ */
+class UnfoldMoveFromCenterAnimator @JvmOverloads constructor(
+    private val windowManager: WindowManager,
+    /**
+     * Allows to set custom translation applier
+     * Could be useful when a view could be translated from
+     * several sources and we want to set the translation
+     * using custom methods instead of [View.setTranslationX] or
+     * [View.setTranslationY]
+     */
+    private val translationApplier: TranslationApplier = object : TranslationApplier {},
+    /**
+     * Allows to set custom implementation for getting
+     * view location. Could be useful if logical view bounds
+     * are different than actual bounds (e.g. view container may
+     * have larger width than width of the items in the container)
+     */
+    private val viewCenterProvider: ViewCenterProvider = object : ViewCenterProvider {}
+) : UnfoldTransitionProgressProvider.TransitionProgressListener {
+
+    private val screenSize = Point()
+    private var isVerticalFold = false
+
+    private val animatedViews: MutableList<AnimatedView> = arrayListOf()
+
+    private var lastAnimationProgress: Float = 0f
+
+    /**
+     * Updates display properties in order to calculate the initial position for the views
+     * Must be called before [registerViewForAnimation]
+     */
+    fun updateDisplayProperties() {
+        windowManager.defaultDisplay.getSize(screenSize)
+
+        // Simple implementation to get current fold orientation,
+        // this might not be correct on all devices
+        // TODO: use JetPack WindowManager library to get the fold orientation
+        isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 ||
+            windowManager.defaultDisplay.rotation == Surface.ROTATION_180
+    }
+
+    /**
+     * If target view positions have changed (e.g. because of layout changes) call this method
+     * to re-query view positions and update the translations
+     */
+    fun updateViewPositions() {
+        animatedViews.forEach { animatedView ->
+            animatedView.view.get()?.let {
+                animatedView.updateAnimatedView(it)
+            }
+        }
+        onTransitionProgress(lastAnimationProgress)
+    }
+
+    /**
+     * Registers a view to be animated, the view should be measured and layouted
+     * After finishing the animation it is necessary to clear
+     * the views using [clearRegisteredViews]
+     */
+    fun registerViewForAnimation(view: View) {
+        val animatedView = createAnimatedView(view)
+        animatedViews.add(animatedView)
+    }
+
+    /**
+     * Unregisters all registered views and resets their translation
+     */
+    fun clearRegisteredViews() {
+        onTransitionProgress(1f)
+        animatedViews.clear()
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        animatedViews.forEach {
+            it.view.get()?.let { view ->
+                translationApplier.apply(
+                    view = view,
+                    x = it.startTranslationX * (1 - progress),
+                    y = it.startTranslationY * (1 - progress)
+                )
+            }
+        }
+        lastAnimationProgress = progress
+    }
+
+    private fun createAnimatedView(view: View): AnimatedView =
+        AnimatedView(view = WeakReference(view)).updateAnimatedView(view)
+
+    private fun AnimatedView.updateAnimatedView(view: View): AnimatedView {
+        val viewCenter = Point()
+        viewCenterProvider.getViewCenter(view, viewCenter)
+
+        val viewCenterX = viewCenter.x
+        val viewCenterY = viewCenter.y
+
+        if (isVerticalFold) {
+            val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX
+            startTranslationX = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
+            startTranslationY = 0f
+        } else {
+            val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY
+            startTranslationX = 0f
+            startTranslationY = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
+        }
+
+        return this
+    }
+
+    /**
+     * Interface that allows to use custom logic to apply translation to view
+     */
+    interface TranslationApplier {
+        /**
+         * Called when we need to apply [x] and [y] translation to [view]
+         */
+        fun apply(view: View, x: Float, y: Float) {
+            view.translationX = x
+            view.translationY = y
+        }
+    }
+
+    /**
+     * Interface that allows to use custom logic to get the center of the view
+     */
+    interface ViewCenterProvider {
+        /**
+         * Called when we need to get the center of the view
+         */
+        fun getViewCenter(view: View, outPoint: Point) {
+            val viewLocation = IntArray(2)
+            view.getLocationOnScreen(viewLocation)
+
+            val viewX = viewLocation[0]
+            val viewY = viewLocation[1]
+
+            outPoint.x = viewX + view.width / 2
+            outPoint.y = viewY + view.height / 2
+        }
+    }
+
+    private class AnimatedView(
+        val view: WeakReference<View>,
+        var startTranslationX: Float = 0f,
+        var startTranslationY: Float = 0f
+    )
+}
+
+private const val TRANSLATION_PERCENTAGE = 0.3f
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/RegionSamplingHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/navigationbar/gestural/RegionSamplingHelper.java
rename to packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java
index 560d89a..9808374 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/RegionSamplingHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java
@@ -14,12 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.navigationbar.gestural;
+package com.android.systemui.shared.navigationbar;
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import android.content.res.Resources;
+import android.annotation.TargetApi;
 import android.graphics.Rect;
+import android.os.Build;
 import android.os.Handler;
 import android.view.CompositionSamplingListener;
 import android.view.SurfaceControl;
@@ -27,16 +28,22 @@
 import android.view.ViewRootImpl;
 import android.view.ViewTreeObserver;
 
-import com.android.systemui.R;
-
 import java.io.PrintWriter;
+import java.util.concurrent.Executor;
 
 /**
  * A helper class to sample regions on the screen and inspect its luminosity.
  */
+@TargetApi(Build.VERSION_CODES.Q)
 public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
         View.OnLayoutChangeListener {
 
+    // Luminance threshold to determine black/white contrast for the navigation affordances.
+    // Passing the threshold of this luminance value will make the button black otherwise white
+    private static final float NAVIGATION_LUMINANCE_THRESHOLD = 0.5f;
+    // Luminance change threshold that allows applying new value if difference was exceeded
+    private static final float NAVIGATION_LUMINANCE_CHANGE_THRESHOLD = 0.05f;
+
     private final Handler mHandler = new Handler();
     private final View mSampledView;
 
@@ -52,6 +59,7 @@
      */
     private final Rect mRegisteredSamplingBounds = new Rect();
     private final SamplingCallback mCallback;
+    private final Executor mBackgroundExecutor;
     private boolean mSamplingEnabled = false;
     private boolean mSamplingListenerRegistered = false;
 
@@ -60,13 +68,12 @@
     private boolean mWaitingOnDraw;
     private boolean mIsDestroyed;
 
-    // Passing the threshold of this luminance value will make the button black otherwise white
-    private final float mLuminanceThreshold;
-    private final float mLuminanceChangeThreshold;
     private boolean mFirstSamplingAfterStart;
     private boolean mWindowVisible;
     private boolean mWindowHasBlurs;
     private SurfaceControl mRegisteredStopLayer = null;
+    // A copy of mRegisteredStopLayer where we own the life cycle and can access from a bg thread.
+    private SurfaceControl mWrappedStopLayer = null;
     private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() {
         @Override
         public void onDraw() {
@@ -82,7 +89,9 @@
         }
     };
 
-    public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback) {
+    public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
+            Executor backgroundExecutor) {
+        mBackgroundExecutor = backgroundExecutor;
         mSamplingListener = new CompositionSamplingListener(
                 sampledView.getContext().getMainExecutor()) {
             @Override
@@ -96,9 +105,6 @@
         mSampledView.addOnAttachStateChangeListener(this);
         mSampledView.addOnLayoutChangeListener(this);
 
-        final Resources res = sampledView.getResources();
-        mLuminanceThreshold = res.getFloat(R.dimen.navigation_luminance_threshold);
-        mLuminanceChangeThreshold = res.getFloat(R.dimen.navigation_luminance_change_threshold);
         mCallback = samplingCallback;
     }
 
@@ -180,13 +186,21 @@
             }
             if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds)
                     || mRegisteredStopLayer != stopLayerControl) {
-                // We only want to reregister if something actually changed
+                // We only want to re-register if something actually changed
                 unregisterSamplingListener();
                 mSamplingListenerRegistered = true;
-                CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
-                        stopLayerControl, mSamplingRequestBounds);
+                SurfaceControl wrappedStopLayer = stopLayerControl == null
+                        ? null : new SurfaceControl(stopLayerControl, "regionSampling");
+                mBackgroundExecutor.execute(() -> {
+                    if (wrappedStopLayer != null && !wrappedStopLayer.isValid()) {
+                        return;
+                    }
+                    CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
+                            wrappedStopLayer, mSamplingRequestBounds);
+                });
                 mRegisteredSamplingBounds.set(mSamplingRequestBounds);
                 mRegisteredStopLayer = stopLayerControl;
+                mWrappedStopLayer = wrappedStopLayer;
             }
             mFirstSamplingAfterStart = false;
         } else {
@@ -197,9 +211,15 @@
     private void unregisterSamplingListener() {
         if (mSamplingListenerRegistered) {
             mSamplingListenerRegistered = false;
+            SurfaceControl wrappedStopLayer = mWrappedStopLayer;
             mRegisteredStopLayer = null;
             mRegisteredSamplingBounds.setEmpty();
-            CompositionSamplingListener.unregister(mSamplingListener);
+            mBackgroundExecutor.execute(() -> {
+                CompositionSamplingListener.unregister(mSamplingListener);
+                if (wrappedStopLayer != null && wrappedStopLayer.isValid()) {
+                    wrappedStopLayer.release();
+                }
+            });
         }
     }
 
@@ -208,8 +228,10 @@
 
         // If the difference between the new luma and the current luma is larger than threshold
         // then apply the current luma, this is to prevent small changes causing colors to flicker
-        if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) > mLuminanceChangeThreshold) {
-            mCallback.onRegionDarknessChanged(medianLuma < mLuminanceThreshold /* isRegionDark */);
+        if (Math.abs(mCurrentMedianLuma - mLastMedianLuma)
+                > NAVIGATION_LUMINANCE_CHANGE_THRESHOLD) {
+            mCallback.onRegionDarknessChanged(
+                    medianLuma < NAVIGATION_LUMINANCE_THRESHOLD /* isRegionDark */);
             mLastMedianLuma = medianLuma;
         }
     }
@@ -251,6 +273,7 @@
         pw.println("  mWindowHasBlurs: " + mWindowHasBlurs);
         pw.println("  mWaitingOnDraw: " + mWaitingOnDraw);
         pw.println("  mRegisteredStopLayer: " + mRegisteredStopLayer);
+        pw.println("  mWrappedStopLayer: " + mWrappedStopLayer);
         pw.println("  mIsDestroyed: " + mIsDestroyed);
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
new file mode 100644
index 0000000..9ea4b57
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
@@ -0,0 +1,432 @@
+/*
+ * 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.systemui.shared.plugins;
+
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.LayoutInflater;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Coordinates all the available plugins for a given action.
+ *
+ * The available plugins are queried from the {@link PackageManager} via an an {@link Intent}
+ * action.
+ *
+ * @param <T> The type of plugin that this contains.
+ */
+public class PluginActionManager<T extends Plugin> {
+
+    private static final boolean DEBUG = false;
+
+    private static final String TAG = "PluginInstanceManager";
+    public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
+
+    private final Context mContext;
+    private final PluginListener<T> mListener;
+    private final String mAction;
+    private final boolean mAllowMultiple;
+    private final NotificationManager mNotificationManager;
+    private final PluginEnabler mPluginEnabler;
+    private final PluginInstance.Factory mPluginInstanceFactory;
+    private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
+
+    @VisibleForTesting
+    private final ArrayList<PluginInstance<T>> mPluginInstances = new ArrayList<>();
+    private final boolean mIsDebuggable;
+    private final PackageManager mPm;
+    private final Class<T> mPluginClass;
+    private final Executor mMainExecutor;
+    private final Executor mBgExecutor;
+
+    private PluginActionManager(
+            Context context,
+            PackageManager pm,
+            String action,
+            PluginListener<T> listener,
+            Class<T> pluginClass,
+            boolean allowMultiple,
+            Executor mainExecutor,
+            Executor bgExecutor,
+            boolean debuggable,
+            NotificationManager notificationManager,
+            PluginEnabler pluginEnabler,
+            List<String> privilegedPlugins,
+            PluginInstance.Factory pluginInstanceFactory) {
+        mPluginClass = pluginClass;
+        mMainExecutor = mainExecutor;
+        mBgExecutor = bgExecutor;
+        mContext = context;
+        mPm = pm;
+        mAction = action;
+        mListener = listener;
+        mAllowMultiple = allowMultiple;
+        mNotificationManager = notificationManager;
+        mPluginEnabler = pluginEnabler;
+        mPluginInstanceFactory = pluginInstanceFactory;
+        mPrivilegedPlugins.addAll(privilegedPlugins);
+        mIsDebuggable = debuggable;
+    }
+
+    /** Load all plugins matching this instance's action. */
+    public void loadAll() {
+        if (DEBUG) Log.d(TAG, "startListening");
+        mBgExecutor.execute(this::queryAll);
+    }
+
+    /** Unload all plugins managed by this instance. */
+    public void destroy() {
+        if (DEBUG) Log.d(TAG, "stopListening");
+        ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
+        for (PluginInstance<T> plugInstance : plugins) {
+            mMainExecutor.execute(() -> onPluginDisconnected(plugInstance));
+        }
+    }
+
+    /** Unload all matching plugins managed by this instance. */
+    public void onPackageRemoved(String pkg) {
+        mBgExecutor.execute(() -> removePkg(pkg));
+    }
+
+    /** Unload and then reload all matching plugins managed by this instance. */
+    public void reloadPackage(String pkg) {
+        mBgExecutor.execute(() -> {
+            removePkg(pkg);
+            queryPkg(pkg);
+        });
+    }
+
+    /** Disable a specific plugin managed by this instance. */
+    public boolean checkAndDisable(String className) {
+        boolean disableAny = false;
+        ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
+        for (PluginInstance<T> info : plugins) {
+            if (className.startsWith(info.getPackage())) {
+                disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
+            }
+        }
+        return disableAny;
+    }
+
+    /** Disable all plugins managed by this instance. */
+    public boolean disableAll() {
+        ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
+        boolean disabledAny = false;
+        for (int i = 0; i < plugins.size(); i++) {
+            disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
+        }
+        return disabledAny;
+    }
+
+    boolean isPluginPrivileged(ComponentName pluginName) {
+        for (String componentNameOrPackage : mPrivilegedPlugins) {
+            ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
+            if (componentName == null) {
+                if (componentNameOrPackage.equals(pluginName.getPackageName())) {
+                    return true;
+                }
+            } else {
+                if (componentName.equals(pluginName)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean disable(
+            PluginInstance<T> pluginInstance, @PluginEnabler.DisableReason int reason) {
+        // Live by the sword, die by the sword.
+        // Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
+
+        ComponentName pluginComponent = pluginInstance.getComponentName();
+        // If a plugin is detected in the stack of a crash then this will be called for that
+        // plugin, if the plugin causing a crash cannot be identified, they are all disabled
+        // assuming one of them must be bad.
+        if (isPluginPrivileged(pluginComponent)) {
+            // Don't disable privileged plugins as they are a part of the OS.
+            return false;
+        }
+        Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
+        mPluginEnabler.setDisabled(pluginComponent, reason);
+
+        return true;
+    }
+
+    <C> boolean dependsOn(Plugin p, Class<C> cls) {
+        ArrayList<PluginInstance<T>> instances = new ArrayList<>(mPluginInstances);
+        for (PluginInstance<T> instance : instances) {
+            if (instance.containsPluginClass(p.getClass())) {
+                return instance.getVersionInfo() != null && instance.getVersionInfo().hasClass(cls);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s@%s (action=%s)",
+                getClass().getSimpleName(), hashCode(), mAction);
+    }
+
+    private void onPluginConnected(PluginInstance<T> pluginInstance) {
+        if (DEBUG) Log.d(TAG, "onPluginConnected");
+        PluginPrefs.setHasPlugins(mContext);
+        pluginInstance.onCreate(mContext, mListener);
+    }
+
+    private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
+        if (DEBUG) Log.d(TAG, "onPluginDisconnected");
+        pluginInstance.onDestroy(mListener);
+    }
+
+    private void queryAll() {
+        if (DEBUG) Log.d(TAG, "queryAll " + mAction);
+        for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
+            PluginInstance<T> pluginInstance = mPluginInstances.get(i);
+            mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
+        }
+        mPluginInstances.clear();
+        handleQueryPlugins(null);
+    }
+
+    private void removePkg(String pkg) {
+        for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
+            final PluginInstance<T> pluginInstance = mPluginInstances.get(i);
+            if (pluginInstance.getPackage().equals(pkg)) {
+                mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
+                mPluginInstances.remove(i);
+            }
+        }
+    }
+
+    private void queryPkg(String pkg) {
+        if (DEBUG) Log.d(TAG, "queryPkg " + pkg + " " + mAction);
+        if (mAllowMultiple || (mPluginInstances.size() == 0)) {
+            handleQueryPlugins(pkg);
+        } else {
+            if (DEBUG) Log.d(TAG, "Too many of " + mAction);
+        }
+    }
+
+    private void handleQueryPlugins(String pkgName) {
+        // This isn't actually a service and shouldn't ever be started, but is
+        // a convenient PM based way to manage our plugins.
+        Intent intent = new Intent(mAction);
+        if (pkgName != null) {
+            intent.setPackage(pkgName);
+        }
+        List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);
+        if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
+        if (result.size() > 1 && !mAllowMultiple) {
+            // TODO: Show warning.
+            Log.w(TAG, "Multiple plugins found for " + mAction);
+            if (DEBUG) {
+                for (ResolveInfo info : result) {
+                    ComponentName name = new ComponentName(info.serviceInfo.packageName,
+                            info.serviceInfo.name);
+                    Log.w(TAG, "  " + name);
+                }
+            }
+            return;
+        }
+        for (ResolveInfo info : result) {
+            ComponentName name = new ComponentName(info.serviceInfo.packageName,
+                    info.serviceInfo.name);
+            PluginInstance<T> pluginInstance = loadPluginComponent(name);
+            if (pluginInstance != null) {
+                // add plugin before sending PLUGIN_CONNECTED message
+                mPluginInstances.add(pluginInstance);
+                mMainExecutor.execute(() -> onPluginConnected(pluginInstance));
+            }
+        }
+    }
+
+    private PluginInstance<T> loadPluginComponent(ComponentName component) {
+        // This was already checked, but do it again here to make extra extra sure, we don't
+        // use these on production builds.
+        if (!mIsDebuggable && !isPluginPrivileged(component)) {
+            // Never ever ever allow these on production builds, they are only for prototyping.
+            Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
+            return null;
+        }
+        if (!mPluginEnabler.isEnabled(component)) {
+            if (DEBUG) {
+                Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
+            }
+            return null;
+        }
+        String packageName = component.getPackageName();
+        try {
+            // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
+            if (mPm.checkPermission(PLUGIN_PERMISSION, packageName)
+                    != PackageManager.PERMISSION_GRANTED) {
+                Log.d(TAG, "Plugin doesn't have permission: " + packageName);
+                return null;
+            }
+
+            ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
+            // TODO: Only create the plugin before version check if we need it for
+            // legacy version check.
+            if (DEBUG) {
+                Log.d(TAG, "createPlugin");
+            }
+            try {
+                return mPluginInstanceFactory.create(
+                        mContext, appInfo, component,
+                        mPluginClass);
+            } catch (InvalidVersionException e) {
+                reportInvalidVersion(component, component.getClassName(), e);
+            }
+        } catch (Throwable e) {
+            Log.w(TAG, "Couldn't load plugin: " + packageName, e);
+            return null;
+        }
+
+        return null;
+    }
+
+    private void reportInvalidVersion(
+            ComponentName component, String className, InvalidVersionException e) {
+        final int icon = Resources.getSystem().getIdentifier(
+                "stat_sys_warning", "drawable", "android");
+        final int color = Resources.getSystem().getIdentifier(
+                "system_notification_accent_color", "color", "android");
+        final Notification.Builder nb = new Notification.Builder(mContext,
+                PluginManager.NOTIFICATION_CHANNEL_ID)
+                .setStyle(new Notification.BigTextStyle())
+                .setSmallIcon(icon)
+                .setWhen(0)
+                .setShowWhen(false)
+                .setVisibility(Notification.VISIBILITY_PUBLIC)
+                .setColor(mContext.getColor(color));
+        String label = className;
+        try {
+            label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
+        } catch (NameNotFoundException e2) {
+            // no-op
+        }
+        if (!e.isTooNew()) {
+            // Localization not required as this will never ever appear in a user build.
+            nb.setContentTitle("Plugin \"" + label + "\" is too old")
+                    .setContentText("Contact plugin developer to get an updated"
+                            + " version.\n" + e.getMessage());
+        } else {
+            // Localization not required as this will never ever appear in a user build.
+            nb.setContentTitle("Plugin \"" + label + "\" is too new")
+                    .setContentText("Check to see if an OTA is available.\n"
+                            + e.getMessage());
+        }
+        Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
+                Uri.parse("package://" + component.flattenToString()));
+        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i,
+                PendingIntent.FLAG_IMMUTABLE);
+        nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
+        mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
+        // TODO: Warn user.
+        Log.w(TAG, "Plugin has invalid interface version " + e.getActualVersion()
+                + ", expected " + e.getExpectedVersion());
+    }
+
+    /**
+     * Construct a {@link PluginActionManager}
+     */
+    public static class Factory {
+        private final Context mContext;
+        private final PackageManager mPackageManager;
+        private final Executor mMainExecutor;
+        private final Executor mBgExecutor;
+        private final NotificationManager mNotificationManager;
+        private final PluginEnabler mPluginEnabler;
+        private final List<String> mPrivilegedPlugins;
+        private final PluginInstance.Factory mPluginInstanceFactory;
+
+        public Factory(Context context, PackageManager packageManager,
+                Executor mainExecutor, Executor bgExecutor,
+                NotificationManager notificationManager, PluginEnabler pluginEnabler,
+                List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory) {
+            mContext = context;
+            mPackageManager = packageManager;
+            mMainExecutor = mainExecutor;
+            mBgExecutor = bgExecutor;
+            mNotificationManager = notificationManager;
+            mPluginEnabler = pluginEnabler;
+            mPrivilegedPlugins = privilegedPlugins;
+            mPluginInstanceFactory = pluginInstanceFactory;
+        }
+
+        <T extends Plugin> PluginActionManager<T> create(
+                String action, PluginListener<T> listener, Class<T> pluginClass,
+                boolean allowMultiple, boolean debuggable) {
+            return new PluginActionManager<>(mContext, mPackageManager, action, listener,
+                    pluginClass, allowMultiple, mMainExecutor, mBgExecutor,
+                    debuggable, mNotificationManager, mPluginEnabler,
+                    mPrivilegedPlugins, mPluginInstanceFactory);
+        }
+    }
+
+    /** */
+    public static class PluginContextWrapper extends ContextWrapper {
+        private final ClassLoader mClassLoader;
+        private LayoutInflater mInflater;
+
+        public PluginContextWrapper(Context base, ClassLoader classLoader) {
+            super(base);
+            mClassLoader = classLoader;
+        }
+
+        @Override
+        public ClassLoader getClassLoader() {
+            return mClassLoader;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (LAYOUT_INFLATER_SERVICE.equals(name)) {
+                if (mInflater == null) {
+                    mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
+                }
+                return mInflater;
+            }
+            return getBaseContext().getSystemService(name);
+        }
+    }
+
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInitializer.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInitializer.java
deleted file mode 100644
index 42bc1d0..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInitializer.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.shared.plugins;
-
-import android.content.Context;
-import android.os.Looper;
-
-/**
- * Provides necessary components for initializing {@link PluginManagerImpl}.
- */
-public interface PluginInitializer {
-
-    Looper getBgLooper();
-
-    /**
-     * Called from the bg looper during initialization of {@link PluginManagerImpl}.
-     */
-    void onPluginManagerInit();
-
-    String[] getWhitelistedPlugins(Context context);
-
-    PluginEnabler getPluginEnabler(Context context);
-
-    /**
-     * Called from {@link PluginManagerImpl#handleWtfs()}.
-     */
-    void handleWtfs();
-
-    /**
-     * Returns if pluging manager should run in debug mode.
-     */
-    boolean isDebuggable();
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
new file mode 100644
index 0000000..2f84602
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.plugins;
+
+import android.app.LoadedApk;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginFragment;
+import com.android.systemui.plugins.PluginListener;
+
+import dalvik.system.PathClassLoader;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Contains a single instantiation of a Plugin.
+ *
+ * This class and its related Factory are in charge of actually instantiating a plugin and
+ * managing any state related to it.
+ *
+ * @param <T> The type of plugin that this contains.
+ */
+public class PluginInstance<T extends Plugin> {
+    private static final String TAG = "PluginInstance";
+    private static final Map<String, ClassLoader> sClassLoaders = new ArrayMap<>();
+
+    private final Context mPluginContext;
+    private final VersionInfo mVersionInfo;
+    private final ComponentName mComponentName;
+    private final T mPlugin;
+
+    /** */
+    public PluginInstance(ComponentName componentName, T plugin, Context pluginContext,
+            VersionInfo versionInfo) {
+        mComponentName = componentName;
+        mPlugin = plugin;
+        mPluginContext = pluginContext;
+        mVersionInfo = versionInfo;
+    }
+
+    /** Alerts listener and plugin that the plugin has been created. */
+    public void onCreate(Context appContext, PluginListener<T> listener) {
+        if (!(mPlugin instanceof PluginFragment)) {
+            // Only call onCreate for plugins that aren't fragments, as fragments
+            // will get the onCreate as part of the fragment lifecycle.
+            mPlugin.onCreate(appContext, mPluginContext);
+        }
+        listener.onPluginConnected(mPlugin, mPluginContext);
+    }
+
+    /** Alerts listener and plugin that the plugin is being shutdown. */
+    public void onDestroy(PluginListener<T> listener) {
+        listener.onPluginDisconnected(mPlugin);
+        if (!(mPlugin instanceof PluginFragment)) {
+            // Only call onDestroy for plugins that aren't fragments, as fragments
+            // will get the onDestroy as part of the fragment lifecycle.
+            mPlugin.onDestroy();
+        }
+    }
+
+    /**
+     * Returns if the contained plugin matches the passed in class name.
+     *
+     * It does this by string comparison of the class names.
+     **/
+    public boolean containsPluginClass(Class pluginClass) {
+        return mPlugin.getClass().getName().equals(pluginClass.getName());
+    }
+
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    public String getPackage() {
+        return mComponentName.getPackageName();
+    }
+
+    public VersionInfo getVersionInfo() {
+        return mVersionInfo;
+    }
+
+    @VisibleForTesting
+    Context getPluginContext() {
+        return mPluginContext;
+    }
+
+    /** Used to create new {@link PluginInstance}s. */
+    public static class Factory {
+        private final ClassLoader mBaseClassLoader;
+        private final InstanceFactory<?> mInstanceFactory;
+        private final VersionChecker mVersionChecker;
+        private final boolean mIsDebug;
+        private final List<String> mPrivilegedPlugins;
+
+        /** Factory used to construct {@link PluginInstance}s. */
+        public Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory,
+                VersionChecker versionChecker,
+                List<String> privilegedPlugins,
+                boolean isDebug) {
+            mPrivilegedPlugins = privilegedPlugins;
+            mBaseClassLoader = classLoader;
+            mInstanceFactory = instanceFactory;
+            mVersionChecker = versionChecker;
+            mIsDebug = isDebug;
+        }
+
+        /** Construct a new PluginInstance. */
+        public <T extends Plugin> PluginInstance<T> create(
+                Context context,
+                ApplicationInfo appInfo,
+                ComponentName componentName,
+                Class<T> pluginClass)
+                throws PackageManager.NameNotFoundException, ClassNotFoundException,
+                InstantiationException, IllegalAccessException {
+
+            ClassLoader classLoader = getClassLoader(appInfo, mBaseClassLoader);
+            Context pluginContext = new PluginActionManager.PluginContextWrapper(
+                    context.createApplicationContext(appInfo, 0), classLoader);
+            Class<T> instanceClass = (Class<T>) Class.forName(
+                    componentName.getClassName(), true, classLoader);
+            // TODO: Only create the plugin before version check if we need it for
+            // legacy version check.
+            T instance = (T) mInstanceFactory.create(instanceClass);
+            VersionInfo version = mVersionChecker.checkVersion(
+                    instanceClass, pluginClass, instance);
+            return new PluginInstance<T>(componentName, instance, pluginContext, version);
+        }
+
+        private boolean isPluginPackagePrivileged(String packageName) {
+            for (String componentNameOrPackage : mPrivilegedPlugins) {
+                ComponentName componentName = ComponentName.unflattenFromString(
+                        componentNameOrPackage);
+                if (componentName != null) {
+                    if (componentName.getPackageName().equals(packageName)) {
+                        return true;
+                    }
+                } else if (componentNameOrPackage.equals(packageName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private ClassLoader getParentClassLoader(ClassLoader baseClassLoader) {
+            return new PluginManagerImpl.ClassLoaderFilter(
+                    baseClassLoader, "com.android.systemui.plugin");
+        }
+
+        /** Returns class loader specific for the given plugin. */
+        private ClassLoader getClassLoader(ApplicationInfo appInfo,
+                ClassLoader baseClassLoader) {
+            if (!mIsDebug && !isPluginPackagePrivileged(appInfo.packageName)) {
+                Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
+                        + appInfo.sourceDir + ", pkg: " + appInfo.packageName);
+                return null;
+            }
+            if (sClassLoaders.containsKey(appInfo.packageName)) {
+                return sClassLoaders.get(appInfo.packageName);
+            }
+
+            List<String> zipPaths = new ArrayList<>();
+            List<String> libPaths = new ArrayList<>();
+            LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
+            ClassLoader classLoader = new PathClassLoader(
+                    TextUtils.join(File.pathSeparator, zipPaths),
+                    TextUtils.join(File.pathSeparator, libPaths),
+                    getParentClassLoader(baseClassLoader));
+            sClassLoaders.put(appInfo.packageName, classLoader);
+            return classLoader;
+        }
+    }
+
+    /** Class that compares a plugin class against an implementation for version matching. */
+    public static class VersionChecker {
+        /** Compares two plugin classes. */
+        public <T extends Plugin> VersionInfo checkVersion(
+                Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
+            VersionInfo pluginVersion = new VersionInfo().addClass(pluginClass);
+            VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
+            if (instanceVersion.hasVersionInfo()) {
+                pluginVersion.checkVersion(instanceVersion);
+            } else {
+                int fallbackVersion = plugin.getVersion();
+                if (fallbackVersion != pluginVersion.getDefaultVersion()) {
+                    throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
+                }
+                return null;
+            }
+            return instanceVersion;
+        }
+    }
+
+    /**
+     *  Simple class to create a new instance. Useful for testing.
+     *
+     * @param <T> The type of plugin this create.
+     **/
+    public static class InstanceFactory<T extends Plugin> {
+        T create(Class cls) throws IllegalAccessException, InstantiationException {
+            return (T) cls.newInstance();
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstanceManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstanceManager.java
deleted file mode 100644
index 2b35bcd..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstanceManager.java
+++ /dev/null
@@ -1,462 +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.systemui.shared.plugins;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.util.ArraySet;
-import android.util.Log;
-import android.view.LayoutInflater;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.systemui.plugins.Plugin;
-import com.android.systemui.plugins.PluginFragment;
-import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-public class PluginInstanceManager<T extends Plugin> {
-
-    private static final boolean DEBUG = false;
-
-    private static final String TAG = "PluginInstanceManager";
-    public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
-
-    private final Context mContext;
-    private final PluginListener<T> mListener;
-    private final String mAction;
-    private final boolean mAllowMultiple;
-    private final VersionInfo mVersion;
-
-    @VisibleForTesting
-    final MainHandler mMainHandler;
-    @VisibleForTesting
-    final PluginHandler mPluginHandler;
-    private final boolean isDebuggable;
-    private final PackageManager mPm;
-    private final PluginManagerImpl mManager;
-    private final ArraySet<String> mWhitelistedPlugins = new ArraySet<>();
-
-    PluginInstanceManager(Context context, String action, PluginListener<T> listener,
-            boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager) {
-        this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
-                manager, manager.isDebuggable(), manager.getWhitelistedPlugins());
-    }
-
-    @VisibleForTesting
-    PluginInstanceManager(Context context, PackageManager pm, String action,
-            PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,
-            PluginManagerImpl manager, boolean debuggable, String[] pluginWhitelist) {
-        mMainHandler = new MainHandler(Looper.getMainLooper());
-        mPluginHandler = new PluginHandler(looper);
-        mManager = manager;
-        mContext = context;
-        mPm = pm;
-        mAction = action;
-        mListener = listener;
-        mAllowMultiple = allowMultiple;
-        mVersion = version;
-        mWhitelistedPlugins.addAll(Arrays.asList(pluginWhitelist));
-        isDebuggable = debuggable;
-    }
-
-    public PluginInfo<T> getPlugin() {
-        if (Looper.myLooper() != Looper.getMainLooper()) {
-            throw new RuntimeException("Must be called from UI thread");
-        }
-        mPluginHandler.handleQueryPlugins(null /* All packages */);
-        if (mPluginHandler.mPlugins.size() > 0) {
-            mMainHandler.removeMessages(MainHandler.PLUGIN_CONNECTED);
-            PluginInfo<T> info = mPluginHandler.mPlugins.get(0);
-            PluginPrefs.setHasPlugins(mContext);
-            info.mPlugin.onCreate(mContext, info.mPluginContext);
-            return info;
-        }
-        return null;
-    }
-
-    public void loadAll() {
-        if (DEBUG) Log.d(TAG, "startListening");
-        mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
-    }
-
-    public void destroy() {
-        if (DEBUG) Log.d(TAG, "stopListening");
-        ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins);
-        for (PluginInfo plugin : plugins) {
-            mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
-                    plugin.mPlugin).sendToTarget();
-        }
-    }
-
-    public void onPackageRemoved(String pkg) {
-        mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
-    }
-
-    public void onPackageChange(String pkg) {
-        mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
-        mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkg).sendToTarget();
-    }
-
-    public boolean checkAndDisable(String className) {
-        boolean disableAny = false;
-        ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins);
-        for (PluginInfo info : plugins) {
-            if (className.startsWith(info.mPackage)) {
-                disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
-            }
-        }
-        return disableAny;
-    }
-
-    public boolean disableAll() {
-        ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins);
-        boolean disabledAny = false;
-        for (int i = 0; i < plugins.size(); i++) {
-            disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
-        }
-        return disabledAny;
-    }
-
-    private boolean isPluginWhitelisted(ComponentName pluginName) {
-        for (String componentNameOrPackage : mWhitelistedPlugins) {
-            ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
-            if (componentName == null) {
-                if (componentNameOrPackage.equals(pluginName.getPackageName())) {
-                    return true;
-                }
-            } else {
-                if (componentName.equals(pluginName)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private boolean disable(PluginInfo info, @PluginEnabler.DisableReason int reason) {
-        // Live by the sword, die by the sword.
-        // Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
-
-        ComponentName pluginComponent = new ComponentName(info.mPackage, info.mClass);
-        // If a plugin is detected in the stack of a crash then this will be called for that
-        // plugin, if the plugin causing a crash cannot be identified, they are all disabled
-        // assuming one of them must be bad.
-        if (isPluginWhitelisted(pluginComponent)) {
-            // Don't disable whitelisted plugins as they are a part of the OS.
-            return false;
-        }
-        Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
-        mManager.getPluginEnabler().setDisabled(pluginComponent, reason);
-
-        return true;
-    }
-
-    public <T> boolean dependsOn(Plugin p, Class<T> cls) {
-        ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins);
-        for (PluginInfo info : plugins) {
-            if (info.mPlugin.getClass().getName().equals(p.getClass().getName())) {
-                return info.mVersion != null && info.mVersion.hasClass(cls);
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public String toString() {
-        return String.format("%s@%s (action=%s)",
-                getClass().getSimpleName(), hashCode(), mAction);
-    }
-
-    private class MainHandler extends Handler {
-        private static final int PLUGIN_CONNECTED = 1;
-        private static final int PLUGIN_DISCONNECTED = 2;
-
-        public MainHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case PLUGIN_CONNECTED:
-                    if (DEBUG) Log.d(TAG, "onPluginConnected");
-                    PluginPrefs.setHasPlugins(mContext);
-                    PluginInfo<T> info = (PluginInfo<T>) msg.obj;
-                    mManager.handleWtfs();
-                    if (!(msg.obj instanceof PluginFragment)) {
-                        // Only call onDestroy for plugins that aren't fragments, as fragments
-                        // will get the onCreate as part of the fragment lifecycle.
-                        info.mPlugin.onCreate(mContext, info.mPluginContext);
-                    }
-                    mListener.onPluginConnected(info.mPlugin, info.mPluginContext);
-                    break;
-                case PLUGIN_DISCONNECTED:
-                    if (DEBUG) Log.d(TAG, "onPluginDisconnected");
-                    mListener.onPluginDisconnected((T) msg.obj);
-                    if (!(msg.obj instanceof PluginFragment)) {
-                        // Only call onDestroy for plugins that aren't fragments, as fragments
-                        // will get the onDestroy as part of the fragment lifecycle.
-                        ((T) msg.obj).onDestroy();
-                    }
-                    break;
-                default:
-                    super.handleMessage(msg);
-                    break;
-            }
-        }
-    }
-
-    private class PluginHandler extends Handler {
-        private static final int QUERY_ALL = 1;
-        private static final int QUERY_PKG = 2;
-        private static final int REMOVE_PKG = 3;
-
-        private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>();
-
-        public PluginHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case QUERY_ALL:
-                    if (DEBUG) Log.d(TAG, "queryAll " + mAction);
-                    for (int i = mPlugins.size() - 1; i >= 0; i--) {
-                        PluginInfo<T> pluginInfo = mPlugins.get(i);
-                        mMainHandler.obtainMessage(
-                                MainHandler.PLUGIN_DISCONNECTED, pluginInfo.mPlugin).sendToTarget();
-                    }
-                    mPlugins.clear();
-                    handleQueryPlugins(null);
-                    break;
-                case REMOVE_PKG:
-                    String pkg = (String) msg.obj;
-                    for (int i = mPlugins.size() - 1; i >= 0; i--) {
-                        final PluginInfo<T> plugin = mPlugins.get(i);
-                        if (plugin.mPackage.equals(pkg)) {
-                            mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
-                                    plugin.mPlugin).sendToTarget();
-                            mPlugins.remove(i);
-                        }
-                    }
-                    break;
-                case QUERY_PKG:
-                    String p = (String) msg.obj;
-                    if (DEBUG) Log.d(TAG, "queryPkg " + p + " " + mAction);
-                    if (mAllowMultiple || (mPlugins.size() == 0)) {
-                        handleQueryPlugins(p);
-                    } else {
-                        if (DEBUG) Log.d(TAG, "Too many of " + mAction);
-                    }
-                    break;
-                default:
-                    super.handleMessage(msg);
-            }
-        }
-
-        private void handleQueryPlugins(String pkgName) {
-            // This isn't actually a service and shouldn't ever be started, but is
-            // a convenient PM based way to manage our plugins.
-            Intent intent = new Intent(mAction);
-            if (pkgName != null) {
-                intent.setPackage(pkgName);
-            }
-            List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);
-            if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
-            if (result.size() > 1 && !mAllowMultiple) {
-                // TODO: Show warning.
-                Log.w(TAG, "Multiple plugins found for " + mAction);
-                if (DEBUG) {
-                    for (ResolveInfo info : result) {
-                        ComponentName name = new ComponentName(info.serviceInfo.packageName,
-                                info.serviceInfo.name);
-                        Log.w(TAG, "  " + name);
-                    }
-                }
-                return;
-            }
-            for (ResolveInfo info : result) {
-                ComponentName name = new ComponentName(info.serviceInfo.packageName,
-                        info.serviceInfo.name);
-                PluginInfo<T> t = handleLoadPlugin(name);
-                if (t == null) continue;
-
-                // add plugin before sending PLUGIN_CONNECTED message
-                mPlugins.add(t);
-                mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget();
-            }
-        }
-
-        protected PluginInfo<T> handleLoadPlugin(ComponentName component) {
-            // This was already checked, but do it again here to make extra extra sure, we don't
-            // use these on production builds.
-            if (!isDebuggable && !isPluginWhitelisted(component)) {
-                // Never ever ever allow these on production builds, they are only for prototyping.
-                Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
-                return null;
-            }
-            if (!mManager.getPluginEnabler().isEnabled(component)) {
-                if (DEBUG) Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
-                return null;
-            }
-            String pkg = component.getPackageName();
-            String cls = component.getClassName();
-            try {
-                ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
-                // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
-                if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
-                        != PackageManager.PERMISSION_GRANTED) {
-                    Log.d(TAG, "Plugin doesn't have permission: " + pkg);
-                    return null;
-                }
-                // Create our own ClassLoader so we can use our own code as the parent.
-                ClassLoader classLoader = mManager.getClassLoader(info);
-                Context pluginContext = new PluginContextWrapper(
-                        mContext.createApplicationContext(info, 0), classLoader);
-                Class<?> pluginClass = Class.forName(cls, true, classLoader);
-                // TODO: Only create the plugin before version check if we need it for
-                // legacy version check.
-                T plugin = (T) pluginClass.newInstance();
-                try {
-                    VersionInfo version = checkVersion(pluginClass, plugin, mVersion);
-                    if (DEBUG) Log.d(TAG, "createPlugin");
-                    return new PluginInfo(pkg, cls, plugin, pluginContext, version);
-                } catch (InvalidVersionException e) {
-                    final int icon = Resources.getSystem().getIdentifier(
-                            "stat_sys_warning", "drawable", "android");
-                    final int color = Resources.getSystem().getIdentifier(
-                            "system_notification_accent_color", "color", "android");
-                    final Notification.Builder nb = new Notification.Builder(mContext,
-                            PluginManager.NOTIFICATION_CHANNEL_ID)
-                                    .setStyle(new Notification.BigTextStyle())
-                                    .setSmallIcon(icon)
-                                    .setWhen(0)
-                                    .setShowWhen(false)
-                                    .setVisibility(Notification.VISIBILITY_PUBLIC)
-                                    .setColor(mContext.getColor(color));
-                    String label = cls;
-                    try {
-                        label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
-                    } catch (NameNotFoundException e2) {
-                    }
-                    if (!e.isTooNew()) {
-                        // Localization not required as this will never ever appear in a user build.
-                        nb.setContentTitle("Plugin \"" + label + "\" is too old")
-                                .setContentText("Contact plugin developer to get an updated"
-                                        + " version.\n" + e.getMessage());
-                    } else {
-                        // Localization not required as this will never ever appear in a user build.
-                        nb.setContentTitle("Plugin \"" + label + "\" is too new")
-                                .setContentText("Check to see if an OTA is available.\n"
-                                        + e.getMessage());
-                    }
-                    Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
-                            Uri.parse("package://" + component.flattenToString()));
-                    PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i,
-                            PendingIntent.FLAG_IMMUTABLE);
-                    nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
-                    mContext.getSystemService(NotificationManager.class)
-                            .notify(SystemMessage.NOTE_PLUGIN, nb.build());
-                    // TODO: Warn user.
-                    Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion()
-                            + ", expected " + mVersion);
-                    return null;
-                }
-            } catch (Throwable e) {
-                Log.w(TAG, "Couldn't load plugin: " + pkg, e);
-                return null;
-            }
-        }
-
-        private VersionInfo checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
-                throws InvalidVersionException {
-            VersionInfo pv = new VersionInfo().addClass(pluginClass);
-            if (pv.hasVersionInfo()) {
-                version.checkVersion(pv);
-            } else {
-                int fallbackVersion = plugin.getVersion();
-                if (fallbackVersion != version.getDefaultVersion()) {
-                    throw new InvalidVersionException("Invalid legacy version", false);
-                }
-                return null;
-            }
-            return pv;
-        }
-    }
-
-    public static class PluginContextWrapper extends ContextWrapper {
-        private final ClassLoader mClassLoader;
-        private LayoutInflater mInflater;
-
-        public PluginContextWrapper(Context base, ClassLoader classLoader) {
-            super(base);
-            mClassLoader = classLoader;
-        }
-
-        @Override
-        public ClassLoader getClassLoader() {
-            return mClassLoader;
-        }
-
-        @Override
-        public Object getSystemService(String name) {
-            if (LAYOUT_INFLATER_SERVICE.equals(name)) {
-                if (mInflater == null) {
-                    mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
-                }
-                return mInflater;
-            }
-            return getBaseContext().getSystemService(name);
-        }
-    }
-
-    static class PluginInfo<T> {
-        private final Context mPluginContext;
-        private final VersionInfo mVersion;
-        private String mClass;
-        T mPlugin;
-        String mPackage;
-
-        public PluginInfo(String pkg, String cls, T plugin, Context pluginContext,
-                VersionInfo info) {
-            mPlugin = plugin;
-            mClass = cls;
-            mPackage = pkg;
-            mPluginContext = pluginContext;
-            mVersion = info;
-        }
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java
index 3f907a8..c89be86 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java
@@ -27,18 +27,18 @@
     // must be one of the channels created in NotificationChannels.java
     String NOTIFICATION_CHANNEL_ID = "ALR";
 
-    String[] getWhitelistedPlugins();
+    /** Returns plugins that don't get disabled when an exceptoin occurs. */
+    String[] getPrivilegedPlugins();
 
-    <T extends Plugin> T getOneShotPlugin(Class<T> cls);
-    <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls);
-
-    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls);
-    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
+    /** */
+    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls);
+    /** */
+    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
             boolean allowMultiple);
     <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            Class<?> cls);
+            Class<T> cls);
     <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            Class cls, boolean allowMultiple);
+            Class<T> cls, boolean allowMultiple);
 
     void removePluginListener(PluginListener<?> listener);
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java
index 2b4cdd6..7539f99 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java
@@ -14,48 +14,31 @@
 
 package com.android.systemui.shared.plugins;
 
-import android.app.LoadedApk;
-import android.app.Notification;
-import android.app.Notification.Action;
 import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.SystemProperties;
-import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 import android.widget.Toast;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-import com.android.systemui.shared.plugins.PluginInstanceManager.PluginInfo;
 
-import dalvik.system.PathClassLoader;
-
-import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.Thread.UncaughtExceptionHandler;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+
 /**
  * @see Plugin
  */
@@ -64,113 +47,65 @@
     private static final String TAG = PluginManagerImpl.class.getSimpleName();
     static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";
 
-    private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap
+    private final ArrayMap<PluginListener<?>, PluginActionManager<?>> mPluginMap
             = new ArrayMap<>();
     private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
-    private final ArraySet<String> mOneShotPackages = new ArraySet<>();
-    private final ArraySet<String> mWhitelistedPlugins = new ArraySet<>();
+    private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
     private final Context mContext;
-    private final PluginInstanceManagerFactory mFactory;
+    private final PluginActionManager.Factory mActionManagerFactory;
     private final boolean mIsDebuggable;
     private final PluginPrefs mPluginPrefs;
     private final PluginEnabler mPluginEnabler;
-    private final PluginInitializer mPluginInitializer;
-    private ClassLoaderFilter mParentClassLoader;
     private boolean mListening;
-    private boolean mHasOneShot;
-    private Looper mLooper;
 
-    public PluginManagerImpl(Context context, PluginInitializer initializer) {
-        this(context, new PluginInstanceManagerFactory(), initializer.isDebuggable(),
-                Thread.getUncaughtExceptionPreHandler(), initializer);
-    }
-
-    @VisibleForTesting
-    PluginManagerImpl(Context context, PluginInstanceManagerFactory factory, boolean debuggable,
-            UncaughtExceptionHandler defaultHandler, final PluginInitializer initializer) {
+    public PluginManagerImpl(Context context,
+            PluginActionManager.Factory actionManagerFactory,
+            boolean debuggable,
+            Optional<UncaughtExceptionHandler> defaultHandlerOptional,
+            PluginEnabler pluginEnabler,
+            PluginPrefs pluginPrefs,
+            List<String> privilegedPlugins) {
         mContext = context;
-        mFactory = factory;
-        mLooper = initializer.getBgLooper();
+        mActionManagerFactory = actionManagerFactory;
         mIsDebuggable = debuggable;
-        mWhitelistedPlugins.addAll(Arrays.asList(initializer.getWhitelistedPlugins(mContext)));
-        mPluginPrefs = new PluginPrefs(mContext);
-        mPluginEnabler = initializer.getPluginEnabler(mContext);
-        mPluginInitializer = initializer;
+        mPrivilegedPlugins.addAll(privilegedPlugins);
+        mPluginPrefs = pluginPrefs;
+        mPluginEnabler = pluginEnabler;
 
         PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler(
-                defaultHandler);
+                defaultHandlerOptional);
         Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler);
-
-        new Handler(mLooper).post(new Runnable() {
-            @Override
-            public void run() {
-                initializer.onPluginManagerInit();
-            }
-        });
     }
 
     public boolean isDebuggable() {
         return mIsDebuggable;
     }
 
-    public String[] getWhitelistedPlugins() {
-        return mWhitelistedPlugins.toArray(new String[0]);
+    public String[] getPrivilegedPlugins() {
+        return mPrivilegedPlugins.toArray(new String[0]);
     }
 
-    public PluginEnabler getPluginEnabler() {
-        return mPluginEnabler;
-    }
-
-    // TODO(mankoff): This appears to be only called from tests. Remove?
-    public <T extends Plugin> T getOneShotPlugin(Class<T> cls) {
-        ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
-        if (info == null) {
-            throw new RuntimeException(cls + " doesn't provide an interface");
-        }
-        if (TextUtils.isEmpty(info.action())) {
-            throw new RuntimeException(cls + " doesn't provide an action");
-        }
-        return getOneShotPlugin(info.action(), cls);
-    }
-
-    public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
-        if (Looper.myLooper() != Looper.getMainLooper()) {
-            throw new RuntimeException("Must be called from UI thread");
-        }
-        // Passing null causes compiler to complain about incompatible (generic) types.
-        PluginListener<Plugin> dummy = null;
-        PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, dummy,
-                false, mLooper, cls, this);
-        mPluginPrefs.addAction(action);
-        PluginInfo<T> info = p.getPlugin();
-        if (info != null) {
-            mOneShotPackages.add(info.mPackage);
-            mHasOneShot = true;
-            startListening();
-            return info.mPlugin;
-        }
-        return null;
-    }
-
-    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
+    /** */
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls) {
         addPluginListener(listener, cls, false);
     }
 
-    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
+    /** */
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
             boolean allowMultiple) {
         addPluginListener(PluginManager.Helper.getAction(cls), listener, cls, allowMultiple);
     }
 
     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            Class<?> cls) {
+            Class<T> cls) {
         addPluginListener(action, listener, cls, false);
     }
 
     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            Class cls, boolean allowMultiple) {
+            Class<T> cls, boolean allowMultiple) {
         mPluginPrefs.addAction(action);
-        PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
-                allowMultiple, mLooper, cls, this);
+        PluginActionManager<T> p = mActionManagerFactory.create(action, listener, cls,
+                allowMultiple, isDebuggable());
         p.loadAll();
         synchronized (this) {
             mPluginMap.put(listener, p);
@@ -202,14 +137,13 @@
         filter.addAction(PLUGIN_CHANGED);
         filter.addAction(DISABLE_PLUGIN);
         filter.addDataScheme("package");
-        mContext.registerReceiver(this, filter, PluginInstanceManager.PLUGIN_PERMISSION, null);
+        mContext.registerReceiver(this, filter, PluginActionManager.PLUGIN_PERMISSION, null);
         filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
         mContext.registerReceiver(this, filter);
     }
 
     private void stopListening() {
-        // Never stop listening if a one-shot is present.
-        if (!mListening || mHasOneShot) return;
+        if (!mListening) return;
         mListening = false;
         mContext.unregisterReceiver(this);
     }
@@ -218,7 +152,7 @@
     public void onReceive(Context context, Intent intent) {
         if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
             synchronized (this) {
-                for (PluginInstanceManager manager : mPluginMap.values()) {
+                for (PluginActionManager<?> manager : mPluginMap.values()) {
                     manager.loadAll();
                 }
             }
@@ -226,46 +160,17 @@
             Uri uri = intent.getData();
             ComponentName component = ComponentName.unflattenFromString(
                     uri.toString().substring(10));
-            if (isPluginWhitelisted(component)) {
-                // Don't disable whitelisted plugins as they are a part of the OS.
+            if (isPluginPrivileged(component)) {
+                // Don't disable privileged plugins as they are a part of the OS.
                 return;
             }
-            getPluginEnabler().setDisabled(component, PluginEnabler.DISABLED_INVALID_VERSION);
+            mPluginEnabler.setDisabled(component, PluginEnabler.DISABLED_INVALID_VERSION);
             mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(),
                     SystemMessage.NOTE_PLUGIN);
         } else {
             Uri data = intent.getData();
             String pkg = data.getEncodedSchemeSpecificPart();
             ComponentName componentName = ComponentName.unflattenFromString(pkg);
-            if (mOneShotPackages.contains(pkg)) {
-                int icon = Resources.getSystem().getIdentifier(
-                        "stat_sys_warning", "drawable", "android");
-                int color = Resources.getSystem().getIdentifier(
-                        "system_notification_accent_color", "color", "android");
-                String label = pkg;
-                try {
-                    PackageManager pm = mContext.getPackageManager();
-                    label = pm.getApplicationInfo(pkg, 0).loadLabel(pm).toString();
-                } catch (NameNotFoundException e) {
-                }
-                // Localization not required as this will never ever appear in a user build.
-                final Notification.Builder nb =
-                        new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
-                                .setSmallIcon(icon)
-                                .setWhen(0)
-                                .setShowWhen(false)
-                                .setPriority(Notification.PRIORITY_MAX)
-                                .setVisibility(Notification.VISIBILITY_PUBLIC)
-                                .setColor(mContext.getColor(color))
-                                .setContentTitle("Plugin \"" + label + "\" has updated")
-                                .setContentText("Restart SysUI for changes to take effect.");
-                Intent i = new Intent("com.android.systemui.action.RESTART").setData(
-                            Uri.parse("package://" + pkg));
-                PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, PendingIntent.FLAG_MUTABLE_UNAUDITED);
-                nb.addAction(new Action.Builder(null, "Restart SysUI", pi).build());
-                mContext.getSystemService(NotificationManager.class)
-                        .notify(SystemMessage.NOTE_PLUGIN, nb.build());
-            }
             if (clearClassLoader(pkg)) {
                 if (Build.IS_ENG) {
                     Toast.makeText(mContext, "Reloading " + pkg, Toast.LENGTH_LONG).show();
@@ -276,22 +181,24 @@
             if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())
                     && componentName != null) {
                 @PluginEnabler.DisableReason int disableReason =
-                        getPluginEnabler().getDisableReason(componentName);
+                        mPluginEnabler.getDisableReason(componentName);
                 if (disableReason == PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH
                         || disableReason == PluginEnabler.DISABLED_FROM_SYSTEM_CRASH
                         || disableReason == PluginEnabler.DISABLED_INVALID_VERSION) {
                     Log.i(TAG, "Re-enabling previously disabled plugin that has been "
                             + "updated: " + componentName.flattenToShortString());
-                    getPluginEnabler().setEnabled(componentName);
+                    mPluginEnabler.setEnabled(componentName);
                 }
             }
             synchronized (this) {
-                if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
-                    for (PluginInstanceManager manager : mPluginMap.values()) {
-                        manager.onPackageChange(pkg);
+                if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())
+                        || Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())
+                        || Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
+                    for (PluginActionManager<?> actionManager : mPluginMap.values()) {
+                        actionManager.reloadPackage(pkg);
                     }
                 } else {
-                    for (PluginInstanceManager manager : mPluginMap.values()) {
+                    for (PluginActionManager<?> manager : mPluginMap.values()) {
                         manager.onPackageRemoved(pkg);
                     }
                 }
@@ -299,41 +206,10 @@
         }
     }
 
-    /** Returns class loader specific for the given plugin. */
-    public ClassLoader getClassLoader(ApplicationInfo appInfo) {
-        if (!mIsDebuggable && !isPluginPackageWhitelisted(appInfo.packageName)) {
-            Log.w(TAG, "Cannot get class loader for non-whitelisted plugin. Src:"
-                    + appInfo.sourceDir + ", pkg: " + appInfo.packageName);
-            return null;
-        }
-        if (mClassLoaders.containsKey(appInfo.packageName)) {
-            return mClassLoaders.get(appInfo.packageName);
-        }
-
-        List<String> zipPaths = new ArrayList<>();
-        List<String> libPaths = new ArrayList<>();
-        LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
-        ClassLoader classLoader = new PathClassLoader(
-                TextUtils.join(File.pathSeparator, zipPaths),
-                TextUtils.join(File.pathSeparator, libPaths),
-                getParentClassLoader());
-        mClassLoaders.put(appInfo.packageName, classLoader);
-        return classLoader;
-    }
-
     private boolean clearClassLoader(String pkg) {
         return mClassLoaders.remove(pkg) != null;
     }
 
-    ClassLoader getParentClassLoader() {
-        if (mParentClassLoader == null) {
-            // Lazily load this so it doesn't have any effect on devices without plugins.
-            mParentClassLoader = new ClassLoaderFilter(getClass().getClassLoader(),
-                    "com.android.systemui.plugin");
-        }
-        return mParentClassLoader;
-    }
-
     public <T> boolean dependsOn(Plugin p, Class<T> cls) {
         synchronized (this) {
             for (int i = 0; i < mPluginMap.size(); i++) {
@@ -345,46 +221,18 @@
         return false;
     }
 
-    public void handleWtfs() {
-        mPluginInitializer.handleWtfs();
-    }
-
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         synchronized (this) {
             pw.println(String.format("  plugin map (%d):", mPluginMap.size()));
-            for (PluginListener listener : mPluginMap.keySet()) {
+            for (PluginListener<?> listener : mPluginMap.keySet()) {
                 pw.println(String.format("    %s -> %s",
                         listener, mPluginMap.get(listener)));
             }
         }
     }
 
-    @VisibleForTesting
-    public static class PluginInstanceManagerFactory {
-        public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
-                String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,
-                Class<?> cls, PluginManagerImpl manager) {
-            return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
-                    new VersionInfo().addClass(cls), manager);
-        }
-    }
-
-    private boolean isPluginPackageWhitelisted(String packageName) {
-        for (String componentNameOrPackage : mWhitelistedPlugins) {
-            ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
-            if (componentName != null) {
-                if (componentName.getPackageName().equals(packageName)) {
-                    return true;
-                }
-            } else if (componentNameOrPackage.equals(packageName)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean isPluginWhitelisted(ComponentName pluginName) {
-        for (String componentNameOrPackage : mWhitelistedPlugins) {
+    private boolean isPluginPrivileged(ComponentName pluginName) {
+        for (String componentNameOrPackage : mPrivilegedPlugins) {
             ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
             if (componentName != null) {
                 if (componentName.equals(pluginName)) {
@@ -399,7 +247,7 @@
 
     // This allows plugins to include any libraries or copied code they want by only including
     // classes from the plugin library.
-    private static class ClassLoaderFilter extends ClassLoader {
+    static class ClassLoaderFilter extends ClassLoader {
         private final String mPackage;
         private final ClassLoader mBase;
 
@@ -417,16 +265,20 @@
     }
 
     private class PluginExceptionHandler implements UncaughtExceptionHandler {
-        private final UncaughtExceptionHandler mHandler;
+        private final Optional<UncaughtExceptionHandler> mExceptionHandlerOptional;
 
-        private PluginExceptionHandler(UncaughtExceptionHandler handler) {
-            mHandler = handler;
+        private PluginExceptionHandler(
+                Optional<UncaughtExceptionHandler> exceptionHandlerOptional) {
+            mExceptionHandlerOptional = exceptionHandlerOptional;
         }
 
         @Override
         public void uncaughtException(Thread thread, Throwable throwable) {
             if (SystemProperties.getBoolean("plugin.debugging", false)) {
-                mHandler.uncaughtException(thread, throwable);
+                Throwable finalThrowable = throwable;
+                mExceptionHandlerOptional.ifPresent(
+                        handler -> handler.uncaughtException(thread, finalThrowable));
+
                 return;
             }
             // Search for and disable plugins that may have been involved in this crash.
@@ -436,7 +288,7 @@
                 // disable all the plugins, so we can be sure that SysUI is running as
                 // best as possible.
                 synchronized (this) {
-                    for (PluginInstanceManager manager : mPluginMap.values()) {
+                    for (PluginActionManager<?> manager : mPluginMap.values()) {
                         disabledAny |= manager.disableAll();
                     }
                 }
@@ -446,7 +298,9 @@
             }
 
             // Run the normal exception handler so we can crash and cleanup our state.
-            mHandler.uncaughtException(thread, throwable);
+            Throwable finalThrowable = throwable;
+            mExceptionHandlerOptional.ifPresent(
+                    handler -> handler.uncaughtException(thread, finalThrowable));
         }
 
         private boolean checkStack(Throwable throwable) {
@@ -454,7 +308,7 @@
             boolean disabledAny = false;
             synchronized (this) {
                 for (StackTraceElement element : throwable.getStackTrace()) {
-                    for (PluginInstanceManager manager : mPluginMap.values()) {
+                    for (PluginActionManager<?> manager : mPluginMap.values()) {
                         disabledAny |= manager.checkAndDisable(element.getClassName());
                     }
                 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java
index bb845cd..6be3243 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java
@@ -119,6 +119,8 @@
 
     public static class InvalidVersionException extends RuntimeException {
         private final boolean mTooNew;
+        private int mExpected;
+        private int mActual;
 
         public InvalidVersionException(String str, boolean tooNew) {
             super(str);
@@ -128,11 +130,21 @@
         public InvalidVersionException(Class<?> cls, boolean tooNew, int expected, int actual) {
             super(cls.getSimpleName() + " expected version " + expected + " but had " + actual);
             mTooNew = tooNew;
+            mExpected = expected;
+            mActual = actual;
         }
 
         public boolean isTooNew() {
             return mTooNew;
         }
+
+        public int getExpectedVersion() {
+            return mExpected;
+        }
+
+        public int getActualVersion() {
+            return mActual;
+        }
     }
 
     private static class Version {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 277b2e3..8bd0f91 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -77,8 +77,22 @@
     void onSplitScreenSecondaryBoundsChanged(in Rect bounds, in Rect insets) = 17;
 
     /**
-     * Sent IME status changes
+     * Sent when suggested rotation button could be shown
      */
-    void onImeWindowStatusChanged(int displayId, IBinder token, int vis, int backDisposition,
-                         boolean showImeSwitcher) = 18;
+    void onRotationProposal(int rotation, boolean isValid) = 18;
+
+    /**
+     * Sent when disable flags change
+     */
+    void disable(int displayId, int state1, int state2, boolean animate) = 19;
+
+    /**
+     * Sent when behavior changes. See WindowInsetsController#@Behavior
+     */
+    void onSystemBarAttributesChanged(int displayId, int behavior) = 20;
+
+    /**
+     * Sent when screen turned on and ready to use (blocker scrim is hidden)
+     */
+    void onScreenTurnedOn() = 21;
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index f72245b..5b7e500 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -140,5 +140,15 @@
     /** Notifies that a swipe-up gesture has started */
     oneway void notifySwipeUpGestureStarted() = 46;
 
-    // Next id = 47
+    /** Notifies when taskbar status updated */
+    oneway void notifyTaskbarStatus(boolean visible, boolean stashed) = 47;
+
+    /**
+     * Notifies sysui when taskbar requests autoHide to stop auto-hiding
+     * If called to suspend, caller is also responsible for calling this method to un-suspend
+     * @param suspend should be true to stop auto-hide, false to resume normal behavior
+     */
+    oneway void notifyTaskbarAutohideSuspend(boolean suspend) = 48;
+
+    // Next id = 49
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
index 7dffc26..6154d84 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
@@ -16,13 +16,27 @@
 
 package com.android.systemui.shared.recents.utilities;
 
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
+import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
+
+import android.annotation.TargetApi;
+import android.content.Context;
 import android.graphics.Color;
+import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Message;
+import android.util.DisplayMetrics;
+import android.view.Surface;
+import android.view.WindowManager;
 
 /* Common code */
 public class Utilities {
 
+    private static final float TABLET_MIN_DPS = 600;
+
     /**
      * Posts a runnable on a handler at the front of the queue ignoring any sync barriers.
      */
@@ -31,6 +45,23 @@
         h.sendMessageAtFrontOfQueue(msg);
     }
 
+    public static boolean isRotationAnimationCCW(int from, int to) {
+        // All 180deg WM rotation animations are CCW, match that
+        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
+        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
+        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
+        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
+        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
+        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
+        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
+        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
+        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
+        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
+        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
+        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
+        return false; // Default
+    }
+
     /** Calculates the constrast between two colors, using the algorithm provided by the WCAG v2. */
     public static float computeContrastBetweenColors(int bg, int fg) {
         float bgR = Color.red(bg) / 255f;
@@ -58,4 +89,51 @@
     public static float clamp(float value, float min, float max) {
         return Math.max(min, Math.min(max, value));
     }
+
+    /**
+     * @return updated set of flags from InputMethodService based off {@param oldHints}
+     *          Leaves original hints unmodified
+     */
+    public static int calculateBackDispositionHints(int oldHints, int backDisposition,
+            boolean imeShown, boolean showImeSwitcher) {
+        int hints = oldHints;
+        switch (backDisposition) {
+            case InputMethodService.BACK_DISPOSITION_DEFAULT:
+            case InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS:
+            case InputMethodService.BACK_DISPOSITION_WILL_DISMISS:
+                if (imeShown) {
+                    hints |= NAVIGATION_HINT_BACK_ALT;
+                } else {
+                    hints &= ~NAVIGATION_HINT_BACK_ALT;
+                }
+                break;
+            case InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING:
+                hints &= ~NAVIGATION_HINT_BACK_ALT;
+                break;
+        }
+        if (showImeSwitcher) {
+            hints |= NAVIGATION_HINT_IME_SHOWN;
+        } else {
+            hints &= ~NAVIGATION_HINT_IME_SHOWN;
+        }
+
+        return hints;
+    }
+
+    /** @return whether or not {@param context} represents that of a large screen device or not */
+    @TargetApi(Build.VERSION_CODES.R)
+    public static boolean isTablet(Context context) {
+        final WindowManager windowManager = context.getSystemService(WindowManager.class);
+        final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
+
+        float originalSmallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
+                context.getResources().getConfiguration().densityDpi);
+        return dpiFromPx(Math.min(bounds.width(), bounds.height()), DENSITY_DEVICE_STABLE)
+                >= TABLET_MIN_DPS && originalSmallestWidth >= TABLET_MIN_DPS;
+    }
+
+    public static float dpiFromPx(float size, int densityDpi) {
+        float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+        return (size / densityRatio);
+    }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/ViewRippler.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/ViewRippler.java
new file mode 100644
index 0000000..5581a1c
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/ViewRippler.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.recents.utilities;
+
+import android.view.View;
+
+/**
+ * Shows view ripples by toggling the provided Views "pressed" state.
+ * Ripples 4 times.
+ */
+public class ViewRippler {
+    private static final int RIPPLE_OFFSET_MS = 50;
+    private static final int RIPPLE_INTERVAL_MS = 2000;
+    private View mRoot;
+
+    public void start(View root) {
+        stop(); // Stop any pending ripple animations
+
+        mRoot = root;
+
+        // Schedule pending ripples, offset the 1st to avoid problems with visibility change
+        mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
+        mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
+        mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS);
+        mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS);
+        mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS);
+    }
+
+    public void stop() {
+        if (mRoot != null) mRoot.removeCallbacks(mRipple);
+    }
+
+    private final Runnable mRipple = new Runnable() {
+        @Override
+        public void run() { // Cause the ripple to fire via false presses
+            if (!mRoot.isAttachedToWindow()) return;
+            mRoot.setPressed(true /* pressed */);
+            mRoot.setPressed(false /* pressed */);
+        }
+    };
+}
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index 44271687..b82d896 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -80,8 +80,7 @@
     public static void begin(View v, @CujType int cujType, long timeout) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return;
         Configuration.Builder builder =
-                new Configuration.Builder(cujType)
-                        .setView(v)
+                Configuration.Builder.withView(cujType, v)
                         .setTimeout(timeout);
         InteractionJankMonitor.getInstance().begin(builder);
     }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index c468e41..d447b48 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -112,6 +112,14 @@
     public static final int SYSUI_STATE_IME_SHOWING = 1 << 18;
     // The window magnification is overlapped with system gesture insets at the bottom.
     public static final int SYSUI_STATE_MAGNIFICATION_OVERLAP = 1 << 19;
+    // ImeSwitcher is showing
+    public static final int SYSUI_STATE_IME_SWITCHER_SHOWING = 1 << 20;
+    // Device dozing/AOD state
+    public static final int SYSUI_STATE_DEVICE_DOZING = 1 << 21;
+    // The home feature is disabled (either by SUW/SysUI/device policy)
+    public static final int SYSUI_STATE_BACK_DISABLED = 1 << 22;
+    // The bubble stack is expanded AND the mange menu for bubbles is expanded on top of it.
+    public static final int SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED = 1 << 23;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({SYSUI_STATE_SCREEN_PINNING,
@@ -133,7 +141,11 @@
             SYSUI_STATE_ONE_HANDED_ACTIVE,
             SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
             SYSUI_STATE_IME_SHOWING,
-            SYSUI_STATE_MAGNIFICATION_OVERLAP
+            SYSUI_STATE_MAGNIFICATION_OVERLAP,
+            SYSUI_STATE_IME_SWITCHER_SHOWING,
+            SYSUI_STATE_DEVICE_DOZING,
+            SYSUI_STATE_BACK_DISABLED,
+            SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED
     })
     public @interface SystemUiStateFlags {}
 
@@ -162,6 +174,11 @@
                 ? "allow_gesture" : "");
         str.add((flags & SYSUI_STATE_IME_SHOWING) != 0 ? "ime_visible" : "");
         str.add((flags & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0 ? "magnification_overlap" : "");
+        str.add((flags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0 ? "ime_switcher_showing" : "");
+        str.add((flags & SYSUI_STATE_DEVICE_DOZING) != 0 ? "device_dozing" : "");
+        str.add((flags & SYSUI_STATE_BACK_DISABLED) != 0 ? "back_disabled" : "");
+        str.add((flags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0
+                ? "bubbles_mange_menu_expanded" : "");
         return str.toString();
     }
 
@@ -273,8 +290,8 @@
      * These values are expressed in pixels because they should not respect display or font
      * scaling, this means that we don't have to reload them on config changes.
      */
-    public static float getWindowCornerRadius(Resources resources) {
-        return ScreenDecorationsUtils.getWindowCornerRadius(resources);
+    public static float getWindowCornerRadius(Context context) {
+        return ScreenDecorationsUtils.getWindowCornerRadius(context);
     }
 
     /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index ee55bf0..dcc4ea1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -37,9 +37,10 @@
 import android.view.SurfaceControl;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 
-import java.util.ArrayList;
+import com.android.wm.shell.util.CounterRotator;
 
 /**
  * @see RemoteAnimationAdapter
@@ -62,14 +63,16 @@
 
     /** Helper to just build a remote transition. Use this if the legacy adapter isn't needed. */
     public static RemoteTransitionCompat buildRemoteTransition(RemoteAnimationRunnerCompat runner) {
-        return new RemoteTransitionCompat(wrapRemoteTransition(runner));
+        return new RemoteTransitionCompat(
+                new RemoteTransition(wrapRemoteTransition(runner)));
     }
 
     public RemoteTransitionCompat getRemoteTransition() {
         return mRemoteTransition;
     }
 
-    private static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner(
+    /** Wraps a RemoteAnimationRunnerCompat in an IRemoteAnimationRunner. */
+    public static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner(
             final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
         return new IRemoteAnimationRunner.Stub() {
             @Override
@@ -106,52 +109,6 @@
         };
     }
 
-    private static class CounterRotator {
-        SurfaceControl mSurface = null;
-        ArrayList<SurfaceControl> mRotateChildren = null;
-
-        void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta,
-                float displayW, float displayH) {
-            if (rotateDelta == 0) return;
-            mRotateChildren = new ArrayList<>();
-            // We want to counter-rotate, so subtract from 4
-            rotateDelta = 4 - (rotateDelta + 4) % 4;
-            mSurface = new SurfaceControl.Builder()
-                    .setName("Transition Unrotate")
-                    .setContainerLayer()
-                    .setParent(parent)
-                    .build();
-            // column-major
-            if (rotateDelta == 1) {
-                t.setMatrix(mSurface, 0, 1, -1, 0);
-                t.setPosition(mSurface, displayW, 0);
-            } else if (rotateDelta == 2) {
-                t.setMatrix(mSurface, -1, 0, 0, -1);
-                t.setPosition(mSurface, displayW, displayH);
-            } else if (rotateDelta == 3) {
-                t.setMatrix(mSurface, 0, -1, 1, 0);
-                t.setPosition(mSurface, 0, displayH);
-            }
-            t.show(mSurface);
-        }
-
-        void addChild(SurfaceControl.Transaction t, SurfaceControl child) {
-            if (mSurface == null) return;
-            t.reparent(child, mSurface);
-            mRotateChildren.add(child);
-        }
-
-        void cleanUp(SurfaceControl rootLeash) {
-            if (mSurface == null) return;
-            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-            for (int i = mRotateChildren.size() - 1; i >= 0; --i) {
-                t.reparent(mRotateChildren.get(i), rootLeash);
-            }
-            t.remove(mSurface);
-            t.apply();
-        }
-    }
-
     private static IRemoteTransition.Stub wrapRemoteTransition(
             final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
         return new IRemoteTransition.Stub() {
@@ -201,14 +158,14 @@
                 if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) {
                     counterLauncher.setup(t, info.getChange(launcherTask.getParent()).getLeash(),
                             rotateDelta, displayW, displayH);
-                    if (counterLauncher.mSurface != null) {
-                        t.setLayer(counterLauncher.mSurface, launcherLayer);
+                    if (counterLauncher.getSurface() != null) {
+                        t.setLayer(counterLauncher.getSurface(), launcherLayer);
                     }
                 }
 
                 if (isReturnToHome) {
-                    if (counterLauncher.mSurface != null) {
-                        t.setLayer(counterLauncher.mSurface, info.getChanges().size() * 3);
+                    if (counterLauncher.getSurface() != null) {
+                        t.setLayer(counterLauncher.getSurface(), info.getChanges().size() * 3);
                     }
                     // Need to "boost" the closing things since that's what launcher expects.
                     for (int i = info.getChanges().size() - 1; i >= 0; --i) {
@@ -234,8 +191,8 @@
                     if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) {
                         counterWallpaper.setup(t, info.getChange(wallpaper.getParent()).getLeash(),
                                 rotateDelta, displayW, displayH);
-                        if (counterWallpaper.mSurface != null) {
-                            t.setLayer(counterWallpaper.mSurface, -1);
+                        if (counterWallpaper.getSurface() != null) {
+                            t.setLayer(counterWallpaper.getSurface(), -1);
                             counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash()));
                         }
                     }
@@ -260,7 +217,7 @@
                                 t.remove(leashMap.valueAt(i));
                             }
                             t.apply();
-                            finishCallback.onTransitionFinished(null /* wct */);
+                            finishCallback.onTransitionFinished(null /* wct */, null /* sct */);
                         } catch (RemoteException e) {
                             Log.e("ActivityOptionsCompat", "Failed to call app controlled animation"
                                     + " finished callback", e);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index 2407d21..30db136 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -68,11 +68,16 @@
     public final boolean isNotInRecents;
     public final Rect contentInsets;
     public final ActivityManager.RunningTaskInfo taskInfo;
+    public final boolean allowEnterPip;
     public final int rotationChange;
     public final int windowType;
+    public final WindowConfiguration windowConfiguration;
 
     private final SurfaceControl mStartLeash;
 
+    // Fields used only to unrap into RemoteAnimationTarget
+    private final Rect startBounds;
+
     public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
         taskId = app.taskId;
         mode = app.mode;
@@ -88,10 +93,13 @@
         contentInsets = app.contentInsets;
         activityType = app.windowConfiguration.getActivityType();
         taskInfo = app.taskInfo;
+        allowEnterPip = app.allowEnterPip;
         rotationChange = 0;
 
         mStartLeash = app.startLeash;
         windowType = app.windowType;
+        windowConfiguration = app.windowConfiguration;
+        startBounds = app.startBounds;
     }
 
     private static int newModeToLegacyMode(int newMode) {
@@ -107,6 +115,14 @@
         }
     }
 
+    public RemoteAnimationTarget unwrap() {
+        return new RemoteAnimationTarget(
+                taskId, mode, leash.getSurfaceControl(), isTranslucent, clipRect, contentInsets,
+                prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration,
+                isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType
+        );
+    }
+
 
     /**
      * Almost a copy of Transitions#setupStartState.
@@ -214,9 +230,15 @@
             activityType = ACTIVITY_TYPE_UNDEFINED;
         }
         taskInfo = change.getTaskInfo();
+        allowEnterPip = change.getAllowEnterPip();
         mStartLeash = null;
         rotationChange = change.getEndRotation() - change.getStartRotation();
         windowType = INVALID_WINDOW_TYPE;
+
+        windowConfiguration = change.getTaskInfo() != null
+            ? change.getTaskInfo().configuration.windowConfiguration
+            : new WindowConfiguration();
+        startBounds = change.getStartAbsBounds();
     }
 
     public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index 653d730..9ec95a3 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -17,14 +17,20 @@
 package com.android.systemui.shared.system;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.content.ComponentName;
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.os.Parcelable;
@@ -36,6 +42,7 @@
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.PictureInPictureSurfaceTransaction;
+import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 import android.window.TransitionInfo;
 import android.window.WindowContainerToken;
@@ -57,23 +64,23 @@
 public class RemoteTransitionCompat implements Parcelable {
     private static final String TAG = "RemoteTransitionCompat";
 
-    @NonNull final IRemoteTransition mTransition;
+    @NonNull final RemoteTransition mTransition;
     @Nullable TransitionFilter mFilter = null;
 
-    RemoteTransitionCompat(IRemoteTransition transition) {
+    RemoteTransitionCompat(RemoteTransition transition) {
         mTransition = transition;
     }
 
     public RemoteTransitionCompat(@NonNull RemoteTransitionRunner runner,
             @NonNull Executor executor) {
-        mTransition = new IRemoteTransition.Stub() {
+        IRemoteTransition remote = new IRemoteTransition.Stub() {
             @Override
             public void startAnimation(IBinder transition, TransitionInfo info,
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishedCallback) {
                 final Runnable finishAdapter = () ->  {
                     try {
-                        finishedCallback.onTransitionFinished(null /* wct */);
+                        finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to call transition finished callback", e);
                     }
@@ -87,7 +94,7 @@
                     IRemoteTransitionFinishedCallback finishedCallback) {
                 final Runnable finishAdapter = () ->  {
                     try {
-                        finishedCallback.onTransitionFinished(null /* wct */);
+                        finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to call transition finished callback", e);
                     }
@@ -96,12 +103,13 @@
                         finishAdapter));
             }
         };
+        mTransition = new RemoteTransition(remote);
     }
 
     /** Constructor specifically for recents animation */
     public RemoteTransitionCompat(RecentsAnimationListener recents,
             RecentsAnimationControllerCompat controller) {
-        mTransition = new IRemoteTransition.Stub() {
+        IRemoteTransition remote = new IRemoteTransition.Stub() {
             final RecentsControllerWrap mRecentsSession = new RecentsControllerWrap();
             IBinder mToken = null;
 
@@ -119,13 +127,20 @@
                 // This transition is for opening recents, so recents is on-top. We want to draw
                 // the current going-away task on top of recents, though, so move it to front
                 WindowContainerToken pausingTask = null;
+                WindowContainerToken pipTask = null;
                 for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                     final TransitionInfo.Change change = info.getChanges().get(i);
                     if (change.getMode() == TRANSIT_CLOSE || change.getMode() == TRANSIT_TO_BACK) {
                         t.setLayer(leashMap.get(change.getLeash()),
                                 info.getChanges().size() * 3 - i);
-                        if (change.getTaskInfo() != null) {
-                            pausingTask = change.getTaskInfo().token;
+                        final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+                        if (taskInfo == null) {
+                            continue;
+                        }
+                        pausingTask = taskInfo.token;
+                        if (taskInfo.pictureInPictureParams != null
+                                && taskInfo.pictureInPictureParams.isAutoEnterEnabled()) {
+                            pipTask = taskInfo.token;
                         }
                     }
                 }
@@ -134,8 +149,8 @@
                     t.setAlpha(wallpapers[i].leash.mSurfaceControl, 1);
                 }
                 t.apply();
-                mRecentsSession.setup(controller, info, finishedCallback, pausingTask,
-                        leashMap);
+                mRecentsSession.setup(controller, info, finishedCallback, pausingTask, pipTask,
+                        leashMap, mToken);
                 recents.onAnimationStart(mRecentsSession, apps, wallpapers, new Rect(0, 0, 0, 0),
                         new Rect());
             }
@@ -147,23 +162,31 @@
                 if (!mergeTarget.equals(mToken)) return;
                 if (!mRecentsSession.merge(info, t, recents)) return;
                 try {
-                    finishedCallback.onTransitionFinished(null /* wct */);
+                    finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Error merging transition.", e);
                 }
             }
         };
+        mTransition = new RemoteTransition(remote);
     }
 
     /** Adds a filter check that restricts this remote transition to home open transitions. */
-    public void addHomeOpenCheck() {
+    public void addHomeOpenCheck(ComponentName homeActivity) {
         if (mFilter == null) {
             mFilter = new TransitionFilter();
         }
+        // No need to handle the transition that also dismisses keyguard.
+        mFilter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
         mFilter.mRequirements =
-                new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
+                new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(),
+                        new TransitionFilter.Requirement()};
         mFilter.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
+        mFilter.mRequirements[0].mTopActivity = homeActivity;
         mFilter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+        mFilter.mRequirements[0].mOrder = CONTAINER_ORDER_TOP;
+        mFilter.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD;
+        mFilter.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
     }
 
     /**
@@ -175,13 +198,17 @@
         private RecentsAnimationControllerCompat mWrapped = null;
         private IRemoteTransitionFinishedCallback mFinishCB = null;
         private WindowContainerToken mPausingTask = null;
+        private WindowContainerToken mPipTask = null;
         private TransitionInfo mInfo = null;
         private SurfaceControl mOpeningLeash = null;
         private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null;
+        private PictureInPictureSurfaceTransaction mPipTransaction = null;
+        private IBinder mTransition = null;
 
         void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
                 IRemoteTransitionFinishedCallback finishCB, WindowContainerToken pausingTask,
-                ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+                WindowContainerToken pipTask, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+                IBinder transition) {
             if (mInfo != null) {
                 throw new IllegalStateException("Trying to run a new recents animation while"
                         + " recents is already active.");
@@ -190,7 +217,9 @@
             mInfo = info;
             mFinishCB = finishCB;
             mPausingTask = pausingTask;
+            mPipTask = pipTask;
             mLeashMap = leashMap;
+            mTransition = transition;
         }
 
         @SuppressLint("NewApi")
@@ -247,6 +276,7 @@
 
         @Override public void setFinishTaskTransaction(int taskId,
                 PictureInPictureSurfaceTransaction finishTransaction, SurfaceControl overlay) {
+            mPipTransaction = finishTransaction;
             if (mWrapped != null) {
                 mWrapped.setFinishTaskTransaction(taskId, finishTransaction, overlay);
             }
@@ -263,10 +293,13 @@
             try {
                 if (!toHome && mPausingTask != null && mOpeningLeash == null) {
                     // The gesture went back to opening the app rather than continuing with
-                    // recents, so end the transition by moving the app back to the top.
+                    // recents, so end the transition by moving the app back to the top (and also
+                    // re-showing it's task).
                     final WindowContainerTransaction wct = new WindowContainerTransaction();
                     wct.reorder(mPausingTask, true /* onTop */);
-                    mFinishCB.onTransitionFinished(wct);
+                    final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                    t.show(mInfo.getChange(mPausingTask).getLeash());
+                    mFinishCB.onTransitionFinished(wct, t);
                 } else {
                     if (mOpeningLeash != null) {
                         // TODO: the launcher animation should handle this
@@ -275,7 +308,18 @@
                         t.setAlpha(mOpeningLeash, 1.f);
                         t.apply();
                     }
-                    mFinishCB.onTransitionFinished(null /* wct */);
+                    if (mPipTask != null && mPipTransaction != null) {
+                        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                        t.show(mInfo.getChange(mPipTask).getLeash());
+                        PictureInPictureSurfaceTransaction.apply(mPipTransaction,
+                                mInfo.getChange(mPipTask).getLeash(), t);
+                        mPipTask = null;
+                        mPipTransaction = null;
+                        mFinishCB.onTransitionFinished(null /* wct */, t);
+                    } else {
+                        mFinishCB.onTransitionFinished(null /* wct */, null /* sct */);
+                    }
+
                 }
             } catch (RemoteException e) {
                 Log.e("RemoteTransitionCompat", "Failed to call animation finish callback", e);
@@ -298,6 +342,7 @@
             mInfo = null;
             mOpeningLeash = null;
             mLeashMap = null;
+            mTransition = null;
         }
 
         @Override public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
@@ -318,11 +363,28 @@
         @Override public boolean removeTask(int taskId) {
             return mWrapped != null ? mWrapped.removeTask(taskId) : false;
         }
+
+        /**
+         * @see IRecentsAnimationController#detachNavigationBarFromApp
+         */
+        @Override public void detachNavigationBarFromApp(boolean moveHomeToTop) {
+            try {
+                ActivityTaskManager.getService().detachNavigationBarFromApp(mTransition);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to detach the navigation bar from app", e);
+            }
+        }
+
+        /**
+         * @see IRecentsAnimationController#animateNavigationBarToApp(long)
+         */
+        @Override public void animateNavigationBarToApp(long duration) {
+        }
     }
 
 
 
-    // Code below generated by codegen v1.0.21.
+    // Code below generated by codegen v1.0.23.
     //
     // DO NOT MODIFY!
     // CHECKSTYLE:OFF Generated code
@@ -334,8 +396,21 @@
     //   Settings > Editor > Code Style > Formatter Control
     //@formatter:off
 
+
     @DataClass.Generated.Member
-    public @NonNull IRemoteTransition getTransition() {
+    /* package-private */ RemoteTransitionCompat(
+            @NonNull RemoteTransition transition,
+            @Nullable TransitionFilter filter) {
+        this.mTransition = transition;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mTransition);
+        this.mFilter = filter;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull RemoteTransition getTransition() {
         return mTransition;
     }
 
@@ -353,7 +428,7 @@
         byte flg = 0;
         if (mFilter != null) flg |= 0x2;
         dest.writeByte(flg);
-        dest.writeStrongInterface(mTransition);
+        dest.writeTypedObject(mTransition, flags);
         if (mFilter != null) dest.writeTypedObject(mFilter, flags);
     }
 
@@ -369,7 +444,7 @@
         // static FieldType unparcelFieldName(Parcel in) { ... }
 
         byte flg = in.readByte();
-        IRemoteTransition transition = IRemoteTransition.Stub.asInterface(in.readStrongBinder());
+        RemoteTransition transition = (RemoteTransition) in.readTypedObject(RemoteTransition.CREATOR);
         TransitionFilter filter = (flg & 0x2) == 0 ? null : (TransitionFilter) in.readTypedObject(TransitionFilter.CREATOR);
 
         this.mTransition = transition;
@@ -401,20 +476,20 @@
     @DataClass.Generated.Member
     public static class Builder {
 
-        private @NonNull IRemoteTransition mTransition;
+        private @NonNull RemoteTransition mTransition;
         private @Nullable TransitionFilter mFilter;
 
         private long mBuilderFieldsSet = 0L;
 
         public Builder(
-                @NonNull IRemoteTransition transition) {
+                @NonNull RemoteTransition transition) {
             mTransition = transition;
             com.android.internal.util.AnnotationValidations.validate(
                     NonNull.class, null, mTransition);
         }
 
         @DataClass.Generated.Member
-        public @NonNull Builder setTransition(@NonNull IRemoteTransition value) {
+        public @NonNull Builder setTransition(@NonNull RemoteTransition value) {
             checkNotUsed();
             mBuilderFieldsSet |= 0x1;
             mTransition = value;
@@ -437,8 +512,9 @@
             if ((mBuilderFieldsSet & 0x2) == 0) {
                 mFilter = null;
             }
-            RemoteTransitionCompat o = new RemoteTransitionCompat(mTransition);
-            o.mFilter = this.mFilter;
+            RemoteTransitionCompat o = new RemoteTransitionCompat(
+                    mTransition,
+                    mFilter);
             return o;
         }
 
@@ -451,10 +527,10 @@
     }
 
     @DataClass.Generated(
-            time = 1606862689344L,
-            codegenVersion = "1.0.21",
+            time = 1629321609807L,
+            codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java",
-            inputSignatures = "final @android.annotation.NonNull com.android.systemui.shared.system.IRemoteTransition mTransition\n @android.annotation.Nullable android.window.TransitionFilter mFilter\npublic  void addHomeOpenCheck()\nclass RemoteTransitionCompat extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass")
+            inputSignatures = "private static final  java.lang.String TAG\nfinal @android.annotation.NonNull android.window.RemoteTransition mTransition\n @android.annotation.Nullable android.window.TransitionFilter mFilter\npublic  void addHomeOpenCheck(android.content.ComponentName)\nclass RemoteTransitionCompat extends java.lang.Object implements [android.os.Parcelable]\nprivate  com.android.systemui.shared.system.RecentsAnimationControllerCompat mWrapped\nprivate  android.window.IRemoteTransitionFinishedCallback mFinishCB\nprivate  android.window.WindowContainerToken mPausingTask\nprivate  android.window.WindowContainerToken mPipTask\nprivate  android.window.TransitionInfo mInfo\nprivate  android.view.SurfaceControl mOpeningLeash\nprivate  android.util.ArrayMap<android.view.SurfaceControl,android.view.SurfaceControl> mLeashMap\nprivate  android.window.PictureInPictureSurfaceTransaction mPipTransaction\nprivate  android.os.IBinder mTransition\n  void setup(com.android.systemui.shared.system.RecentsAnimationControllerCompat,android.window.TransitionInfo,android.window.IRemoteTransitionFinishedCallback,android.window.WindowContainerToken,android.window.WindowContainerToken,android.util.ArrayMap<android.view.SurfaceControl,android.view.SurfaceControl>,android.os.IBinder)\n @android.annotation.SuppressLint boolean merge(android.window.TransitionInfo,android.view.SurfaceControl.Transaction,com.android.systemui.shared.system.RecentsAnimationListener)\npublic @java.lang.Override com.android.systemui.shared.recents.model.ThumbnailData screenshotTask(int)\npublic @java.lang.Override void setInputConsumerEnabled(boolean)\npublic @java.lang.Override void setAnimationTargetsBehindSystemBars(boolean)\npublic @java.lang.Override void hideCurrentInputMethod()\npublic @java.lang.Override void setFinishTaskTransaction(int,android.window.PictureInPictureSurfaceTransaction,android.view.SurfaceControl)\npublic @java.lang.Override @android.annotation.SuppressLint void finish(boolean,boolean)\npublic @java.lang.Override void setDeferCancelUntilNextTransition(boolean,boolean)\npublic @java.lang.Override void cleanupScreenshot()\npublic @java.lang.Override void setWillFinishToHome(boolean)\npublic @java.lang.Override boolean removeTask(int)\npublic @java.lang.Override void detachNavigationBarFromApp(boolean)\npublic @java.lang.Override void animateNavigationBarToApp(long)\nclass RecentsControllerWrap extends com.android.systemui.shared.system.RecentsAnimationControllerCompat implements []\n@com.android.internal.util.DataClass")
     @Deprecated
     private void __metadata() {}
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java
index b38270c..85d5de0 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java
@@ -27,10 +27,12 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.InsetsController;
 import android.view.InsetsState;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
+import android.view.animation.Interpolator;
 
 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
 import com.android.systemui.shared.recents.view.RecentsTransition;
@@ -89,6 +91,9 @@
     public static final int ITYPE_BOTTOM_TAPPABLE_ELEMENT =
             InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
 
+    public static final int ANIMATION_DURATION_RESIZE = InsetsController.ANIMATION_DURATION_RESIZE;
+    public static final Interpolator RESIZE_INTERPOLATOR = InsetsController.RESIZE_INTERPOLATOR;
+
     private static final WindowManagerWrapper sInstance = new WindowManagerWrapper();
 
     public static WindowManagerWrapper getInstance() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackController.java b/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java
similarity index 100%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackController.java
rename to packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
new file mode 100644
index 0000000..b6be6ed
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("UnfoldTransitionFactory")
+
+package com.android.systemui.unfold
+
+import android.content.Context
+import android.hardware.SensorManager
+import android.hardware.devicestate.DeviceStateManager
+import android.os.Handler
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
+import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
+import com.android.systemui.unfold.config.UnfoldTransitionConfig
+import com.android.systemui.unfold.progress.FixedTimingTransitionProgressProvider
+import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
+import com.android.systemui.unfold.updates.DeviceFoldStateProvider
+import com.android.systemui.unfold.updates.hinge.EmptyHingeAngleProvider
+import com.android.systemui.unfold.updates.hinge.HingeSensorAngleProvider
+import java.lang.IllegalStateException
+import java.util.concurrent.Executor
+
+fun createUnfoldTransitionProgressProvider(
+    context: Context,
+    config: UnfoldTransitionConfig,
+    screenStatusProvider: ScreenStatusProvider,
+    deviceStateManager: DeviceStateManager,
+    sensorManager: SensorManager,
+    mainHandler: Handler,
+    mainExecutor: Executor
+): UnfoldTransitionProgressProvider {
+
+    if (!config.isEnabled) {
+        throw IllegalStateException("Trying to create " +
+            "UnfoldTransitionProgressProvider when the transition is disabled")
+    }
+
+    val hingeAngleProvider =
+        if (config.isHingeAngleEnabled) {
+            HingeSensorAngleProvider(sensorManager)
+        } else {
+            EmptyHingeAngleProvider()
+        }
+
+    val foldStateProvider = DeviceFoldStateProvider(
+        context,
+        hingeAngleProvider,
+        screenStatusProvider,
+        deviceStateManager,
+        mainExecutor
+    )
+
+    return if (config.isHingeAngleEnabled) {
+        PhysicsBasedUnfoldTransitionProgressProvider(
+            mainHandler,
+            foldStateProvider
+        )
+    } else {
+        FixedTimingTransitionProgressProvider(foldStateProvider)
+    }
+}
+
+fun createConfig(context: Context): UnfoldTransitionConfig =
+    ResourceUnfoldTransitionConfig(context)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..e17f43e
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold
+
+import android.annotation.FloatRange
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.statusbar.policy.CallbackController
+
+/**
+ * Interface that allows to receive unfold transition progress updates.
+ * It can be used to update view properties based on the current animation progress.
+ * onTransitionProgress callback could be called on each frame.
+ *
+ * Use [createUnfoldTransitionProgressProvider] to create instances of this interface
+ */
+interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> {
+
+    fun destroy()
+
+    interface TransitionProgressListener {
+        fun onTransitionStarted() {}
+        fun onTransitionFinished() {}
+        fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt
new file mode 100644
index 0000000..3f027e3
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.config
+
+import android.content.Context
+import android.os.SystemProperties
+
+internal class ResourceUnfoldTransitionConfig(
+    private val context: Context
+) : UnfoldTransitionConfig {
+
+    override val isEnabled: Boolean
+        get() = readIsEnabledResource() && isPropertyEnabled
+
+    override val isHingeAngleEnabled: Boolean
+        get() = readIsHingeAngleEnabled()
+
+    private val isPropertyEnabled: Boolean
+        get() = SystemProperties.getInt(UNFOLD_TRANSITION_MODE_PROPERTY_NAME,
+            UNFOLD_TRANSITION_PROPERTY_ENABLED) == UNFOLD_TRANSITION_PROPERTY_ENABLED
+
+    private fun readIsEnabledResource(): Boolean = context.resources
+        .getBoolean(com.android.internal.R.bool.config_unfoldTransitionEnabled)
+
+    private fun readIsHingeAngleEnabled(): Boolean = context.resources
+        .getBoolean(com.android.internal.R.bool.config_unfoldTransitionHingeAngle)
+}
+
+/**
+ * Temporary persistent property to control unfold transition mode
+ * See [com.android.unfold.config.AnimationMode]
+ */
+private const val UNFOLD_TRANSITION_MODE_PROPERTY_NAME = "persist.unfold.transition_enabled"
+private const val UNFOLD_TRANSITION_PROPERTY_ENABLED = 1
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/packages/SystemUI/shared/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to packages/SystemUI/shared/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt
index b391bd9..5b187b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,14 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.systemui.unfold.config
 
-package com.android.systemui.statusbar.policy;
-
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
+interface UnfoldTransitionConfig {
+    val isEnabled: Boolean
+    val isHingeAngleEnabled: Boolean
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
new file mode 100644
index 0000000..732882e
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.progress
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.util.FloatProperty
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
+import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
+import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
+
+/**
+ * Emits animation progress with fixed timing after unfolding
+ */
+internal class FixedTimingTransitionProgressProvider(
+    private val foldStateProvider: FoldStateProvider
+) : UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener {
+
+    private val animatorListener = AnimatorListener()
+    private val animator =
+        ObjectAnimator.ofFloat(this, AnimationProgressProperty, 0f, 1f)
+            .apply {
+                duration = TRANSITION_TIME_MILLIS
+                addListener(animatorListener)
+            }
+
+
+    private var transitionProgress: Float = 0.0f
+        set(value) {
+            listeners.forEach { it.onTransitionProgress(value) }
+            field = value
+        }
+
+    private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+
+    init {
+        foldStateProvider.addCallback(this)
+        foldStateProvider.start()
+    }
+
+    override fun destroy() {
+        animator.cancel()
+        foldStateProvider.removeCallback(this)
+        foldStateProvider.stop()
+    }
+
+    override fun onFoldUpdate(@FoldUpdate update: Int) {
+        when (update) {
+            FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE ->
+                animator.start()
+            FOLD_UPDATE_FINISH_CLOSED ->
+                animator.cancel()
+        }
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners.remove(listener)
+    }
+
+    override fun onHingeAngleUpdate(angle: Float) {
+    }
+
+    private object AnimationProgressProperty :
+        FloatProperty<FixedTimingTransitionProgressProvider>("animation_progress") {
+
+        override fun setValue(
+            provider: FixedTimingTransitionProgressProvider,
+            value: Float
+        ) {
+            provider.transitionProgress = value
+        }
+
+        override fun get(provider: FixedTimingTransitionProgressProvider): Float =
+            provider.transitionProgress
+    }
+
+    private inner class AnimatorListener : Animator.AnimatorListener {
+
+        override fun onAnimationStart(animator: Animator) {
+            listeners.forEach { it.onTransitionStarted() }
+        }
+
+        override fun onAnimationEnd(animator: Animator) {
+            listeners.forEach { it.onTransitionFinished() }
+        }
+
+        override fun onAnimationRepeat(animator: Animator) {
+        }
+
+        override fun onAnimationCancel(animator: Animator) {
+        }
+    }
+
+    private companion object {
+        private const val TRANSITION_TIME_MILLIS = 400L
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..90f5998
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.progress
+
+import android.os.Handler
+import android.util.Log
+import android.util.MathUtils.saturate
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
+import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
+import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
+import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
+import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+
+/**
+ * Maps fold updates to unfold transition progress using DynamicAnimation.
+ *
+ * TODO(b/193793338) Current limitations:
+ *  - doesn't handle postures
+ */
+internal class PhysicsBasedUnfoldTransitionProgressProvider(
+    private val handler: Handler,
+    private val foldStateProvider: FoldStateProvider
+) :
+    UnfoldTransitionProgressProvider,
+    FoldUpdatesListener,
+    DynamicAnimation.OnAnimationEndListener {
+
+    private val springAnimation = SpringAnimation(this, AnimationProgressProperty)
+        .apply {
+            addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider)
+        }
+
+    private val timeoutRunnable = TimeoutRunnable()
+
+    private var isTransitionRunning = false
+    private var isAnimatedCancelRunning = false
+
+    private var transitionProgress: Float = 0.0f
+        set(value) {
+            if (isTransitionRunning) {
+                listeners.forEach { it.onTransitionProgress(value) }
+            }
+            field = value
+        }
+
+    private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+
+    init {
+        foldStateProvider.addCallback(this)
+        foldStateProvider.start()
+    }
+
+    override fun destroy() {
+        foldStateProvider.stop()
+    }
+
+    override fun onHingeAngleUpdate(angle: Float) {
+        if (!isTransitionRunning || isAnimatedCancelRunning) return
+        val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION)
+        springAnimation.animateToFinalPosition(progress)
+    }
+
+    override fun onFoldUpdate(@FoldUpdate update: Int) {
+        when (update) {
+            FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> {
+                startTransition(startValue = 0f)
+
+                // Stop the animation if the device has already opened by the time when
+                // the display is available as we won't receive the full open event anymore
+                if (foldStateProvider.isFullyOpened) {
+                    cancelTransition(endValue = 1f, animate = true)
+                }
+            }
+            FOLD_UPDATE_FINISH_FULL_OPEN -> {
+                // Do not cancel if we haven't started the transition yet.
+                // This could happen when we fully unfolded the device before the screen
+                // became available. In this case we start and immediately cancel the animation
+                // in FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE event handler, so we don't need to
+                // cancel it here.
+                if (isTransitionRunning) {
+                    cancelTransition(endValue = 1f, animate = true)
+                }
+            }
+            FOLD_UPDATE_FINISH_CLOSED -> {
+                cancelTransition(endValue = 0f, animate = false)
+            }
+            FOLD_UPDATE_START_CLOSING -> {
+                startTransition(startValue = 1f)
+            }
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "onFoldUpdate = $update")
+        }
+    }
+
+    private fun cancelTransition(endValue: Float, animate: Boolean) {
+        handler.removeCallbacks(timeoutRunnable)
+
+        if (isTransitionRunning && animate) {
+            isAnimatedCancelRunning = true
+            springAnimation.animateToFinalPosition(endValue)
+        } else {
+            transitionProgress = endValue
+            isAnimatedCancelRunning = false
+            isTransitionRunning = false
+            springAnimation.cancel()
+
+            listeners.forEach {
+                it.onTransitionFinished()
+            }
+
+            if (DEBUG) {
+                Log.d(TAG, "onTransitionFinished")
+            }
+        }
+    }
+
+    override fun onAnimationEnd(
+        animation: DynamicAnimation<out DynamicAnimation<*>>,
+        canceled: Boolean,
+        value: Float,
+        velocity: Float
+    ) {
+        if (isAnimatedCancelRunning) {
+            cancelTransition(value, animate = false)
+        }
+    }
+
+    private fun onStartTransition() {
+        listeners.forEach {
+            it.onTransitionStarted()
+        }
+        isTransitionRunning = true
+
+        if (DEBUG) {
+            Log.d(TAG, "onTransitionStarted")
+        }
+    }
+
+    private fun startTransition(startValue: Float) {
+        if (!isTransitionRunning) onStartTransition()
+
+        springAnimation.apply {
+            spring = SpringForce().apply {
+                finalPosition = startValue
+                dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
+                stiffness = SPRING_STIFFNESS
+            }
+            minimumVisibleChange = MINIMAL_VISIBLE_CHANGE
+            setStartValue(startValue)
+            setMinValue(0f)
+            setMaxValue(1f)
+        }
+
+        springAnimation.start()
+
+        handler.postDelayed(timeoutRunnable, TRANSITION_TIMEOUT_MILLIS)
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners.remove(listener)
+    }
+
+    private inner class TimeoutRunnable : Runnable {
+
+        override fun run() {
+            cancelTransition(endValue = 1f, animate = true)
+        }
+    }
+
+    private object AnimationProgressProperty :
+        FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") {
+
+        override fun setValue(
+            provider: PhysicsBasedUnfoldTransitionProgressProvider,
+            value: Float
+        ) {
+            provider.transitionProgress = value
+        }
+
+        override fun getValue(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float =
+            provider.transitionProgress
+    }
+}
+
+private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider"
+private const val DEBUG = true
+
+private const val TRANSITION_TIMEOUT_MILLIS = 2000L
+private const val SPRING_STIFFNESS = 200.0f
+private const val MINIMAL_VISIBLE_CHANGE = 0.001f
+private const val FINAL_HINGE_ANGLE_POSITION = 165f
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
new file mode 100644
index 0000000..35e2b30
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.updates
+
+import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
+import androidx.core.util.Consumer
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
+import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
+import java.util.concurrent.Executor
+
+class DeviceFoldStateProvider(
+    context: Context,
+    private val hingeAngleProvider: HingeAngleProvider,
+    private val screenStatusProvider: ScreenStatusProvider,
+    private val deviceStateManager: DeviceStateManager,
+    private val mainExecutor: Executor
+) : FoldStateProvider {
+
+    private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
+
+    @FoldUpdate
+    private var lastFoldUpdate: Int? = null
+
+    private val hingeAngleListener = HingeAngleListener()
+    private val screenListener = ScreenStatusListener()
+    private val foldStateListener = FoldStateListener(context)
+
+    private var isFolded = false
+    private var isUnfoldHandled = true
+
+    override fun start() {
+        deviceStateManager.registerCallback(
+            mainExecutor,
+            foldStateListener
+        )
+        screenStatusProvider.addCallback(screenListener)
+        hingeAngleProvider.addCallback(hingeAngleListener)
+    }
+
+    override fun stop() {
+        screenStatusProvider.removeCallback(screenListener)
+        deviceStateManager.unregisterCallback(foldStateListener)
+        hingeAngleProvider.removeCallback(hingeAngleListener)
+        hingeAngleProvider.stop()
+    }
+
+    override fun addCallback(listener: FoldUpdatesListener) {
+        outputListeners.add(listener)
+    }
+
+    override fun removeCallback(listener: FoldUpdatesListener) {
+        outputListeners.remove(listener)
+    }
+
+    override val isFullyOpened: Boolean
+        get() = !isFolded && lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN
+
+    private fun onHingeAngle(angle: Float) {
+        when (lastFoldUpdate) {
+            FOLD_UPDATE_FINISH_FULL_OPEN -> {
+                if (FULLY_OPEN_DEGREES - angle > START_CLOSING_THRESHOLD_DEGREES) {
+                    lastFoldUpdate = FOLD_UPDATE_START_CLOSING
+                    outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_CLOSING) }
+                }
+            }
+            FOLD_UPDATE_START_OPENING -> {
+                if (FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES) {
+                    lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN
+                    outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) }
+                }
+            }
+            FOLD_UPDATE_START_CLOSING -> {
+                if (FULLY_OPEN_DEGREES - angle < START_CLOSING_THRESHOLD_DEGREES) {
+                    lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN
+                    outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) }
+                }
+            }
+        }
+
+        outputListeners.forEach { it.onHingeAngleUpdate(angle) }
+    }
+
+    private inner class FoldStateListener(context: Context) :
+        DeviceStateManager.FoldStateListener(context, { folded: Boolean ->
+            isFolded = folded
+
+            if (folded) {
+                lastFoldUpdate = FOLD_UPDATE_FINISH_CLOSED
+                outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) }
+                hingeAngleProvider.stop()
+                isUnfoldHandled = false
+            } else {
+                lastFoldUpdate = FOLD_UPDATE_START_OPENING
+                outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_OPENING) }
+                hingeAngleProvider.start()
+            }
+        })
+
+    private inner class ScreenStatusListener :
+        ScreenStatusProvider.ScreenListener {
+
+        override fun onScreenTurnedOn() {
+            // Trigger this event only if we are unfolded and this is the first screen
+            // turned on event since unfold started. This prevents running the animation when
+            // turning on the internal display using the power button.
+            // Initially isUnfoldHandled is true so it will be reset to false *only* when we
+            // receive 'folded' event. If SystemUI started when device is already folded it will
+            // still receive 'folded' event on startup.
+            if (!isFolded && !isUnfoldHandled) {
+                outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) }
+                isUnfoldHandled = true
+            }
+        }
+    }
+
+    private inner class HingeAngleListener : Consumer<Float> {
+
+        override fun accept(angle: Float) {
+            onHingeAngle(angle)
+        }
+    }
+}
+
+private const val START_CLOSING_THRESHOLD_DEGREES = 95f
+private const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/FoldStateProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/FoldStateProvider.kt
new file mode 100644
index 0000000..643ece3
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/FoldStateProvider.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.updates
+
+import android.annotation.FloatRange
+import android.annotation.IntDef
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.systemui.statusbar.policy.CallbackController
+
+/**
+ * Allows to subscribe to main events related to fold/unfold process such as hinge angle update,
+ * start folding/unfolding, screen availability
+ */
+interface FoldStateProvider : CallbackController<FoldUpdatesListener> {
+    fun start()
+    fun stop()
+
+    val isFullyOpened: Boolean
+
+    interface FoldUpdatesListener {
+        fun onHingeAngleUpdate(@FloatRange(from = 0.0, to = 180.0) angle: Float)
+        fun onFoldUpdate(@FoldUpdate update: Int)
+    }
+
+    @IntDef(prefix = ["FOLD_UPDATE_"], value = [
+        FOLD_UPDATE_START_OPENING,
+        FOLD_UPDATE_HALF_OPEN,
+        FOLD_UPDATE_START_CLOSING,
+        FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE,
+        FOLD_UPDATE_FINISH_HALF_OPEN,
+        FOLD_UPDATE_FINISH_FULL_OPEN,
+        FOLD_UPDATE_FINISH_CLOSED
+    ])
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class FoldUpdate
+}
+
+const val FOLD_UPDATE_START_OPENING = 0
+const val FOLD_UPDATE_HALF_OPEN = 1
+const val FOLD_UPDATE_START_CLOSING = 2
+const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 3
+const val FOLD_UPDATE_FINISH_HALF_OPEN = 4
+const val FOLD_UPDATE_FINISH_FULL_OPEN = 5
+const val FOLD_UPDATE_FINISH_CLOSED = 6
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/EmptyHingeAngleProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/EmptyHingeAngleProvider.kt
new file mode 100644
index 0000000..9b58b1f
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/EmptyHingeAngleProvider.kt
@@ -0,0 +1,17 @@
+package com.android.systemui.unfold.updates.hinge
+
+import androidx.core.util.Consumer
+
+internal class EmptyHingeAngleProvider : HingeAngleProvider {
+    override fun start() {
+    }
+
+    override fun stop() {
+    }
+
+    override fun removeCallback(listener: Consumer<Float>) {
+    }
+
+    override fun addCallback(listener: Consumer<Float>) {
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/HingeAngleProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/HingeAngleProvider.kt
new file mode 100644
index 0000000..6f52456
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/HingeAngleProvider.kt
@@ -0,0 +1,18 @@
+package com.android.systemui.unfold.updates.hinge
+
+import androidx.core.util.Consumer
+import com.android.systemui.statusbar.policy.CallbackController
+
+/**
+ * Emits device hinge angle values (angle between two integral parts of the device).
+ * The hinge angle could be from 0 to 360 degrees inclusive.
+ * For foldable devices usually 0 corresponds to fully closed (folded) state and
+ * 180 degrees corresponds to fully open (flat) state
+ */
+interface HingeAngleProvider : CallbackController<Consumer<Float>> {
+    fun start()
+    fun stop()
+}
+
+const val FULLY_OPEN_DEGREES = 180f
+const val FULLY_CLOSED_DEGREES = 0f
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
new file mode 100644
index 0000000..a42ebef
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
@@ -0,0 +1,42 @@
+package com.android.systemui.unfold.updates.hinge
+
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import androidx.core.util.Consumer
+
+internal class HingeSensorAngleProvider(
+    private val sensorManager: SensorManager
+) : HingeAngleProvider {
+
+    private val sensorListener = HingeAngleSensorListener()
+    private val listeners: MutableList<Consumer<Float>> = arrayListOf()
+
+    override fun start() {
+        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
+        sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_FASTEST)
+    }
+
+    override fun stop() {
+        sensorManager.unregisterListener(sensorListener)
+    }
+
+    override fun removeCallback(listener: Consumer<Float>) {
+        listeners.remove(listener)
+    }
+
+    override fun addCallback(listener: Consumer<Float>) {
+        listeners.add(listener)
+    }
+
+    private inner class HingeAngleSensorListener : SensorEventListener {
+
+        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
+        }
+
+        override fun onSensorChanged(event: SensorEvent) {
+            listeners.forEach { it.accept(event.values[0]) }
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt
new file mode 100644
index 0000000..1eec803
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.updates.screen
+
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import com.android.systemui.statusbar.policy.CallbackController
+
+interface ScreenStatusProvider : CallbackController<ScreenListener> {
+
+    interface ScreenListener {
+        /**
+         * Called when the screen is on and ready (windows are drawn and screen blocker is removed)
+         */
+        fun onScreenTurnedOn()
+    }
+}
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java b/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
new file mode 100644
index 0000000..5b6845f
--- /dev/null
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.systemui.dagger.SysUISingleton;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+/**
+ * Concrete implementation of the a Flag manager that returns default values for debug builds
+ *
+ * Flags can be set (or unset) via the following adb command:
+ *
+ *   adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>]
+ *
+ * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command.
+ */
+@SysUISingleton
+public class FeatureFlagManager implements FlagReader, FlagWriter {
+    private static final String TAG = "SysUIFlags";
+
+    private static final String SYSPROP_PREFIX = "persist.systemui.flag_";
+    private static final String FIELD_TYPE = "type";
+    private static final String FIELD_ID = "id";
+    private static final String FIELD_VALUE = "value";
+    private static final String TYPE_BOOLEAN = "boolean";
+    private static final String ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG";
+    private static final String FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS";
+    private final SystemPropertiesHelper mSystemPropertiesHelper;
+
+    private final Map<Integer, Boolean> mBooleanFlagCache = new HashMap<>();
+
+    @Inject
+    public FeatureFlagManager(SystemPropertiesHelper systemPropertiesHelper, Context context) {
+        mSystemPropertiesHelper = systemPropertiesHelper;
+
+        IntentFilter filter = new IntentFilter(ACTION_SET_FLAG);
+        context.registerReceiver(mReceiver, filter, FLAGS_PERMISSION, null);
+    }
+
+    /** Return a {@link BooleanFlag}'s value. */
+    public boolean isEnabled(int id, boolean defaultValue) {
+        if (!mBooleanFlagCache.containsKey(id)) {
+            Boolean result = isEnabledInternal(id);
+            mBooleanFlagCache.put(id, result == null ? defaultValue : result);
+        }
+
+        return mBooleanFlagCache.get(id);
+    }
+
+    /** Returns the stored value or null if not set. */
+    private Boolean isEnabledInternal(int id) {
+        String data = mSystemPropertiesHelper.get(keyToSysPropKey(id));
+        if (data.isEmpty()) {
+            return null;
+        }
+        JSONObject json;
+        try {
+            json = new JSONObject(data);
+            if (!assertType(json, TYPE_BOOLEAN)) {
+                return null;
+            }
+
+            return json.getBoolean(FIELD_VALUE);
+        } catch (JSONException e) {
+            eraseInternal(id);  // Don't restart SystemUI in this case.
+        }
+        return null;
+    }
+
+    /** Set whether a given {@link BooleanFlag} is enabled or not. */
+    public void setEnabled(int id, boolean value) {
+        Boolean currentValue = isEnabledInternal(id);
+        if (currentValue != null && currentValue == value) {
+            return;
+        }
+
+        JSONObject json = new JSONObject();
+        try {
+            json.put(FIELD_TYPE, TYPE_BOOLEAN);
+            json.put(FIELD_VALUE, value);
+            mSystemPropertiesHelper.set(keyToSysPropKey(id), json.toString());
+            Log.i(TAG, "Set id " + id + " to " + value);
+            restartSystemUI();
+        } catch (JSONException e) {
+            // no-op
+        }
+    }
+
+    /** Erase a flag's overridden value if there is one. */
+    public void eraseFlag(int id) {
+        eraseInternal(id);
+        restartSystemUI();
+    }
+
+    /** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
+    private void eraseInternal(int id) {
+        // We can't actually "erase" things from sysprops, but we can set them to empty!
+        mSystemPropertiesHelper.set(keyToSysPropKey(id), "");
+        Log.i(TAG, "Erase id " + id);
+    }
+
+    public void addListener(Listener run) {}
+
+    public void removeListener(Listener run) {}
+
+    private void restartSystemUI() {
+        Log.i(TAG, "Restarting SystemUI");
+        // SysUI starts back when up exited. Is there a better way to do this?
+        System.exit(0);
+    }
+
+    private static String keyToSysPropKey(int key) {
+        return SYSPROP_PREFIX + key;
+    }
+
+    private static boolean assertType(JSONObject json, String type) {
+        try {
+            return json.getString(FIELD_TYPE).equals(TYPE_BOOLEAN);
+        } catch (JSONException e) {
+            return false;
+        }
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
+
+            if (ACTION_SET_FLAG.equals(action)) {
+                handleSetFlag(intent.getExtras());
+            }
+        }
+
+        private void handleSetFlag(Bundle extras) {
+            int id = extras.getInt(FIELD_ID);
+            if (id <= 0) {
+                Log.w(TAG, "ID not set or less than  or equal to 0: " + id);
+                return;
+            }
+
+            Map<Integer, Flag<?>> flagMap = Flags.collectFlags();
+            if (!flagMap.containsKey(id)) {
+                Log.w(TAG, "Tried to set unknown id: " + id);
+                return;
+            }
+            Flag<?> flag = flagMap.get(id);
+
+            if (!extras.containsKey(FIELD_VALUE)) {
+                eraseFlag(id);
+                return;
+            }
+
+            if (flag instanceof BooleanFlag) {
+                setEnabled(id, extras.getBoolean(FIELD_VALUE));
+            }
+        }
+    };
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index a5b2509..6fd0c82 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -11,9 +11,13 @@
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnPreDrawListener;
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.keyguard.dagger.KeyguardStatusViewScope;
 import com.android.systemui.R;
@@ -22,6 +26,8 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
 import java.util.TimeZone;
 
@@ -35,7 +41,14 @@
 
     private static final long CLOCK_OUT_MILLIS = 150;
     private static final long CLOCK_IN_MILLIS = 200;
-    private static final long SMARTSPACE_MOVE_MILLIS = 350;
+    private static final long STATUS_AREA_MOVE_MILLIS = 350;
+
+    @IntDef({LARGE, SMALL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ClockSize { }
+
+    public static final int LARGE = 0;
+    public static final int SMALL = 1;
 
     /**
      * Optional/alternative clock injected via plugin.
@@ -50,13 +63,7 @@
     private AnimatableClockView mClockView;
     private AnimatableClockView mLargeClockView;
 
-    /**
-     * Status area (date and other stuff) shown below the clock. Plugin can decide whether or not to
-     * show it below the alternate clock.
-     */
-    private View mKeyguardStatusArea;
-    /** Mutually exclusive with mKeyguardStatusArea */
-    private View mSmartspaceView;
+    private View mStatusArea;
     private int mSmartspaceTopOffset;
 
     /**
@@ -65,14 +72,14 @@
     private float mDarkAmount;
 
     /**
-     * Boolean value indicating if notifications are visible on lock screen. Use null to signify
-     * it is uninitialized.
+     * Indicates which clock is currently displayed - should be one of {@link ClockSize}.
+     * Use null to signify it is uninitialized.
      */
-    private Boolean mHasVisibleNotifications = null;
+    @ClockSize private Integer mDisplayedClockSize = null;
 
-    private AnimatorSet mClockInAnim = null;
-    private AnimatorSet mClockOutAnim = null;
-    private ObjectAnimator mSmartspaceAnim = null;
+    @VisibleForTesting AnimatorSet mClockInAnim = null;
+    @VisibleForTesting AnimatorSet mClockOutAnim = null;
+    private ObjectAnimator mStatusAreaAnim = null;
 
     /**
      * If the Keyguard Slice has a header (big center-aligned text.)
@@ -81,6 +88,7 @@
     private int[] mColorPalette;
 
     private int mClockSwitchYAmount;
+    @VisibleForTesting boolean mChildrenAreLaidOut = false;
 
     public KeyguardClockSwitch(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -117,7 +125,7 @@
         mClockView = findViewById(R.id.animatable_clock_view);
         mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
         mLargeClockView = findViewById(R.id.animatable_clock_view_large);
-        mKeyguardStatusArea = findViewById(R.id.keyguard_status_area);
+        mStatusArea = findViewById(R.id.keyguard_status_area);
 
         onDensityOrFontScaleChanged();
     }
@@ -186,22 +194,22 @@
     private void animateClockChange(boolean useLargeClock) {
         if (mClockInAnim != null) mClockInAnim.cancel();
         if (mClockOutAnim != null) mClockOutAnim.cancel();
-        if (mSmartspaceAnim != null) mSmartspaceAnim.cancel();
+        if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
 
         View in, out;
         int direction = 1;
-        float smartspaceYTranslation;
+        float statusAreaYTranslation;
         if (useLargeClock) {
             out = mClockFrame;
             in = mLargeClockFrame;
             if (indexOfChild(in) == -1) addView(in);
             direction = -1;
-            smartspaceYTranslation = mSmartspaceView == null ? 0
-                    : mClockFrame.getTop() - mSmartspaceView.getTop() + mSmartspaceTopOffset;
+            statusAreaYTranslation = mClockFrame.getTop() - mStatusArea.getTop()
+                    + mSmartspaceTopOffset;
         } else {
             in = mClockFrame;
             out = mLargeClockFrame;
-            smartspaceYTranslation = 0f;
+            statusAreaYTranslation = 0f;
 
             // Must remove in order for notifications to appear in the proper place
             removeView(out);
@@ -237,18 +245,16 @@
         mClockInAnim.start();
         mClockOutAnim.start();
 
-        if (mSmartspaceView != null) {
-            mSmartspaceAnim = ObjectAnimator.ofFloat(mSmartspaceView, View.TRANSLATION_Y,
-                    smartspaceYTranslation);
-            mSmartspaceAnim.setDuration(SMARTSPACE_MOVE_MILLIS);
-            mSmartspaceAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            mSmartspaceAnim.addListener(new AnimatorListenerAdapter() {
-                public void onAnimationEnd(Animator animation) {
-                    mSmartspaceAnim = null;
-                }
-            });
-            mSmartspaceAnim.start();
-        }
+        mStatusAreaAnim = ObjectAnimator.ofFloat(mStatusArea, View.TRANSLATION_Y,
+                statusAreaYTranslation);
+        mStatusAreaAnim.setDuration(STATUS_AREA_MOVE_MILLIS);
+        mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator animation) {
+                mStatusAreaAnim = null;
+            }
+        });
+        mStatusAreaAnim.start();
     }
 
     /**
@@ -264,19 +270,37 @@
     }
 
     /**
-     * Based upon whether notifications are showing or not, display/hide the large clock and
-     * the smaller version.
+     * Display the desired clock and hide the other one
+     *
+     * @return true if desired clock appeared and false if it was already visible
      */
-    boolean willSwitchToLargeClock(boolean hasVisibleNotifications) {
-        if (mHasVisibleNotifications != null
-                && hasVisibleNotifications == mHasVisibleNotifications) {
+    boolean switchToClock(@ClockSize int clockSize) {
+        if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) {
             return false;
         }
-        boolean useLargeClock = !hasVisibleNotifications;
-        animateClockChange(useLargeClock);
 
-        mHasVisibleNotifications = hasVisibleNotifications;
-        return useLargeClock;
+        // let's make sure clock is changed only after all views were laid out so we can
+        // translate them properly
+        if (mChildrenAreLaidOut) {
+            animateClockChange(clockSize == LARGE);
+            mDisplayedClockSize = clockSize;
+        } else {
+            getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+                @Override
+                public boolean onPreDraw() {
+                    switchToClock(clockSize);
+                    getViewTreeObserver().removeOnPreDrawListener(this);
+                    return true;
+                }
+            });
+        }
+        return true;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mChildrenAreLaidOut = true;
     }
 
     public Paint getPaint() {
@@ -320,10 +344,6 @@
         }
     }
 
-    void setSmartspaceView(View smartspaceView) {
-        mSmartspaceView = smartspaceView;
-    }
-
     void updateColors(ColorExtractor.GradientColors colors) {
         mSupportsDarkText = colors.supportsDarkText();
         mColorPalette = colors.getColorPalette();
@@ -337,8 +357,7 @@
         pw.println("  mClockPlugin: " + mClockPlugin);
         pw.println("  mClockFrame: " + mClockFrame);
         pw.println("  mLargeClockFrame: " + mLargeClockFrame);
-        pw.println("  mKeyguardStatusArea: " + mKeyguardStatusArea);
-        pw.println("  mSmartspaceView: " + mSmartspaceView);
+        pw.println("  mStatusArea: " + mStatusArea);
         pw.println("  mDarkAmount: " + mDarkAmount);
         pw.println("  mSupportsDarkText: " + mSupportsDarkText);
         pw.println("  mColorPalette: " + Arrays.toString(mColorPalette));
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 7a01b4e..01976b4 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -19,11 +19,15 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
+import static com.android.keyguard.KeyguardClockSwitch.LARGE;
+
 import android.app.WallpaperManager;
 import android.content.res.Resources;
 import android.text.TextUtils;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 import com.android.internal.colorextraction.ColorExtractor;
@@ -97,7 +101,8 @@
 
     private final ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin;
 
-    // If set, will replace keyguard_status_area
+    private ViewGroup mStatusArea;
+    // If set will replace keyguard_slice_view
     private View mSmartspaceView;
 
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
@@ -189,8 +194,8 @@
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
 
         if (mOnlyClock) {
-            View ksa = mView.findViewById(R.id.keyguard_status_area);
-            ksa.setVisibility(View.GONE);
+            View ksv = mView.findViewById(R.id.keyguard_slice_view);
+            ksv.setVisibility(View.GONE);
 
             View nic = mView.findViewById(
                     R.id.left_aligned_notification_icon_container);
@@ -199,19 +204,18 @@
         }
         updateAodIcons();
 
+        mStatusArea = mView.findViewById(R.id.keyguard_status_area);
+
         if (mSmartspaceController.isEnabled()) {
             mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
+            View ksv = mView.findViewById(R.id.keyguard_slice_view);
+            int ksvIndex = mStatusArea.indexOfChild(ksv);
+            ksv.setVisibility(View.GONE);
 
-            View ksa = mView.findViewById(R.id.keyguard_status_area);
-            int ksaIndex = mView.indexOfChild(ksa);
-            ksa.setVisibility(View.GONE);
-
-            // Place smartspace view below normal clock...
-            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                     MATCH_PARENT, WRAP_CONTENT);
-            lp.addRule(RelativeLayout.BELOW, R.id.lockscreen_clock_view);
 
-            mView.addView(mSmartspaceView, ksaIndex, lp);
+            mStatusArea.addView(mSmartspaceView, ksvIndex, lp);
             int startPadding = getContext().getResources()
                     .getDimensionPixelSize(R.dimen.below_clock_padding_start);
             int endPadding = getContext().getResources()
@@ -219,14 +223,6 @@
             mSmartspaceView.setPaddingRelative(startPadding, 0, endPadding, 0);
 
             updateClockLayout();
-
-            View nic = mView.findViewById(
-                    R.id.left_aligned_notification_icon_container);
-            lp = (RelativeLayout.LayoutParams) nic.getLayoutParams();
-            lp.addRule(RelativeLayout.BELOW, mSmartspaceView.getId());
-            nic.setLayoutParams(lp);
-
-            mView.setSmartspaceView(mSmartspaceView);
             mSmartspaceTransitionController.setLockscreenSmartspace(mSmartspaceView);
         }
     }
@@ -242,18 +238,6 @@
         }
         mColorExtractor.removeOnColorsChangedListener(mColorsListener);
         mView.setClockPlugin(null, mStatusBarStateController.getState());
-
-        mSmartspaceController.disconnect();
-
-        // TODO: This is an unfortunate necessity since smartspace plugin retains a single instance
-        // of the smartspace view -- if we don't remove the view, it can't be reused by a later
-        // instance of this class. In order to fix this, we need to modify the plugin so that
-        // (a) we get a new view each time and (b) we can properly clean up an old view by making
-        // it unregister itself as a plugin listener.
-        if (mSmartspaceView != null) {
-            mView.removeView(mSmartspaceView);
-            mSmartspaceView = null;
-        }
     }
 
     /**
@@ -281,10 +265,12 @@
     }
 
     /**
-     * Set whether or not the lock screen is showing notifications.
+     * Set which clock should be displayed on the keyguard. The other one will be automatically
+     * hidden.
      */
-    public void setHasVisibleNotifications(boolean hasVisibleNotifications) {
-        if (mView.willSwitchToLargeClock(hasVisibleNotifications)) {
+    public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize) {
+        boolean appeared = mView.switchToClock(clockSize);
+        if (appeared && clockSize == LARGE) {
             mLargeClockViewController.animateAppear();
         }
     }
@@ -334,8 +320,8 @@
         PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_Y,
                 scale, props, animate);
 
-        if (mSmartspaceView != null) {
-            PropertyAnimator.setProperty(mSmartspaceView, AnimatableProperty.TRANSLATION_X,
+        if (mStatusArea != null) {
+            PropertyAnimator.setProperty(mStatusArea, AnimatableProperty.TRANSLATION_X,
                     x, props, animate);
 
             // If we're unlocking with the SmartSpace shared element transition, let the controller
@@ -346,7 +332,6 @@
         }
 
         mKeyguardSliceViewController.updatePosition(x, props, animate);
-        mNotificationIconAreaController.updatePosition(x, props, animate);
     }
 
     /** Sets an alpha value on every child view except for the smartspace. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 97d3a5a..75425e1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -28,6 +28,7 @@
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
@@ -166,6 +167,7 @@
         private final TelephonyManager mTelephonyManager;
         private final EmergencyButtonController.Factory mEmergencyButtonControllerFactory;
         private final FalsingCollector mFalsingCollector;
+        private final DevicePostureController mDevicePostureController;
 
         @Inject
         public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -175,7 +177,8 @@
                 InputMethodManager inputMethodManager, @Main DelayableExecutor mainExecutor,
                 @Main Resources resources, LiftToActivateListener liftToActivateListener,
                 TelephonyManager telephonyManager, FalsingCollector falsingCollector,
-                EmergencyButtonController.Factory emergencyButtonControllerFactory) {
+                EmergencyButtonController.Factory emergencyButtonControllerFactory,
+                DevicePostureController devicePostureController) {
             mKeyguardUpdateMonitor = keyguardUpdateMonitor;
             mLockPatternUtils = lockPatternUtils;
             mLatencyTracker = latencyTracker;
@@ -187,6 +190,7 @@
             mTelephonyManager = telephonyManager;
             mEmergencyButtonControllerFactory = emergencyButtonControllerFactory;
             mFalsingCollector = falsingCollector;
+            mDevicePostureController = devicePostureController;
         }
 
         /** Create a new {@link KeyguardInputViewController}. */
@@ -200,7 +204,8 @@
                 return new KeyguardPatternViewController((KeyguardPatternView) keyguardInputView,
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
                         keyguardSecurityCallback, mLatencyTracker, mFalsingCollector,
-                        emergencyButtonController, mMessageAreaControllerFactory);
+                        emergencyButtonController, mMessageAreaControllerFactory,
+                        mDevicePostureController);
             } else if (keyguardInputView instanceof KeyguardPasswordView) {
                 return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView,
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
@@ -212,7 +217,8 @@
                 return new KeyguardPinViewController((KeyguardPINView) keyguardInputView,
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
                         keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
-                        mLiftToActivateListener, emergencyButtonController, mFalsingCollector);
+                        mLiftToActivateListener, emergencyButtonController, mFalsingCollector,
+                        mDevicePostureController);
             } else if (keyguardInputView instanceof KeyguardSimPinView) {
                 return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView,
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
index 62411db..11eeac2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
@@ -153,6 +153,10 @@
             colorState = mNextMessageColorState;
             mNextMessageColorState = ColorStateList.valueOf(DEFAULT_COLOR);
         }
+        if (mAltBouncerShowing) {
+            // alt bouncer has a black scrim, so always show the text in white
+            colorState = ColorStateList.valueOf(Color.WHITE);
+        }
         setTextColor(colorState);
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 8fc4240..1efda7e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -16,20 +16,23 @@
 
 package com.android.keyguard;
 
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
+
 import android.content.Context;
 import android.content.res.Configuration;
 import android.util.AttributeSet;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
-import android.widget.LinearLayout;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.settingslib.animation.AppearAnimationUtils;
 import com.android.settingslib.animation.DisappearAnimationUtils;
 import com.android.systemui.R;
-
-import java.util.List;
+import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt;
 
 /**
  * Displays a PIN pad for unlocking.
@@ -39,13 +42,10 @@
     private final AppearAnimationUtils mAppearAnimationUtils;
     private final DisappearAnimationUtils mDisappearAnimationUtils;
     private final DisappearAnimationUtils mDisappearAnimationUtilsLocked;
-    private ViewGroup mContainer;
-    private ViewGroup mRow0;
-    private ViewGroup mRow1;
-    private ViewGroup mRow2;
-    private ViewGroup mRow3;
+    private ConstraintLayout mContainer;
     private int mDisappearYTranslation;
     private View[][] mViews;
+    @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
 
     public KeyguardPINView(Context context) {
         this(context, null);
@@ -72,6 +72,11 @@
         updateMargins();
     }
 
+    void onDevicePostureChanged(@DevicePostureInt int posture) {
+        mLastDevicePosture = posture;
+        updateMargins();
+    }
+
     @Override
     protected void resetState() {
     }
@@ -82,30 +87,48 @@
     }
 
     private void updateMargins() {
+        // Re-apply everything to the keys...
         int bottomMargin = mContext.getResources().getDimensionPixelSize(
-                R.dimen.num_pad_row_margin_bottom);
-
-        for (ViewGroup vg : List.of(mRow1, mRow2, mRow3)) {
-            ((LinearLayout.LayoutParams) vg.getLayoutParams()).setMargins(0, 0, 0, bottomMargin);
-        }
-
-        bottomMargin = mContext.getResources().getDimensionPixelSize(
                 R.dimen.num_pad_entry_row_margin_bottom);
-        ((LinearLayout.LayoutParams) mRow0.getLayoutParams()).setMargins(0, 0, 0, bottomMargin);
+        int rightMargin = mContext.getResources().getDimensionPixelSize(
+                R.dimen.num_pad_key_margin_end);
+        String ratio = mContext.getResources().getString(R.string.num_pad_key_ratio);
 
-        if (mEcaView != null) {
-            int ecaTopMargin = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.keyguard_eca_top_margin);
-            int ecaBottomMargin = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.keyguard_eca_bottom_margin);
-            ((LinearLayout.LayoutParams) mEcaView.getLayoutParams()).setMargins(0, ecaTopMargin,
-                    0, ecaBottomMargin);
+        // mView contains all Views that make up the PIN pad; row0 = the entry test field, then
+        // rows 1-4 contain the buttons. Iterate over all views that make up the buttons in the pad,
+        // and re-set all the margins.
+        for (int row = 1; row < 5; row++) {
+            for (int column = 0; column < 3; column++) {
+                View key = mViews[row][column];
+
+                ConstraintLayout.LayoutParams lp =
+                        (ConstraintLayout.LayoutParams) key.getLayoutParams();
+
+                lp.dimensionRatio = ratio;
+
+                // Don't set any margins on the last row of buttons.
+                if (row != 4) {
+                    lp.bottomMargin = bottomMargin;
+                }
+
+                // Don't set margins on the rightmost buttons.
+                if (column != 2) {
+                    lp.rightMargin = rightMargin;
+                }
+
+                key.setLayoutParams(lp);
+            }
         }
 
-        View entryView = findViewById(R.id.pinEntry);
-        ViewGroup.LayoutParams lp = entryView.getLayoutParams();
-        lp.height = mContext.getResources().getDimensionPixelSize(R.dimen.keyguard_password_height);
-        entryView.setLayoutParams(lp);
+        // Update the guideline based on the device posture...
+        float halfOpenPercentage =
+                mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
+
+        ConstraintSet cs = new ConstraintSet();
+        cs.clone(mContainer);
+        cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
+                mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
+        cs.applyTo(mContainer);
     }
 
     @Override
@@ -113,13 +136,9 @@
         super.onFinishInflate();
 
         mContainer = findViewById(R.id.pin_container);
-        mRow0 = findViewById(R.id.row0);
-        mRow1 = findViewById(R.id.row1);
-        mRow2 = findViewById(R.id.row2);
-        mRow3 = findViewById(R.id.row3);
         mViews = new View[][]{
                 new View[]{
-                        mRow0, null, null
+                        findViewById(R.id.row0), null, null
                 },
                 new View[]{
                         findViewById(R.id.key1), findViewById(R.id.key2),
@@ -188,9 +207,6 @@
     private void enableClipping(boolean enable) {
         mContainer.setClipToPadding(enable);
         mContainer.setClipChildren(enable);
-        mRow1.setClipToPadding(enable);
-        mRow2.setClipToPadding(enable);
-        mRow3.setClipToPadding(enable);
         setClipChildren(enable);
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 92f8454..0529cdb 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -208,8 +208,7 @@
         mView.post(() -> {
             if (mView.isShown()) {
                 mPasswordEntry.requestFocus();
-                mInputMethodManager.showSoftInput(
-                        mPasswordEntry, InputMethodManager.SHOW_IMPLICIT);
+                mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime());
             }
         });
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 98e7fb4..1862fc7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -15,6 +15,8 @@
  */
 package com.android.keyguard;
 
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED;
+
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.SystemClock;
@@ -22,16 +24,19 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
+
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.widget.LockPatternView;
 import com.android.settingslib.animation.AppearAnimationCreator;
 import com.android.settingslib.animation.AppearAnimationUtils;
 import com.android.settingslib.animation.DisappearAnimationUtils;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt;
 
 public class KeyguardPatternView extends KeyguardInputView
         implements AppearAnimationCreator<LockPatternView.CellState> {
@@ -68,7 +73,7 @@
 
     KeyguardMessageArea mSecurityMessageDisplay;
     private View mEcaView;
-    private ViewGroup mContainer;
+    private ConstraintLayout mContainer;
 
     public KeyguardPatternView(Context context) {
         this(context, null);
@@ -90,6 +95,18 @@
                 mContext, android.R.interpolator.fast_out_linear_in));
     }
 
+    void onDevicePostureChanged(@DevicePostureInt int posture) {
+        // Update the guideline based on the device posture...
+        float halfOpenPercentage =
+                mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
+
+        ConstraintSet cs = new ConstraintSet();
+        cs.clone(mContainer);
+        cs.setGuidelinePercent(R.id.pattern_top_guideline, posture == DEVICE_POSTURE_HALF_OPENED
+                ? halfOpenPercentage : 0.0f);
+        cs.applyTo(mContainer);
+    }
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index d5be7ba..94e07b7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -38,6 +38,7 @@
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingClassifier;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 
 import java.util.List;
 
@@ -56,6 +57,9 @@
     private final FalsingCollector mFalsingCollector;
     private final EmergencyButtonController mEmergencyButtonController;
     private final KeyguardMessageAreaController.Factory mMessageAreaControllerFactory;
+    private final DevicePostureController mPostureController;
+    private final DevicePostureController.Callback mPostureCallback =
+            posture -> mView.onDevicePostureChanged(posture);
 
     private KeyguardMessageAreaController mMessageAreaController;
     private LockPatternView mLockPatternView;
@@ -192,7 +196,8 @@
             LatencyTracker latencyTracker,
             FalsingCollector falsingCollector,
             EmergencyButtonController emergencyButtonController,
-            KeyguardMessageAreaController.Factory messageAreaControllerFactory) {
+            KeyguardMessageAreaController.Factory messageAreaControllerFactory,
+            DevicePostureController postureController) {
         super(view, securityMode, keyguardSecurityCallback, emergencyButtonController);
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mLockPatternUtils = lockPatternUtils;
@@ -203,6 +208,7 @@
         KeyguardMessageArea kma = KeyguardMessageArea.findSecurityMessageDisplay(mView);
         mMessageAreaController = mMessageAreaControllerFactory.create(kma);
         mLockPatternView = mView.findViewById(R.id.lockPatternView);
+        mPostureController = postureController;
     }
 
     @Override
@@ -235,6 +241,7 @@
                 getKeyguardSecurityCallback().onCancelClicked();
             });
         }
+        mPostureController.addCallback(mPostureCallback);
     }
 
     @Override
@@ -247,6 +254,7 @@
         if (cancelBtn != null) {
             cancelBtn.setOnClickListener(null);
         }
+        mPostureController.removeCallback(mPostureCallback);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 262bed3..9f4585f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -23,10 +23,14 @@
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 
 public class KeyguardPinViewController
         extends KeyguardPinBasedInputViewController<KeyguardPINView> {
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final DevicePostureController mPostureController;
+    private final DevicePostureController.Callback mPostureCallback = posture ->
+            mView.onDevicePostureChanged(posture);
 
     protected KeyguardPinViewController(KeyguardPINView view,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -35,11 +39,13 @@
             KeyguardMessageAreaController.Factory messageAreaControllerFactory,
             LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
             EmergencyButtonController emergencyButtonController,
-            FalsingCollector falsingCollector) {
+            FalsingCollector falsingCollector,
+            DevicePostureController postureController) {
         super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
                 messageAreaControllerFactory, latencyTracker, liftToActivateListener,
                 emergencyButtonController, falsingCollector);
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mPostureController = postureController;
     }
 
     @Override
@@ -53,6 +59,14 @@
                 getKeyguardSecurityCallback().onCancelClicked();
             });
         }
+
+        mPostureController.addCallback(mPostureCallback);
+    }
+
+    @Override
+    protected void onViewDetached() {
+        super.onViewDetached();
+        mPostureController.removeCallback(mPostureCallback);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index ca4d73b..0328b5a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -23,6 +23,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
@@ -36,10 +37,11 @@
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.ViewPropertyAnimator;
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
@@ -55,7 +57,7 @@
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -85,6 +87,13 @@
 
     private static final long IME_DISAPPEAR_DURATION_MS = 125;
 
+    // The duration of the animation to switch bouncer sides.
+    private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500;
+
+    // How much of the switch sides animation should be dedicated to fading the bouncer out. The
+    // remainder will fade it back in again.
+    private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
+
     @VisibleForTesting
     KeyguardSecurityViewFlipper mSecurityViewFlipper;
     private AlertDialog mAlertDialog;
@@ -104,8 +113,7 @@
 
     private boolean mIsSecurityViewLeftAligned = true;
     private boolean mOneHandedMode = false;
-    private SecurityMode mSecurityMode = SecurityMode.Invalid;
-    private ViewPropertyAnimator mRunningOneHandedAnimator;
+    @Nullable private ValueAnimator mRunningOneHandedAnimator;
 
     private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback =
             new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
@@ -248,66 +256,47 @@
     }
 
     void onResume(SecurityMode securityMode, boolean faceAuthEnabled) {
-        mSecurityMode = securityMode;
         mSecurityViewFlipper.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback);
         updateBiometricRetry(securityMode, faceAuthEnabled);
-
-        updateLayoutForSecurityMode(securityMode);
     }
 
-    void updateLayoutForSecurityMode(SecurityMode securityMode) {
-        mSecurityMode = securityMode;
-        mOneHandedMode = canUseOneHandedBouncer();
-
-        if (mOneHandedMode) {
-            mIsSecurityViewLeftAligned = isOneHandedKeyguardLeftAligned(mContext);
-        }
-
+    /**
+     * Sets whether this security container is in one handed mode. If so, it will measure its
+     * child SecurityViewFlipper in one half of the screen, and move it when tapping on the opposite
+     * side of the screen.
+     */
+    public void setOneHandedMode(boolean oneHandedMode) {
+        mOneHandedMode = oneHandedMode;
         updateSecurityViewGravity();
         updateSecurityViewLocation(false);
     }
 
-    /** Update keyguard position based on a tapped X coordinate. */
-    public void updateKeyguardPosition(float x) {
-        if (mOneHandedMode) {
-            moveBouncerForXCoordinate(x, /* animate= */false);
-        }
+    /** Returns whether this security container is in one-handed mode. */
+    public boolean isOneHandedMode() {
+        return mOneHandedMode;
     }
 
-    /** Return whether the one-handed keyguard should be enabled. */
-    private boolean canUseOneHandedBouncer() {
-        // Is it enabled?
-        if (!getResources().getBoolean(
-                com.android.internal.R.bool.config_enableDynamicKeyguardPositioning)) {
-            return false;
-        }
-
-        if (!KeyguardSecurityModel.isSecurityViewOneHanded(mSecurityMode)) {
-            return false;
-        }
-
-        return getResources().getBoolean(R.bool.can_use_one_handed_bouncer);
+    /**
+     * When in one-handed mode, sets if the inner SecurityViewFlipper should be aligned to the
+     * left-hand side of the screen or not, and whether to animate when moving between the two.
+     */
+    public void setOneHandedModeLeftAligned(boolean leftAligned, boolean animate) {
+        mIsSecurityViewLeftAligned = leftAligned;
+        updateSecurityViewLocation(animate);
     }
 
-    /** Read whether the one-handed keyguard should be on the left/right from settings. */
-    private boolean isOneHandedKeyguardLeftAligned(Context context) {
-        try {
-            return Settings.Global.getInt(context.getContentResolver(),
-                    Settings.Global.ONE_HANDED_KEYGUARD_SIDE)
-                    == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
-        } catch (Settings.SettingNotFoundException ex) {
-            return true;
-        }
+    /** Returns whether the inner SecurityViewFlipper is left-aligned when in one-handed mode. */
+    public boolean isOneHandedModeLeftAligned() {
+        return mIsSecurityViewLeftAligned;
     }
 
     private void updateSecurityViewGravity() {
-        View securityView = findKeyguardSecurityView();
-
-        if (securityView == null) {
+        if (mSecurityViewFlipper == null) {
             return;
         }
 
-        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) securityView.getLayoutParams();
+        FrameLayout.LayoutParams lp =
+                (FrameLayout.LayoutParams) mSecurityViewFlipper.getLayoutParams();
 
         if (mOneHandedMode) {
             lp.gravity = Gravity.LEFT | Gravity.BOTTOM;
@@ -315,7 +304,7 @@
             lp.gravity = Gravity.CENTER_HORIZONTAL;
         }
 
-        securityView.setLayoutParams(lp);
+        mSecurityViewFlipper.setLayoutParams(lp);
     }
 
     /**
@@ -324,14 +313,12 @@
      * by the security view .
      */
     private void updateSecurityViewLocation(boolean animate) {
-        View securityView = findKeyguardSecurityView();
-
-        if (securityView == null) {
+        if (mSecurityViewFlipper == null) {
             return;
         }
 
         if (!mOneHandedMode) {
-            securityView.setTranslationX(0);
+            mSecurityViewFlipper.setTranslationX(0);
             return;
         }
 
@@ -340,42 +327,107 @@
             mRunningOneHandedAnimator = null;
         }
 
-        int targetTranslation = mIsSecurityViewLeftAligned ? 0 : (int) (getMeasuredWidth() / 2f);
+        int targetTranslation = mIsSecurityViewLeftAligned
+                ? 0 : (int) (getMeasuredWidth() - mSecurityViewFlipper.getWidth());
 
         if (animate) {
-            mRunningOneHandedAnimator = securityView.animate().translationX(targetTranslation);
-            mRunningOneHandedAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            mRunningOneHandedAnimator.setListener(new AnimatorListenerAdapter() {
+            // This animation is a bit fun to implement. The bouncer needs to move, and fade in/out
+            // at the same time. The issue is, the bouncer should only move a short amount (120dp or
+            // so), but obviously needs to go from one side of the screen to the other. This needs a
+            // pretty custom animation.
+            //
+            // This works as follows. It uses a ValueAnimation to simply drive the animation
+            // progress. This animator is responsible for both the translation of the bouncer, and
+            // the current fade. It will fade the bouncer out while also moving it along the 120dp
+            // path. Once the bouncer is fully faded out though, it will "snap" the bouncer closer
+            // to its destination, then fade it back in again. The effect is that the bouncer will
+            // move from 0 -> X while fading out, then (destination - X) -> destination while fading
+            // back in again.
+            // TODO(b/195012405): Make this animation properly abortable.
+            Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
+                    mContext, android.R.interpolator.fast_out_extra_slow_in);
+            Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
+            Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+
+            mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+            mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
+            mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR);
+
+            int initialTranslation = (int) mSecurityViewFlipper.getTranslationX();
+            int totalTranslation = (int) getResources().getDimension(
+                    R.dimen.one_handed_bouncer_move_animation_translation);
+
+            final boolean shouldRestoreLayerType = mSecurityViewFlipper.hasOverlappingRendering()
+                    && mSecurityViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
+            if (shouldRestoreLayerType) {
+                mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
+            }
+
+            float initialAlpha = mSecurityViewFlipper.getAlpha();
+
+            mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
                     mRunningOneHandedAnimator = null;
                 }
             });
+            mRunningOneHandedAnimator.addUpdateListener(animation -> {
+                float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION;
+                boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
 
-            mRunningOneHandedAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+                int currentTranslation = (int) (positionInterpolator.getInterpolation(
+                        animation.getAnimatedFraction()) * totalTranslation);
+                int translationRemaining = totalTranslation - currentTranslation;
+
+                // Flip the sign if we're going from right to left.
+                if (mIsSecurityViewLeftAligned) {
+                    currentTranslation = -currentTranslation;
+                    translationRemaining = -translationRemaining;
+                }
+
+                if (isFadingOut) {
+                    // The bouncer fades out over the first X%.
+                    float fadeOutFraction = MathUtils.constrainedMap(
+                            /* rangeMin= */1.0f,
+                            /* rangeMax= */0.0f,
+                            /* valueMin= */0.0f,
+                            /* valueMax= */switchPoint,
+                            animation.getAnimatedFraction());
+                    float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
+
+                    // When fading out, the alpha needs to start from the initial opacity of the
+                    // view flipper, otherwise we get a weird bit of jank as it ramps back to 100%.
+                    mSecurityViewFlipper.setAlpha(opacity * initialAlpha);
+
+                    // Animate away from the source.
+                    mSecurityViewFlipper.setTranslationX(initialTranslation + currentTranslation);
+                } else {
+                    // And in again over the remaining (100-X)%.
+                    float fadeInFraction = MathUtils.constrainedMap(
+                            /* rangeMin= */0.0f,
+                            /* rangeMax= */1.0f,
+                            /* valueMin= */switchPoint,
+                            /* valueMax= */1.0f,
+                            animation.getAnimatedFraction());
+
+                    float opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
+                    mSecurityViewFlipper.setAlpha(opacity);
+
+                    // Fading back in, animate towards the destination.
+                    mSecurityViewFlipper.setTranslationX(targetTranslation - translationRemaining);
+                }
+
+                if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
+                    mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
+                }
+            });
+
             mRunningOneHandedAnimator.start();
         } else {
-            securityView.setTranslationX(targetTranslation);
+            mSecurityViewFlipper.setTranslationX(targetTranslation);
         }
     }
 
-    @Nullable
-    private KeyguardSecurityViewFlipper findKeyguardSecurityView() {
-        for (int i = 0; i < getChildCount(); i++) {
-            View child = getChildAt(i);
-
-            if (isKeyguardSecurityView(child)) {
-                return (KeyguardSecurityViewFlipper) child;
-            }
-        }
-
-        return null;
-    }
-
-    private boolean isKeyguardSecurityView(View view) {
-        return view instanceof KeyguardSecurityViewFlipper;
-    }
-
     public void onPause() {
         if (mAlertDialog != null) {
             mAlertDialog.dismiss();
@@ -510,6 +562,11 @@
                     mIsSecurityViewLeftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
                             : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT);
 
+            int keyguardState = mIsSecurityViewLeftAligned
+                    ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT
+                    : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT;
+            SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState);
+
             updateSecurityViewLocation(animate);
         }
     }
@@ -635,7 +692,7 @@
         for (int i = 0; i < getChildCount(); i++) {
             final View view = getChildAt(i);
             if (view.getVisibility() != GONE) {
-                if (mOneHandedMode && isKeyguardSecurityView(view)) {
+                if (mOneHandedMode && view == mSecurityViewFlipper) {
                     measureChildWithMargins(view, halfWidthMeasureSpec, 0,
                             heightMeasureSpec, 0);
                 } else {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index dd7c7ea..d4d3d5b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -32,6 +32,7 @@
 import android.content.res.Configuration;
 import android.metrics.LogMaker;
 import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.Log;
 import android.util.Slog;
 import android.view.MotionEvent;
@@ -49,6 +50,8 @@
 import com.android.keyguard.dagger.KeyguardBouncerScope;
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.Gefingerpoken;
+import com.android.systemui.R;
+import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -74,12 +77,14 @@
     private final KeyguardSecurityViewFlipperController mSecurityViewFlipperController;
     private final SecurityCallback mSecurityCallback;
     private final ConfigurationController mConfigurationController;
+    private final FalsingCollector mFalsingCollector;
 
     private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
 
     private SecurityMode mCurrentSecurityMode = SecurityMode.Invalid;
 
-    private final Gefingerpoken mGlobalTouchListener = new Gefingerpoken() {
+    @VisibleForTesting
+    final Gefingerpoken mGlobalTouchListener = new Gefingerpoken() {
         private MotionEvent mTouchDown;
         @Override
         public boolean onInterceptTouchEvent(MotionEvent ev) {
@@ -91,6 +96,17 @@
             // Do just a bit of our own falsing. People should only be tapping on the input, not
             // swiping.
             if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                // If we're in one handed mode, the user can tap on the opposite side of the screen
+                // to move the bouncer across. In that case, inhibit the falsing (otherwise the taps
+                // to move the bouncer to each screen side can end up closing it instead).
+                if (mView.isOneHandedMode()) {
+                    if ((mView.isOneHandedModeLeftAligned() && ev.getX() > mView.getWidth() / 2f)
+                            || (!mView.isOneHandedModeLeftAligned()
+                            && ev.getX() <= mView.getWidth() / 2f)) {
+                        mFalsingCollector.avoidGesture();
+                    }
+                }
+
                 if (mTouchDown != null) {
                     mTouchDown.recycle();
                     mTouchDown = null;
@@ -135,9 +151,17 @@
         }
 
         public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) {
+            int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT;
+            if (canUseOneHandedBouncer()) {
+                bouncerSide = isOneHandedKeyguardLeftAligned()
+                        ? SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__LEFT
+                        : SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__RIGHT;
+            }
+
             if (success) {
                 SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED,
-                        SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__SUCCESS);
+                        SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__SUCCESS,
+                        bouncerSide);
                 mLockPatternUtils.reportSuccessfulPasswordAttempt(userId);
                 // Force a garbage collection in an attempt to erase any lockscreen password left in
                 // memory. Do it asynchronously with a 5-sec delay to avoid making the keyguard
@@ -152,7 +176,8 @@
                 });
             } else {
                 SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED,
-                        SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__FAILURE);
+                        SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__FAILURE,
+                        bouncerSide);
                 reportFailedUnlockAttempt(userId, timeoutMs);
             }
             mMetricsLogger.write(new LogMaker(MetricsEvent.BOUNCER)
@@ -184,7 +209,7 @@
     private ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
                 @Override
-                public void onOverlayChanged() {
+                public void onThemeChanged() {
                     mSecurityViewFlipperController.reloadColors();
                 }
 
@@ -204,7 +229,8 @@
             KeyguardStateController keyguardStateController,
             SecurityCallback securityCallback,
             KeyguardSecurityViewFlipperController securityViewFlipperController,
-            ConfigurationController configurationController) {
+            ConfigurationController configurationController,
+            FalsingCollector falsingCollector) {
         super(view);
         mLockPatternUtils = lockPatternUtils;
         mUpdateMonitor = keyguardUpdateMonitor;
@@ -218,6 +244,7 @@
                 mKeyguardSecurityCallback);
         mConfigurationController = configurationController;
         mLastOrientation = getResources().getConfiguration().orientation;
+        mFalsingCollector = falsingCollector;
     }
 
     @Override
@@ -296,6 +323,14 @@
     @Override
     public void onResume(int reason) {
         if (mCurrentSecurityMode != SecurityMode.None) {
+            int state = SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN;
+            if (canUseOneHandedBouncer()) {
+                state = mView.isOneHandedModeLeftAligned()
+                        ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_LEFT
+                        : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_RIGHT;
+            }
+            SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, state);
+
             getCurrentSecurityController().onResume(reason);
         }
         mView.onResume(
@@ -442,13 +477,49 @@
         if (newView != null) {
             newView.onResume(KeyguardSecurityView.VIEW_REVEALED);
             mSecurityViewFlipperController.show(newView);
-            mView.updateLayoutForSecurityMode(securityMode);
+            configureOneHandedMode();
         }
 
         mSecurityCallback.onSecurityModeChanged(
                 securityMode, newView != null && newView.needsInput());
     }
 
+    /** Read whether the one-handed keyguard should be on the left/right from settings. */
+    private boolean isOneHandedKeyguardLeftAligned() {
+        try {
+            return Settings.Global.getInt(mView.getContext().getContentResolver(),
+                    Settings.Global.ONE_HANDED_KEYGUARD_SIDE)
+                    == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
+        } catch (Settings.SettingNotFoundException ex) {
+            return true;
+        }
+    }
+
+    private boolean canUseOneHandedBouncer() {
+        // Is it enabled?
+        if (!getResources().getBoolean(
+                com.android.internal.R.bool.config_enableDynamicKeyguardPositioning)) {
+            return false;
+        }
+
+        if (!KeyguardSecurityModel.isSecurityViewOneHanded(mCurrentSecurityMode)) {
+            return false;
+        }
+
+        return getResources().getBoolean(R.bool.can_use_one_handed_bouncer);
+    }
+
+    private void configureOneHandedMode() {
+        boolean oneHandedMode = canUseOneHandedBouncer();
+
+        mView.setOneHandedMode(oneHandedMode);
+
+        if (oneHandedMode) {
+            mView.setOneHandedModeLeftAligned(
+                    isOneHandedKeyguardLeftAligned(), /* animate= */false);
+        }
+    }
+
     public void reportFailedUnlockAttempt(int userId, int timeoutMs) {
         // +1 for this time
         final int failedAttempts = mLockPatternUtils.getCurrentFailedPasswordAttempts(userId) + 1;
@@ -513,13 +584,15 @@
         int newOrientation = getResources().getConfiguration().orientation;
         if (newOrientation != mLastOrientation) {
             mLastOrientation = newOrientation;
-            mView.updateLayoutForSecurityMode(mCurrentSecurityMode);
+            configureOneHandedMode();
         }
     }
 
     /** Update keyguard position based on a tapped X coordinate. */
     public void updateKeyguardPosition(float x) {
-        mView.updateKeyguardPosition(x);
+        if (mView.isOneHandedMode()) {
+            mView.setOneHandedModeLeftAligned(x <= mView.getWidth() / 2f, false);
+        }
     }
 
     static class Factory {
@@ -535,6 +608,7 @@
         private final KeyguardStateController mKeyguardStateController;
         private final KeyguardSecurityViewFlipperController mSecurityViewFlipperController;
         private final ConfigurationController mConfigurationController;
+        private final FalsingCollector mFalsingCollector;
 
         @Inject
         Factory(KeyguardSecurityContainer view,
@@ -547,7 +621,8 @@
                 UiEventLogger uiEventLogger,
                 KeyguardStateController keyguardStateController,
                 KeyguardSecurityViewFlipperController securityViewFlipperController,
-                ConfigurationController configurationController) {
+                ConfigurationController configurationController,
+                FalsingCollector falsingCollector) {
             mView = view;
             mAdminSecondaryLockScreenControllerFactory = adminSecondaryLockScreenControllerFactory;
             mLockPatternUtils = lockPatternUtils;
@@ -558,6 +633,7 @@
             mKeyguardStateController = keyguardStateController;
             mSecurityViewFlipperController = securityViewFlipperController;
             mConfigurationController = configurationController;
+            mFalsingCollector = falsingCollector;
         }
 
         public KeyguardSecurityContainerController create(
@@ -566,7 +642,7 @@
                     mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils,
                     mKeyguardUpdateMonitor, mKeyguardSecurityModel, mMetricsLogger, mUiEventLogger,
                     mKeyguardStateController, securityCallback, mSecurityViewFlipperController,
-                    mConfigurationController);
+                    mConfigurationController, mFalsingCollector);
         }
 
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
index 428006e..9b76bab 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
@@ -33,12 +33,10 @@
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
 import android.util.AttributeSet;
-import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.View;
 import android.view.animation.Animation;
 import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import androidx.slice.SliceItem;
@@ -85,8 +83,6 @@
     private boolean mHasHeader;
     private View.OnClickListener mOnClickListener;
 
-    private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
-
     public KeyguardSliceView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
@@ -136,35 +132,6 @@
         }
     }
 
-    /**
-     * Updates the lockscreen mode which may change the layout of the keyguard slice view.
-     */
-    public void updateLockScreenMode(int mode) {
-        mLockScreenMode = mode;
-        if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-            mTitle.setPaddingRelative(0, 0, 0, 0);
-            mTitle.setGravity(Gravity.START);
-            setGravity(Gravity.START);
-            RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams();
-            lp.removeRule(RelativeLayout.CENTER_HORIZONTAL);
-            setLayoutParams(lp);
-        } else {
-            final int horizontalPaddingDpValue = (int) TypedValue.applyDimension(
-                    TypedValue.COMPLEX_UNIT_DIP,
-                    44,
-                    getResources().getDisplayMetrics()
-            );
-            mTitle.setPaddingRelative(horizontalPaddingDpValue, 0, horizontalPaddingDpValue, 0);
-            mTitle.setGravity(Gravity.CENTER_HORIZONTAL);
-            setGravity(Gravity.CENTER_HORIZONTAL);
-            RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams();
-            lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
-            setLayoutParams(lp);
-        }
-        mRow.setLockscreenMode(mode);
-        requestLayout();
-    }
-
     Map<View, PendingIntent> showSlice(RowContent header, List<SliceContent> subItems) {
         Trace.beginSection("KeyguardSliceView#showSlice");
         mHasHeader = header != null;
@@ -189,8 +156,7 @@
         final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
         mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);
         LinearLayout.LayoutParams layoutParams = (LayoutParams) mRow.getLayoutParams();
-        layoutParams.gravity = mLockScreenMode !=  KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL
-                ? Gravity.START :  Gravity.CENTER;
+        layoutParams.gravity = Gravity.START;
         mRow.setLayoutParams(layoutParams);
 
         for (int i = startIndex; i < subItemsCount; i++) {
@@ -224,8 +190,7 @@
                 final int iconSize = mHasHeader ? mIconSizeWithHeader : mIconSize;
                 iconDrawable = icon.getIcon().loadDrawable(mContext);
                 if (iconDrawable != null) {
-                    if ((iconDrawable instanceof InsetDrawable)
-                            && mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
+                    if (iconDrawable instanceof InsetDrawable) {
                         // System icons (DnD) use insets which are fine for centered slice content
                         // but will cause a slight indent for left/right-aligned slice views
                         iconDrawable = ((InsetDrawable) iconDrawable).getDrawable();
@@ -321,7 +286,6 @@
         pw.println("  mTextColor: " + Integer.toHexString(mTextColor));
         pw.println("  mDarkAmount: " + mDarkAmount);
         pw.println("  mHasHeader: " + mHasHeader);
-        pw.println("  mLockScreenMode: " + mLockScreenMode);
     }
 
     @Override
@@ -332,7 +296,6 @@
 
     public static class Row extends LinearLayout {
         private Set<KeyguardSliceTextView> mKeyguardSliceTextViewSet = new HashSet();
-        private int mLockScreenModeRow = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
 
         /**
          * This view is visible in AOD, which means that the device will sleep if we
@@ -407,11 +370,7 @@
             for (int i = 0; i < childCount; i++) {
                 View child = getChildAt(i);
                 if (child instanceof KeyguardSliceTextView) {
-                    if (mLockScreenModeRow == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                        ((KeyguardSliceTextView) child).setMaxWidth(Integer.MAX_VALUE);
-                    } else {
-                        ((KeyguardSliceTextView) child).setMaxWidth(width / 3);
-                    }
+                    ((KeyguardSliceTextView) child).setMaxWidth(Integer.MAX_VALUE);
                 }
             }
 
@@ -443,7 +402,6 @@
             super.addView(view, index);
 
             if (view instanceof KeyguardSliceTextView) {
-                ((KeyguardSliceTextView) view).setLockScreenMode(mLockScreenModeRow);
                 mKeyguardSliceTextViewSet.add((KeyguardSliceTextView) view);
             }
         }
@@ -455,24 +413,6 @@
                 mKeyguardSliceTextViewSet.remove((KeyguardSliceTextView) view);
             }
         }
-
-        /**
-         * Updates the lockscreen mode which may change the layout of this view.
-         */
-        public void setLockscreenMode(int mode) {
-            mLockScreenModeRow = mode;
-            if (mLockScreenModeRow == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                setOrientation(LinearLayout.VERTICAL);
-                setGravity(Gravity.START);
-            } else {
-                setOrientation(LinearLayout.HORIZONTAL);
-                setGravity(Gravity.CENTER);
-            }
-
-            for (KeyguardSliceTextView textView : mKeyguardSliceTextViewSet) {
-                textView.setLockScreenMode(mLockScreenModeRow);
-            }
-        }
     }
 
     /**
@@ -480,7 +420,6 @@
      */
     @VisibleForTesting
     static class KeyguardSliceTextView extends TextView {
-        private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
 
         @StyleRes
         private static int sStyleId = R.style.TextAppearance_Keyguard_Secondary;
@@ -509,13 +448,8 @@
             boolean hasText = !TextUtils.isEmpty(getText());
             int padding = (int) getContext().getResources()
                     .getDimension(R.dimen.widget_horizontal_padding) / 2;
-            if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                // orientation is vertical, so add padding to top & bottom
-                setPadding(0, padding, 0, hasText ? padding : 0);
-            } else {
-                // orientation is horizontal, so add padding to left & right
-                setPadding(padding, 0, padding * (hasText ? 1 : -1), 0);
-            }
+            // orientation is vertical, so add padding to top & bottom
+            setPadding(0, padding, 0, hasText ? padding : 0);
 
             setCompoundDrawablePadding((int) mContext.getResources()
                     .getDimension(R.dimen.widget_icon_padding));
@@ -543,18 +477,5 @@
                 }
             }
         }
-
-        /**
-         * Updates the lockscreen mode which may change the layout of this view.
-         */
-        public void setLockScreenMode(int mode) {
-            mLockScreenMode = mode;
-            if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
-            } else {
-                setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
-            }
-            updatePadding();
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
index 8038ce4..d05cc4e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
@@ -73,7 +73,6 @@
     private Uri mKeyguardSliceUri;
     private Slice mSlice;
     private Map<View, PendingIntent> mClickActions;
-    private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
 
     TunerService.Tunable mTunable = (key, newValue) -> setupUri(newValue);
 
@@ -84,7 +83,7 @@
             mView.onDensityOrFontScaleChanged();
         }
         @Override
-        public void onOverlayChanged() {
+        public void onThemeChanged() {
             mView.onOverlayChanged();
         }
     };
@@ -137,7 +136,6 @@
                 TAG + "@" + Integer.toHexString(
                         KeyguardSliceViewController.this.hashCode()),
                 KeyguardSliceViewController.this);
-        mView.updateLockScreenMode(mLockScreenMode);
     }
 
     @Override
@@ -160,14 +158,6 @@
     }
 
     /**
-     * Updates the lockscreen mode which may change the layout of the keyguard slice view.
-     */
-    public void updateLockScreenMode(int mode) {
-        mLockScreenMode = mode;
-        mView.updateLockScreenMode(mLockScreenMode);
-    }
-
-    /**
      * Sets the slice provider Uri.
      */
     public void setupUri(String uriString) {
@@ -249,6 +239,5 @@
     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("  mSlice: " + mSlice);
         pw.println("  mClickActions: " + mClickActions);
-        pw.println("  mLockScreenMode: " + mLockScreenMode);
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 2362a1a..a72a050e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -88,7 +88,7 @@
             mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext));
         }
 
-        mKeyguardSlice = findViewById(R.id.keyguard_status_area);
+        mKeyguardSlice = findViewById(R.id.keyguard_slice_view);
         mTextColor = mClockView.getCurrentTextColor();
 
         mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 6b3e9c2..c58710c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -19,6 +19,7 @@
 import android.graphics.Rect;
 import android.util.Slog;
 
+import com.android.keyguard.KeyguardClockSwitch.ClockSize;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
@@ -116,10 +117,11 @@
     }
 
     /**
-     * Set whether or not the lock screen is showing notifications.
+     * Set which clock should be displayed on the keyguard. The other one will be automatically
+     * hidden.
      */
-    public void setHasVisibleNotifications(boolean hasVisibleNotifications) {
-        mKeyguardClockSwitchController.setHasVisibleNotifications(hasVisibleNotifications);
+    public void displayClock(@ClockSize int clockSize) {
+        mKeyguardClockSwitchController.displayClock(clockSize);
     }
 
     /**
@@ -247,11 +249,6 @@
 
     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
         @Override
-        public void onLockScreenModeChanged(int mode) {
-            mKeyguardSliceViewController.updateLockScreenMode(mode);
-        }
-
-        @Override
         public void onTimeChanged() {
             refreshTime();
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 877e764..a41a497 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -71,7 +71,6 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.Vibrator;
 import android.provider.Settings;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamManager;
@@ -86,10 +85,11 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
-import androidx.annotation.Nullable;
 import androidx.lifecycle.Observer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.util.LatencyTracker;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.settingslib.WirelessUtils;
 import com.android.settingslib.fuelgauge.BatteryStatus;
@@ -97,16 +97,15 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.biometrics.UdfpsController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 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.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -190,9 +189,6 @@
     private static final int MSG_TIME_FORMAT_UPDATE = 344;
     private static final int MSG_REQUIRE_NFC_UNLOCK = 345;
 
-    public static final int LOCK_SCREEN_MODE_NORMAL = 0;
-    public static final int LOCK_SCREEN_MODE_LAYOUT_1 = 1;
-
     /** Biometric authentication state: Not listening. */
     private static final int BIOMETRIC_STATE_STOPPED = 0;
 
@@ -276,7 +272,6 @@
     private boolean mBouncer; // true if bouncerIsOrWillBeShowing
     private boolean mAuthInterruptActive;
     private boolean mNeedsSlowUnlockTransition;
-    private boolean mHasLockscreenWallpaper;
     private boolean mAssistantVisible;
     private boolean mKeyguardOccluded;
     private boolean mOccludingAppRequestingFp;
@@ -285,9 +280,6 @@
     @VisibleForTesting
     protected boolean mTelephonyCapable;
 
-    private final boolean mAcquiredHapticEnabled = false;
-    @Nullable private final Vibrator mVibrator;
-
     // Device provisioning state
     private boolean mDeviceProvisioned;
 
@@ -321,6 +313,8 @@
     private boolean mIsDreaming;
     private final DevicePolicyManager mDevicePolicyManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final InteractionJankMonitor mInteractionJankMonitor;
+    private final LatencyTracker mLatencyTracker;
     private boolean mLogoutEnabled;
     // cached value to avoid IPCs
     private boolean mIsUdfpsEnrolled;
@@ -1405,7 +1399,6 @@
     @VisibleForTesting
     final FingerprintManager.AuthenticationCallback mFingerprintAuthenticationCallback
             = new AuthenticationCallback() {
-                private boolean mPlayedAcquiredHaptic;
 
                 @Override
                 public void onAuthenticationFailed() {
@@ -1417,11 +1410,6 @@
                     Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded");
                     handleFingerprintAuthenticated(result.getUserId(), result.isStrongBiometric());
                     Trace.endSection();
-
-                    // on auth success, we sometimes never received an acquired haptic
-                    if (!mPlayedAcquiredHaptic && isUdfpsEnrolled()) {
-                        playAcquiredHaptic();
-                    }
                 }
 
                 @Override
@@ -1437,17 +1425,11 @@
                 @Override
                 public void onAuthenticationAcquired(int acquireInfo) {
                     handleFingerprintAcquired(acquireInfo);
-                    if (acquireInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD
-                            && isUdfpsEnrolled()) {
-                        mPlayedAcquiredHaptic = true;
-                        playAcquiredHaptic();
-                    }
                 }
 
                 @Override
                 public void onUdfpsPointerDown(int sensorId) {
                     Log.d(TAG, "onUdfpsPointerDown, sensorId: " + sensorId);
-                    mPlayedAcquiredHaptic = false;
                 }
 
                 @Override
@@ -1456,17 +1438,6 @@
                 }
             };
 
-    /**
-     * Play haptic to signal udfps fingeprrint acquired.
-     */
-    @VisibleForTesting
-    public void playAcquiredHaptic() {
-        if (mAcquiredHapticEnabled && mVibrator != null) {
-            mVibrator.vibrate(UdfpsController.EFFECT_CLICK,
-                    UdfpsController.VIBRATION_SONIFICATION_ATTRIBUTES);
-        }
-    }
-
     private final FaceManager.FaceDetectionCallback mFaceDetectionCallback
             = (sensorId, userId, isStrongBiometric) -> {
                 // Trigger the face success path so the bouncer can be shown
@@ -1770,7 +1741,8 @@
             AuthController authController,
             TelephonyListenerManager telephonyListenerManager,
             FeatureFlags featureFlags,
-            @Nullable Vibrator vibrator) {
+            InteractionJankMonitor interactionJankMonitor,
+            LatencyTracker latencyTracker) {
         mContext = context;
         mSubscriptionManager = SubscriptionManager.from(context);
         mTelephonyListenerManager = telephonyListenerManager;
@@ -1778,6 +1750,8 @@
         mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged);
         mBackgroundExecutor = backgroundExecutor;
         mBroadcastDispatcher = broadcastDispatcher;
+        mInteractionJankMonitor = interactionJankMonitor;
+        mLatencyTracker = latencyTracker;
         mRingerModeTracker = ringerModeTracker;
         mStatusBarStateController = statusBarStateController;
         mStatusBarStateController.addCallback(mStatusBarStateControllerListener);
@@ -1785,7 +1759,6 @@
         mLockPatternUtils = lockPatternUtils;
         mAuthController = authController;
         dumpManager.registerDumpable(getClass().getName(), this);
-        mVibrator = vibrator;
 
         mHandler = new Handler(mainLooper) {
             @Override
@@ -1894,9 +1867,6 @@
                     case MSG_KEYGUARD_GOING_AWAY:
                         handleKeyguardGoingAway((boolean) msg.obj);
                         break;
-                    case MSG_LOCK_SCREEN_MODE:
-                        handleLockScreenMode();
-                        break;
                     case MSG_TIME_FORMAT_UPDATE:
                         handleTimeFormatUpdate((String) msg.obj);
                         break;
@@ -2038,8 +2008,6 @@
             }
         }
 
-        updateLockScreenMode(featureFlags.isKeyguardLayoutEnabled());
-
         mTimeFormatChangeObserver = new ContentObserver(mHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -2055,14 +2023,6 @@
                 false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
     }
 
-    private void updateLockScreenMode(boolean isEnabled) {
-        final int newMode = isEnabled ? LOCK_SCREEN_MODE_LAYOUT_1 : LOCK_SCREEN_MODE_NORMAL;
-        if (newMode != mLockScreenMode) {
-            mLockScreenMode = newMode;
-            mHandler.sendEmptyMessage(MSG_LOCK_SCREEN_MODE);
-        }
-    }
-
     private void updateUdfpsEnrolled(int userId) {
         mIsUdfpsEnrolled = mAuthController.isUdfpsEnrolled(userId);
     }
@@ -2575,31 +2535,6 @@
     }
 
     /**
-     * Update the state whether Keyguard currently has a lockscreen wallpaper.
-     *
-     * @param hasLockscreenWallpaper Whether Keyguard has a lockscreen wallpaper.
-     */
-    public void setHasLockscreenWallpaper(boolean hasLockscreenWallpaper) {
-        Assert.isMainThread();
-        if (hasLockscreenWallpaper != mHasLockscreenWallpaper) {
-            mHasLockscreenWallpaper = hasLockscreenWallpaper;
-            for (int i = 0; i < mCallbacks.size(); i++) {
-                KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
-                if (cb != null) {
-                    cb.onHasLockscreenWallpaperChanged(hasLockscreenWallpaper);
-                }
-            }
-        }
-    }
-
-    /**
-     * @return Whether Keyguard has a lockscreen wallpaper.
-     */
-    public boolean hasLockscreenWallpaper() {
-        return mHasLockscreenWallpaper;
-    }
-
-    /**
      * Handle {@link #MSG_DPM_STATE_CHANGED}
      */
     private void handleDevicePolicyManagerStateChanged(int userId) {
@@ -2637,7 +2572,8 @@
     /**
      * Handle {@link #MSG_USER_SWITCH_COMPLETE}
      */
-    private void handleUserSwitchComplete(int userId) {
+    @VisibleForTesting
+    void handleUserSwitchComplete(int userId) {
         Assert.isMainThread();
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
@@ -2645,6 +2581,8 @@
                 cb.onUserSwitchComplete(userId);
             }
         }
+        mInteractionJankMonitor.end(InteractionJankMonitor.CUJ_USER_SWITCH);
+        mLatencyTracker.onActionEnd(LatencyTracker.ACTION_USER_SWITCH);
     }
 
     /**
@@ -2716,20 +2654,6 @@
     }
 
     /**
-     * Handle {@link #MSG_LOCK_SCREEN_MODE}
-     */
-    private void handleLockScreenMode() {
-        Assert.isMainThread();
-        if (DEBUG) Log.d(TAG, "handleLockScreenMode(" + mLockScreenMode + ")");
-        for (int i = 0; i < mCallbacks.size(); i++) {
-            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
-            if (cb != null) {
-                cb.onLockScreenModeChanged(mLockScreenMode);
-            }
-        }
-    }
-
-    /**
      * Handle (@line #MSG_TIMEZONE_UPDATE}
      */
     private void handleTimeZoneUpdate(String timeZone) {
@@ -2908,6 +2832,20 @@
         updateBiometricListeningState();
     }
 
+    /** Notifies that the occluded state changed. */
+    public void onKeyguardOccludedChanged(boolean occluded) {
+        Assert.isMainThread();
+        if (DEBUG) {
+            Log.d(TAG, "onKeyguardOccludedChanged(" + occluded + ")");
+        }
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+            if (cb != null) {
+                cb.onKeyguardOccludedChanged(occluded);
+            }
+        }
+    }
+
     /**
      * Handle {@link #MSG_KEYGUARD_RESET}
      */
@@ -3090,9 +3028,9 @@
         callback.onPhoneStateChanged(mPhoneState);
         callback.onRefreshCarrierInfo();
         callback.onClockVisibilityChanged();
+        callback.onKeyguardOccludedChanged(mKeyguardOccluded);
         callback.onKeyguardVisibilityChangedRaw(mKeyguardIsVisible);
         callback.onTelephonyCapable(mTelephonyCapable);
-        callback.onLockScreenModeChanged(mLockScreenMode);
 
         for (Entry<Integer, SimData> data : mSimDatas.entrySet()) {
             final SimData state = data.getValue();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 9849a7e..12431984 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -88,6 +88,12 @@
      */
     public void onKeyguardVisibilityChanged(boolean showing) { }
 
+    /**
+     * Called when the keyguard occluded state changes.
+     * @param occluded Indicates if the keyguard is now occluded.
+     */
+    public void onKeyguardOccludedChanged(boolean occluded) { }
+
     public void onKeyguardVisibilityChangedRaw(boolean showing) {
         final long now = SystemClock.elapsedRealtime();
         if (showing == mShowing
@@ -286,11 +292,6 @@
     public void onStrongAuthStateChanged(int userId) { }
 
     /**
-     * Called when the state whether we have a lockscreen wallpaper has changed.
-     */
-    public void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) { }
-
-    /**
      * Called when the dream's window state is changed.
      * @param dreaming true if the dream's window has been created and is visible
      */
@@ -324,11 +325,6 @@
     public void onSecondaryLockscreenRequirementChanged(int userId) { }
 
     /**
-     * Called to switch lock screen layout/clock layouts
-     */
-    public void onLockScreenModeChanged(int mode) { }
-
-    /**
      * Called when notifying user to unlock in order to use NFC.
      */
     public void onRequireUnlockForNfc() { }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index ecc8c00..db729da 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -18,7 +18,6 @@
 
 import android.os.Bundle;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewRootImpl;
 
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -186,14 +185,12 @@
     /**
      * Registers the StatusBar to which this Keyguard View is mounted.
      * @param statusBar
-     * @param container
      * @param notificationPanelViewController
      * @param biometricUnlockController
      * @param notificationContainer
      * @param bypassController
      */
     void registerStatusBar(StatusBar statusBar,
-            ViewGroup container,
             NotificationPanelViewController notificationPanelViewController,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconView.java b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
index ef4353b..68132f4 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconView.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
@@ -26,6 +26,7 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
@@ -40,6 +41,17 @@
  * A view positioned under the notification shade.
  */
 public class LockIconView extends FrameLayout implements Dumpable {
+    @IntDef({ICON_NONE, ICON_LOCK, ICON_FINGERPRINT, ICON_UNLOCK})
+    public @interface IconType {}
+
+    public static final int ICON_NONE = -1;
+    public static final int ICON_LOCK = 0;
+    public static final int ICON_FINGERPRINT = 1;
+    public static final int ICON_UNLOCK = 2;
+
+    private @IconType int mIconType;
+    private boolean mAod;
+
     @NonNull private final RectF mSensorRect;
     @NonNull private PointF mLockIconCenter = new PointF(0f, 0f);
     private int mRadius;
@@ -49,6 +61,7 @@
 
     private int mLockIconColor;
     private boolean mUseBackground = false;
+    private float mDozeAmount = 0f;
 
     public LockIconView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -62,11 +75,17 @@
         mBgView = findViewById(R.id.lock_icon_bg);
     }
 
+    void setDozeAmount(float dozeAmount) {
+        mDozeAmount = dozeAmount;
+        updateColorAndBackgroundVisibility();
+    }
+
     void updateColorAndBackgroundVisibility() {
         if (mUseBackground && mLockIcon.getDrawable() != null) {
             mLockIconColor = Utils.getColorAttrDefaultColor(getContext(),
                     android.R.attr.textColorPrimary);
             mBgView.setBackground(getContext().getDrawable(R.drawable.fingerprint_bg));
+            mBgView.setAlpha(1f - mDozeAmount);
             mBgView.setVisibility(View.VISIBLE);
         } else {
             mLockIconColor = Utils.getColorAttrDefaultColor(getContext(),
@@ -129,10 +148,75 @@
         return mLockIconCenter.y - mRadius;
     }
 
+    /**
+     * Updates the icon its default state where no visual is shown.
+     */
+    public void clearIcon() {
+        updateIcon(ICON_NONE, false);
+    }
+
+    /**
+     * Transition the current icon to a new state
+     * @param icon type (ie: lock icon, unlock icon, fingerprint icon)
+     * @param aod whether to use the aod icon variant (some icons don't have aod variants and will
+     *            therefore show no icon)
+     */
+    public void updateIcon(@IconType int icon, boolean aod) {
+        mIconType = icon;
+        mAod = aod;
+
+        mLockIcon.setImageState(getLockIconState(mIconType, mAod), true);
+    }
+
+    private static int[] getLockIconState(@IconType int icon, boolean aod) {
+        if (icon == ICON_NONE) {
+            return new int[0];
+        }
+
+        int[] lockIconState = new int[2];
+        switch (icon) {
+            case ICON_LOCK:
+                lockIconState[0] = android.R.attr.state_first;
+                break;
+            case ICON_FINGERPRINT:
+                lockIconState[0] = android.R.attr.state_middle;
+                break;
+            case ICON_UNLOCK:
+                lockIconState[0] = android.R.attr.state_last;
+                break;
+        }
+
+        if (aod) {
+            lockIconState[1] = android.R.attr.state_single;
+        } else {
+            lockIconState[1] = -android.R.attr.state_single;
+        }
+
+        return lockIconState;
+    }
+
+    private String typeToString(@IconType int type) {
+        switch (type) {
+            case ICON_NONE:
+                return "none";
+            case ICON_LOCK:
+                return "lock";
+            case ICON_FINGERPRINT:
+                return "fingerprint";
+            case ICON_UNLOCK:
+                return "unlock";
+        }
+
+        return "invalid";
+    }
+
     @Override
     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("Center in px (x, y)= (" + mLockIconCenter.x + ", " + mLockIconCenter.y + ")");
         pw.println("Radius in pixels: " + mRadius);
         pw.println("topLeft= (" + getX() + ", " + getY() + ")");
+        pw.println("topLeft= (" + getX() + ", " + getY() + ")");
+        pw.println("mIconType=" + typeToString(mIconType));
+        pw.println("mAod=" + mAod);
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 52ebf2f..94b1728 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -18,17 +18,20 @@
 
 import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
 
+import static com.android.keyguard.LockIconView.ICON_FINGERPRINT;
+import static com.android.keyguard.LockIconView.ICON_LOCK;
+import static com.android.keyguard.LockIconView.ICON_UNLOCK;
 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInProgressOffset;
 
-import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.drawable.AnimatedVectorDrawable;
-import android.graphics.drawable.Drawable;
+import android.graphics.drawable.AnimatedStateListDrawable;
 import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.media.AudioAttributes;
 import android.os.Process;
@@ -37,6 +40,7 @@
 import android.util.MathUtils;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityManager;
@@ -97,14 +101,12 @@
     @NonNull private final AccessibilityManager mAccessibilityManager;
     @NonNull private final ConfigurationController mConfigurationController;
     @NonNull private final DelayableExecutor mExecutor;
+    @NonNull private final LayoutInflater mLayoutInflater;
     private boolean mUdfpsEnrolled;
 
-    @NonNull private LottieAnimationView mAodFp;
+    @Nullable private LottieAnimationView mAodFp;
+    @NonNull private final AnimatedStateListDrawable mIcon;
 
-    @NonNull private final AnimatedVectorDrawable mFpToUnlockIcon;
-    @NonNull private final AnimatedVectorDrawable mLockToUnlockIcon;
-    @NonNull private final Drawable mLockIcon;
-    @NonNull private final Drawable mUnlockIcon;
     @NonNull private CharSequence mUnlockedLabel;
     @NonNull private CharSequence mLockedLabel;
     @Nullable private final Vibrator mVibrator;
@@ -130,13 +132,13 @@
     private boolean mShowLockIcon;
 
     // for udfps when strong auth is required or unlocked on AOD
+    private boolean mShowAodLockIcon;
     private boolean mShowAODFpIcon;
     private final int mMaxBurnInOffsetX;
     private final int mMaxBurnInOffsetY;
     private float mInterpolatedDarkAmount;
 
     private boolean mDownDetected;
-    private boolean mDetectedLongPress;
     private final Rect mSensorTouchLocation = new Rect();
 
     @Inject
@@ -153,7 +155,9 @@
             @NonNull ConfigurationController configurationController,
             @NonNull @Main DelayableExecutor executor,
             @Nullable Vibrator vibrator,
-            @Nullable AuthRippleController authRippleController
+            @Nullable AuthRippleController authRippleController,
+            @NonNull @Main Resources resources,
+            @NonNull LayoutInflater inflater
     ) {
         super(view);
         mStatusBarStateController = statusBarStateController;
@@ -167,27 +171,16 @@
         mExecutor = executor;
         mVibrator = vibrator;
         mAuthRippleController = authRippleController;
+        mLayoutInflater = inflater;
 
-        final Context context = view.getContext();
-        mAodFp = mView.findViewById(R.id.lock_udfps_aod_fp);
-        mMaxBurnInOffsetX = context.getResources()
-                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
-        mMaxBurnInOffsetY = context.getResources()
-                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
+        mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
+        mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
 
-        mUnlockIcon = mView.getContext().getResources().getDrawable(
-            R.drawable.ic_unlock,
-            mView.getContext().getTheme());
-        mLockIcon = mView.getContext().getResources().getDrawable(
-                R.anim.lock_to_unlock,
-                mView.getContext().getTheme());
-        mFpToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable(
-                R.anim.fp_to_unlock, mView.getContext().getTheme());
-        mLockToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable(
-                R.anim.lock_to_unlock,
-                mView.getContext().getTheme());
-        mUnlockedLabel = context.getResources().getString(R.string.accessibility_unlock_button);
-        mLockedLabel = context.getResources().getString(R.string.accessibility_lock_icon);
+        mIcon = (AnimatedStateListDrawable)
+                resources.getDrawable(R.drawable.super_lock_icon, mView.getContext().getTheme());
+        mView.setImageDrawable(mIcon);
+        mUnlockedLabel = resources.getString(R.string.accessibility_unlock_button);
+        mLockedLabel = resources.getString(R.string.accessibility_lock_icon);
         dumpManager.registerDumpable("LockIconViewController", this);
     }
 
@@ -259,47 +252,52 @@
             return;
         }
 
+        boolean wasShowingUnlock = mShowUnlockIcon;
         boolean wasShowingFpIcon = mUdfpsEnrolled && !mShowUnlockIcon && !mShowLockIcon;
-        boolean wasShowingLockIcon = mShowLockIcon;
-        boolean wasShowingUnlockIcon = mShowUnlockIcon;
         mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen()
-            && (!mUdfpsEnrolled || !mRunningFPS);
-        mShowUnlockIcon = mCanDismissLockScreen && isLockScreen();
-        mShowAODFpIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS;
+                && (!mUdfpsEnrolled || !mRunningFPS);
+        mShowUnlockIcon = (mCanDismissLockScreen || mUserUnlockedWithBiometric) && isLockScreen();
+        mShowAODFpIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS && mCanDismissLockScreen;
+        mShowAodLockIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS && !mCanDismissLockScreen;
 
         final CharSequence prevContentDescription = mView.getContentDescription();
         if (mShowLockIcon) {
-            mView.setImageDrawable(mLockIcon);
-            mView.setVisibility(View.VISIBLE);
+            mView.updateIcon(ICON_LOCK, false);
             mView.setContentDescription(mLockedLabel);
-        } else if (mShowUnlockIcon) {
-            if (!wasShowingUnlockIcon) {
-                if (wasShowingFpIcon) {
-                    mView.setImageDrawable(mFpToUnlockIcon);
-                    mFpToUnlockIcon.forceAnimationOnUI();
-                    mFpToUnlockIcon.start();
-                } else if (wasShowingLockIcon) {
-                    mView.setImageDrawable(mLockToUnlockIcon);
-                    mLockToUnlockIcon.forceAnimationOnUI();
-                    mLockToUnlockIcon.start();
-                } else {
-                    mView.setImageDrawable(mUnlockIcon);
-                }
-            }
             mView.setVisibility(View.VISIBLE);
+        } else if (mShowUnlockIcon) {
+            if (wasShowingFpIcon) {
+                // fp icon was shown by UdfpsView, and now we still want to animate the transition
+                // in this drawable
+                mView.updateIcon(ICON_FINGERPRINT, false);
+            }
+            mView.updateIcon(ICON_UNLOCK, false);
             mView.setContentDescription(mUnlockedLabel);
+            mView.setVisibility(View.VISIBLE);
         } else if (mShowAODFpIcon) {
-            mView.setImageDrawable(null);
+            // AOD fp icon is special cased as a lottie view (it updates for each burn-in offset),
+            // this state shows a transparent view
             mView.setContentDescription(null);
             mAodFp.setVisibility(View.VISIBLE);
             mAodFp.setContentDescription(mCanDismissLockScreen ? mUnlockedLabel : mLockedLabel);
+
+            mView.updateIcon(ICON_FINGERPRINT, true); // this shows no icon
+            mView.setVisibility(View.VISIBLE);
+        } else if (mShowAodLockIcon) {
+            if (wasShowingUnlock) {
+                // transition to the unlock icon first
+                mView.updateIcon(ICON_LOCK, false);
+            }
+            mView.updateIcon(ICON_LOCK, true);
+            mView.setContentDescription(mLockedLabel);
             mView.setVisibility(View.VISIBLE);
         } else {
+            mView.clearIcon();
             mView.setVisibility(View.INVISIBLE);
             mView.setContentDescription(null);
         }
 
-        if (!mShowAODFpIcon) {
+        if (!mShowAODFpIcon && mAodFp != null) {
             mAodFp.setVisibility(View.INVISIBLE);
             mAodFp.setContentDescription(null);
         }
@@ -366,8 +364,9 @@
     private void updateLockIconLocation() {
         if (mUdfpsSupported) {
             FingerprintSensorPropertiesInternal props = mAuthController.getUdfpsProps().get(0);
-            mView.setCenterLocation(new PointF(props.sensorLocationX, props.sensorLocationY),
-                    props.sensorRadius);
+            final SensorLocationInternal location = props.getLocation();
+            mView.setCenterLocation(new PointF(location.sensorLocationX, location.sensorLocationY),
+                    location.sensorRadius);
         } else {
             mView.setCenterLocation(
                     new PointF(mWidthPixels / 2,
@@ -383,6 +382,11 @@
         pw.println("mUdfpsSupported: " + mUdfpsSupported);
         pw.println("mUdfpsEnrolled: " + mUdfpsEnrolled);
         pw.println("mIsKeyguardShowing: " + mIsKeyguardShowing);
+        pw.println(" mIcon: ");
+        for (int state : mIcon.getState()) {
+            pw.print(" " + state);
+        }
+        pw.println();
         pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
         pw.println(" mShowLockIcon: " + mShowLockIcon);
         pw.println(" mShowAODFpIcon: " + mShowAODFpIcon);
@@ -414,10 +418,17 @@
                         - mMaxBurnInOffsetY, mInterpolatedDarkAmount);
         float progress = MathUtils.lerp(0f, getBurnInProgressOffset(), mInterpolatedDarkAmount);
 
-        mAodFp.setTranslationX(offsetX);
-        mAodFp.setTranslationY(offsetY);
-        mAodFp.setProgress(progress);
-        mAodFp.setAlpha(255 * mInterpolatedDarkAmount);
+        if (mAodFp != null) {
+            mAodFp.setTranslationX(offsetX);
+            mAodFp.setTranslationY(offsetY);
+            mAodFp.setProgress(progress);
+            mAodFp.setAlpha(255 * mInterpolatedDarkAmount);
+        }
+
+        if (mShowAodLockIcon) {
+            mView.setTranslationX(offsetX);
+            mView.setTranslationY(offsetY);
+        }
     }
 
     private void updateIsUdfpsEnrolled() {
@@ -428,6 +439,10 @@
         mView.setUseBackground(mUdfpsSupported);
 
         mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled();
+        if (!wasUdfpsEnrolled && mUdfpsEnrolled && mAodFp == null) {
+            mLayoutInflater.inflate(R.layout.udfps_aod_lock_icon, mView);
+            mAodFp = mView.findViewById(R.id.lock_udfps_aod_fp);
+        }
         if (wasUdfpsSupported != mUdfpsSupported || wasUdfpsEnrolled != mUdfpsEnrolled) {
             updateVisibility();
         }
@@ -438,6 +453,7 @@
                 @Override
                 public void onDozeAmountChanged(float linear, float eased) {
                     mInterpolatedDarkAmount = eased;
+                    mView.setDozeAmount(eased);
                     updateBurnInOffsets();
                 }
 
@@ -475,13 +491,15 @@
                 @Override
                 public void onBiometricRunningStateChanged(boolean running,
                         BiometricSourceType biometricSourceType) {
+                    final boolean wasRunningFps = mRunningFPS;
+                    final boolean wasUserUnlockedWithBiometric = mUserUnlockedWithBiometric;
                     mUserUnlockedWithBiometric =
                             mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
                                     KeyguardUpdateMonitor.getCurrentUser());
 
                     if (biometricSourceType == FINGERPRINT) {
                         mRunningFPS = running;
-                        if (!mRunningFPS) {
+                        if (wasRunningFps && !mRunningFPS) {
                             if (mCancelDelayedUpdateVisibilityRunnable != null) {
                                 mCancelDelayedUpdateVisibilityRunnable.run();
                             }
@@ -491,10 +509,14 @@
                             // button in this case, so we delay updating the visibility by 50ms.
                             mCancelDelayedUpdateVisibilityRunnable =
                                     mExecutor.executeDelayed(() -> updateVisibility(), 50);
-                        } else {
-                            updateVisibility();
+                            return;
                         }
                     }
+
+                    if (wasUserUnlockedWithBiometric != mUserUnlockedWithBiometric
+                            || wasRunningFps != mRunningFPS) {
+                        updateVisibility();
+                    }
                 }
             };
 
@@ -543,11 +565,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            updateColors();
-        }
-
-        @Override
         public void onConfigChanged(Configuration newConfig) {
             updateConfiguration();
             updateColors();
@@ -557,7 +574,6 @@
     private final GestureDetector mGestureDetector =
             new GestureDetector(new SimpleOnGestureListener() {
                 public boolean onDown(MotionEvent e) {
-                    mDetectedLongPress = false;
                     if (!isClickable()) {
                         mDownDetected = false;
                         return false;
@@ -582,7 +598,6 @@
                     if (!wasClickableOnDownEvent()) {
                         return;
                     }
-                    mDetectedLongPress = true;
 
                     if (onAffordanceClick() && mVibrator != null) {
                         // only vibrate if the click went through and wasn't intercepted by falsing
@@ -648,7 +663,7 @@
     public boolean onTouchEvent(MotionEvent event, Runnable onGestureDetectedRunnable) {
         if (mSensorTouchLocation.contains((int) event.getX(), (int) event.getY())
                 && (mView.getVisibility() == View.VISIBLE
-                || mAodFp.getVisibility() == View.VISIBLE)) {
+                || (mAodFp != null && mAodFp.getVisibility() == View.VISIBLE))) {
             mOnGestureDetectedRunnable = onGestureDetectedRunnable;
             mGestureDetector.onTouchEvent(event);
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
index 57407f1..f4ce643 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
+import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.RippleDrawable;
 import android.graphics.drawable.VectorDrawable;
@@ -26,8 +27,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.settingslib.Utils;
-
 /**
  * Similar to the {@link NumPadKey}, but displays an image.
  */
@@ -59,7 +58,9 @@
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
         // Set width/height to the same value to ensure a smooth circle for the bg, but shrink
-        // the height to match the old pin bouncer
+        // the height to match the old pin bouncer.
+        // This is only used for PIN/PUK; the main PIN pad now uses ConstraintLayout, which will
+        // force our width/height to conform to the ratio in the layout.
         int width = getMeasuredWidth();
 
         boolean shortenHeight = mAnimator == null
@@ -90,8 +91,10 @@
     public void reloadColors() {
         if (mAnimator != null) mAnimator.reloadColors(getContext());
 
-        int textColor = Utils.getColorAttrDefaultColor(getContext(),
-                android.R.attr.colorBackground);
-        ((VectorDrawable) getDrawable()).setTintList(ColorStateList.valueOf(textColor));
+        int[] customAttrs = {android.R.attr.textColorPrimaryInverse};
+        TypedArray a = getContext().obtainStyledAttributes(customAttrs);
+        int imageColor = a.getColor(0, 0);
+        a.recycle();
+        ((VectorDrawable) getDrawable()).setTintList(ColorStateList.valueOf(imageColor));
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
index 232c6fc..e79ea9a 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
@@ -175,7 +175,9 @@
         measureChildren(widthMeasureSpec, heightMeasureSpec);
 
         // Set width/height to the same value to ensure a smooth circle for the bg, but shrink
-        // the height to match the old pin bouncer
+        // the height to match the old pin bouncer.
+        // This is only used for PIN/PUK; the main PIN pad now uses ConstraintLayout, which will
+        // force our width/height to conform to the ratio in the layout.
         int width = getMeasuredWidth();
 
         boolean shortenHeight = mAnimator == null
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewComponent.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewComponent.java
index 49a617e..153da4b 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewComponent.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewComponent.java
@@ -19,6 +19,7 @@
 import com.android.keyguard.KeyguardStatusViewController;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
+import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 
 import dagger.BindsInstance;
 import dagger.Subcomponent;
@@ -34,7 +35,10 @@
     /** Simple factory for {@link KeyguardStatusBarViewComponent}. */
     @Subcomponent.Factory
     interface Factory {
-        KeyguardStatusBarViewComponent build(@BindsInstance KeyguardStatusBarView view);
+        KeyguardStatusBarViewComponent build(
+                @BindsInstance KeyguardStatusBarView view,
+                @BindsInstance NotificationPanelViewController.NotificationPanelViewStateProvider
+                        notificationPanelViewStateProvider);
     }
 
     /** Builds a {@link KeyguardStatusViewController}. */
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
index a672523..fc14b6a 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
@@ -18,6 +18,7 @@
 
 import com.android.keyguard.CarrierText;
 import com.android.systemui.R;
+import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
 
 import dagger.Module;
@@ -31,4 +32,11 @@
     static CarrierText getCarrierText(KeyguardStatusBarView view) {
         return view.findViewById(R.id.keyguard_carrier_text);
     }
+
+    /** */
+    @Provides
+    @KeyguardStatusBarViewScope
+    static BatteryMeterView getBatteryMeterView(KeyguardStatusBarView view) {
+        return view.findViewById(R.id.battery);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java
index 1d51e59..b8841ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java
@@ -34,6 +34,6 @@
 
     @Provides
     static KeyguardSliceView getKeyguardSliceView(KeyguardClockSwitch keyguardClockSwitch) {
-        return keyguardClockSwitch.findViewById(R.id.keyguard_status_area);
+        return keyguardClockSwitch.findViewById(R.id.keyguard_slice_view);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java
index 62d5a45..cc166c2 100644
--- a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java
@@ -39,113 +39,116 @@
 @SysUISingleton
 public class ActivityStarterDelegate implements ActivityStarter {
 
-    private Optional<Lazy<StatusBar>> mActualStarter;
+    private Lazy<Optional<StatusBar>> mActualStarterOptionalLazy;
 
     @Inject
-    public ActivityStarterDelegate(Optional<Lazy<StatusBar>> statusBar) {
-        mActualStarter = statusBar;
+    public ActivityStarterDelegate(Lazy<Optional<StatusBar>> statusBarOptionalLazy) {
+        mActualStarterOptionalLazy = statusBarOptionalLazy;
     }
 
     @Override
     public void startPendingIntentDismissingKeyguard(PendingIntent intent) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startPendingIntentDismissingKeyguard(intent));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startPendingIntentDismissingKeyguard(intent));
     }
 
     @Override
     public void startPendingIntentDismissingKeyguard(PendingIntent intent,
             Runnable intentSentUiThreadCallback) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startPendingIntentDismissingKeyguard(intent,
-                        intentSentUiThreadCallback));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startPendingIntentDismissingKeyguard(
+                        intent, intentSentUiThreadCallback));
     }
 
     @Override
     public void startPendingIntentDismissingKeyguard(PendingIntent intent,
             Runnable intentSentUiThreadCallback, View associatedView) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startPendingIntentDismissingKeyguard(intent,
-                        intentSentUiThreadCallback, associatedView));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startPendingIntentDismissingKeyguard(
+                        intent, intentSentUiThreadCallback, associatedView));
     }
 
     @Override
     public void startPendingIntentDismissingKeyguard(PendingIntent intent,
             Runnable intentSentUiThreadCallback,
             ActivityLaunchAnimator.Controller animationController) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startPendingIntentDismissingKeyguard(intent,
-                        intentSentUiThreadCallback, animationController));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startPendingIntentDismissingKeyguard(
+                        intent, intentSentUiThreadCallback, animationController));
     }
 
     @Override
     public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade,
             int flags) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startActivity(intent, onlyProvisioned, dismissShade,
-                        flags));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startActivity(intent, onlyProvisioned, dismissShade, flags));
     }
 
     @Override
     public void startActivity(Intent intent, boolean dismissShade) {
-        mActualStarter.ifPresent(starter -> starter.get().startActivity(intent, dismissShade));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startActivity(intent, dismissShade));
     }
 
     @Override
     public void startActivity(Intent intent, boolean dismissShade,
-            @Nullable ActivityLaunchAnimator.Controller animationController) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startActivity(intent, dismissShade, animationController));
+            @Nullable ActivityLaunchAnimator.Controller animationController,
+            boolean showOverLockscreenWhenLocked) {
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startActivity(intent, dismissShade, animationController,
+                    showOverLockscreenWhenLocked));
     }
 
     @Override
     public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startActivity(intent, onlyProvisioned, dismissShade));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startActivity(intent, onlyProvisioned, dismissShade));
     }
 
     @Override
     public void startActivity(Intent intent, boolean dismissShade, Callback callback) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().startActivity(intent, dismissShade, callback));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.startActivity(intent, dismissShade, callback));
     }
 
     @Override
     public void postStartActivityDismissingKeyguard(Intent intent, int delay) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().postStartActivityDismissingKeyguard(intent, delay));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.postStartActivityDismissingKeyguard(intent, delay));
     }
 
     @Override
     public void postStartActivityDismissingKeyguard(Intent intent, int delay,
             @Nullable ActivityLaunchAnimator.Controller animationController) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().postStartActivityDismissingKeyguard(intent, delay,
-                        animationController));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.postStartActivityDismissingKeyguard(
+                        intent, delay, animationController));
     }
 
     @Override
     public void postStartActivityDismissingKeyguard(PendingIntent intent) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().postStartActivityDismissingKeyguard(intent));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.postStartActivityDismissingKeyguard(intent));
     }
 
     @Override
     public void postStartActivityDismissingKeyguard(PendingIntent intent,
             ActivityLaunchAnimator.Controller animationController) {
-        mActualStarter.ifPresent(starter ->
-                starter.get().postStartActivityDismissingKeyguard(intent, animationController));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.postStartActivityDismissingKeyguard(
+                        intent, animationController));
     }
 
     @Override
     public void postQSRunnableDismissingKeyguard(Runnable runnable) {
-        mActualStarter.ifPresent(
-                starter -> starter.get().postQSRunnableDismissingKeyguard(runnable));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.postQSRunnableDismissingKeyguard(runnable));
     }
 
     @Override
     public void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancel,
             boolean afterKeyguardGone) {
-        mActualStarter.ifPresent(starter -> starter.get().dismissKeyguardThenExecute(action, cancel,
-                afterKeyguardGone));
+        mActualStarterOptionalLazy.get().ifPresent(
+                starter -> starter.dismissKeyguardThenExecute(action, cancel, afterKeyguardGone));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java b/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java
index 5ed9eaa..12dd8f0 100644
--- a/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java
@@ -86,7 +86,7 @@
     }
 
     @Override
-    public void onOverlayChanged() {
+    public void onThemeChanged() {
         inflateLayout();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 00b33a4..c73d19b 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -47,6 +47,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -67,14 +68,12 @@
 import com.android.systemui.qs.ReduceBrightColorsController;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.recents.Recents;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -100,7 +99,6 @@
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper;
 import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarWindowController;
@@ -117,7 +115,6 @@
 import com.android.systemui.statusbar.policy.HotspotController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.LocationController;
-import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.statusbar.policy.RotationLockController;
@@ -247,7 +244,6 @@
     @Inject Lazy<BluetoothController> mBluetoothController;
     @Inject Lazy<LocationController> mLocationController;
     @Inject Lazy<RotationLockController> mRotationLockController;
-    @Inject Lazy<NetworkController> mNetworkController;
     @Inject Lazy<ZenModeController> mZenModeController;
     @Inject Lazy<HotspotController> mHotspotController;
     @Inject Lazy<CastController> mCastController;
@@ -351,8 +347,6 @@
     @Inject Lazy<DozeParameters> mDozeParameters;
     @Inject Lazy<IWallpaperManager> mWallpaperManager;
     @Inject Lazy<CommandQueue> mCommandQueue;
-    @Inject Lazy<Recents> mRecents;
-    @Inject Lazy<StatusBar> mStatusBar;
     @Inject Lazy<RecordingController> mRecordingController;
     @Inject Lazy<ProtoTracer> mProtoTracer;
     @Inject Lazy<MediaOutputDialogFactory> mMediaOutputDialogFactory;
@@ -395,8 +389,6 @@
 
         mProviders.put(RotationLockController.class, mRotationLockController::get);
 
-        mProviders.put(NetworkController.class, mNetworkController::get);
-
         mProviders.put(ZenModeController.class, mZenModeController::get);
 
         mProviders.put(HotspotController.class, mHotspotController::get);
@@ -557,8 +549,6 @@
         mProviders.put(DozeParameters.class, mDozeParameters::get);
         mProviders.put(IWallpaperManager.class, mWallpaperManager::get);
         mProviders.put(CommandQueue.class, mCommandQueue::get);
-        mProviders.put(Recents.class, mRecents::get);
-        mProviders.put(StatusBar.class, mStatusBar::get);
         mProviders.put(ProtoTracer.class, mProtoTracer::get);
         mProviders.put(DeviceConfigProxy.class, mDeviceConfigProxy::get);
         mProviders.put(TelephonyListenerManager.class, mTelephonyListenerManager::get);
@@ -607,11 +597,6 @@
         if (obj == null) {
             obj = createDependency(key);
             mDependencies.put(key, obj);
-
-            // TODO: Get dependencies to register themselves instead
-            if (autoRegisterModulesForDump() && obj instanceof Dumpable) {
-                mDumpManager.registerDumpable(obj.getClass().getName(), (Dumpable) obj);
-            }
         }
         return obj;
     }
@@ -629,17 +614,6 @@
         return provider.createDependency();
     }
 
-    // Currently, there are situations in tests where we might create more than one instance of a
-    // thing that should be a singleton: the "real" one (created by Dagger, usually as a result of
-    // inflating a view), and a mocked one (injected into Dependency). If we register the mocked
-    // one, the DumpManager will throw an exception complaining (rightly) that we have too many
-    // things registered with that name. So in tests, we disable the auto-registration until the
-    // root cause is fixed, i.e. inflated views in tests with Dagger dependencies.
-    @VisibleForTesting
-    protected boolean autoRegisterModulesForDump() {
-        return true;
-    }
-
     private static Dependency sDependency;
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
index d859a63..15e8c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -42,7 +42,8 @@
     private final Handler mMainHandler;
 
     @Inject
-    public ForegroundServiceController(AppOpsController appOpsController,
+    public ForegroundServiceController(
+            AppOpsController appOpsController,
             @Main Handler mainHandler) {
         mMainHandler = mainHandler;
         appOpsController.addCallback(APP_OPS, (code, uid, packageName, active) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java
index 8379ccf..d2566b4 100644
--- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java
@@ -126,7 +126,13 @@
             mRenderer = getRendererInstance();
             setFixedSizeAllowed(true);
             updateSurfaceSize();
-            mRenderer.setOnBitmapChanged(this::updateMiniBitmap);
+
+            mRenderer.setOnBitmapChanged(b -> {
+                mLocalColorsToAdd.addAll(mColorAreas);
+                if (mLocalColorsToAdd.size() > 0) {
+                    updateMiniBitmapAndNotify(b);
+                }
+            });
             getDisplayContext().getSystemService(DisplayManager.class)
                     .registerDisplayListener(this, mWorker.getThreadHandler());
             Trace.endSection();
@@ -170,7 +176,7 @@
                     computeAndNotifyLocalColors(new ArrayList<>(mColorAreas), mMiniBitmap));
         }
 
-        private void updateMiniBitmap(Bitmap b) {
+        private void updateMiniBitmapAndNotify(Bitmap b) {
             if (b == null) return;
             int size = Math.min(b.getWidth(), b.getHeight());
             float scale = 1.0f;
@@ -186,11 +192,13 @@
         }
 
         private void updateSurfaceSize() {
+            Trace.beginSection("ImageWallpaper#updateSurfaceSize");
             SurfaceHolder holder = getSurfaceHolder();
             Size frameSize = mRenderer.reportSurfaceSize();
             int width = Math.max(MIN_SURFACE_WIDTH, frameSize.getWidth());
             int height = Math.max(MIN_SURFACE_HEIGHT, frameSize.getHeight());
             holder.setFixedSize(width, height);
+            Trace.endSection();
         }
 
         @Override
@@ -209,12 +217,10 @@
                     .unregisterDisplayListener(this);
             mMiniBitmap = null;
             mWorker.getThreadHandler().post(() -> {
-                Trace.beginSection("ImageWallpaper.Engine#onDestroy");
                 mRenderer.finish();
                 mRenderer = null;
                 mEglHelper.finish();
                 mEglHelper = null;
-                Trace.endSection();
             });
         }
 
@@ -232,6 +238,9 @@
                 Bitmap bitmap = mMiniBitmap;
                 if (bitmap == null) {
                     mLocalColorsToAdd.addAll(regions);
+                    mRenderer.use(b -> {
+                        updateMiniBitmapAndNotify(b);
+                    });
                 } else {
                     computeAndNotifyLocalColors(regions, bitmap);
                 }
@@ -437,10 +446,8 @@
 
         public void postRender() {
             // This method should only be invoked from worker thread.
-            Trace.beginSection("ImageWallpaper#postRender");
             scheduleFinishRendering();
             reportEngineShown(false /* waitForEngineShown */);
-            Trace.endSection();
         }
 
         private void cancelFinishRenderingTask() {
diff --git a/packages/SystemUI/src/com/android/systemui/LatencyTester.java b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
index 186917f..d325b92 100644
--- a/packages/SystemUI/src/com/android/systemui/LatencyTester.java
+++ b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
@@ -16,18 +16,13 @@
 
 package com.android.systemui;
 
-import static android.os.PowerManager.WAKE_REASON_UNKNOWN;
-
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.hardware.biometrics.BiometricSourceType;
 import android.os.Build;
-import android.os.PowerManager;
-import android.os.SystemClock;
 
-import com.android.internal.util.LatencyTracker;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -48,20 +43,15 @@
     private static final String
             ACTION_FACE_WAKE =
             "com.android.systemui.latency.ACTION_FACE_WAKE";
-    private static final String
-            ACTION_TURN_ON_SCREEN =
-            "com.android.systemui.latency.ACTION_TURN_ON_SCREEN";
     private final BiometricUnlockController mBiometricUnlockController;
-    private final PowerManager mPowerManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
 
     @Inject
     public LatencyTester(Context context, BiometricUnlockController biometricUnlockController,
-            PowerManager powerManager, BroadcastDispatcher broadcastDispatcher) {
+            BroadcastDispatcher broadcastDispatcher) {
         super(context);
 
         mBiometricUnlockController = biometricUnlockController;
-        mPowerManager = powerManager;
         mBroadcastDispatcher = broadcastDispatcher;
     }
 
@@ -74,7 +64,6 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_FINGERPRINT_WAKE);
         filter.addAction(ACTION_FACE_WAKE);
-        filter.addAction(ACTION_TURN_ON_SCREEN);
         mBroadcastDispatcher.registerReceiver(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
@@ -83,22 +72,11 @@
                     fakeWakeAndUnlock(BiometricSourceType.FINGERPRINT);
                 } else if (ACTION_FACE_WAKE.equals(action)) {
                     fakeWakeAndUnlock(BiometricSourceType.FACE);
-                } else if (ACTION_TURN_ON_SCREEN.equals(action)) {
-                    fakeTurnOnScreen();
                 }
             }
         }, filter);
     }
 
-    private void fakeTurnOnScreen() {
-        if (LatencyTracker.isEnabled(mContext)) {
-            LatencyTracker.getInstance(mContext).onActionStart(
-                    LatencyTracker.ACTION_TURN_ON_SCREEN);
-        }
-        mPowerManager.wakeUp(
-                SystemClock.uptimeMillis(), WAKE_REASON_UNKNOWN, "android.policy:LATENCY_TESTS");
-    }
-
     private void fakeWakeAndUnlock(BiometricSourceType type) {
         mBiometricUnlockController.onBiometricAcquired(type);
         mBiometricUnlockController.onBiometricAuthenticated(
diff --git a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
index f9617ca..c7f1006 100644
--- a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
@@ -53,7 +53,7 @@
 
     private static final String TAG = "PluginInflateContainer";
 
-    private Class<?> mClass;
+    private Class<ViewProvider> mClass;
     private View mPluginView;
 
     public PluginInflateContainer(Context context, @Nullable AttributeSet attrs) {
@@ -61,7 +61,7 @@
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PluginInflateContainer);
         String viewType = a.getString(R.styleable.PluginInflateContainer_viewType);
         try {
-            mClass = Class.forName(viewType);
+            mClass = (Class<ViewProvider>) Class.forName(viewType);
         } catch (Exception e) {
             Log.d(TAG, "Problem getting class info " + viewType, e);
             mClass = null;
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index e9c5653..80c3616 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -42,6 +42,7 @@
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Matrix;
@@ -59,6 +60,7 @@
 import android.os.UserHandle;
 import android.provider.Settings.Secure;
 import android.util.DisplayMetrics;
+import android.util.DisplayUtils;
 import android.util.Log;
 import android.view.Display;
 import android.view.DisplayCutout;
@@ -66,6 +68,7 @@
 import android.view.DisplayInfo;
 import android.view.Gravity;
 import android.view.LayoutInflater;
+import android.view.RoundedCorners;
 import android.view.Surface;
 import android.view.View;
 import android.view.View.OnLayoutChangeListener;
@@ -157,8 +160,11 @@
     private Handler mHandler;
     private boolean mPendingRotationChange;
     private boolean mIsRoundedCornerMultipleRadius;
-    private int mStatusBarHeightPortrait;
-    private int mStatusBarHeightLandscape;
+    private boolean mIsPrivacyDotEnabled;
+    private Drawable mRoundedCornerDrawable;
+    private Drawable mRoundedCornerDrawableTop;
+    private Drawable mRoundedCornerDrawableBottom;
+    private String mDisplayUniqueId;
 
     private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback =
             new CameraAvailabilityListener.CameraTransitionCallback() {
@@ -244,10 +250,12 @@
 
     private void startOnScreenDecorationsThread() {
         mRotation = mContext.getDisplay().getRotation();
+        mDisplayUniqueId = mContext.getDisplay().getUniqueId();
+        mIsRoundedCornerMultipleRadius = isRoundedCornerMultipleRadius(mContext, mDisplayUniqueId);
+        mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot);
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
-        mIsRoundedCornerMultipleRadius = mContext.getResources().getBoolean(
-                R.bool.config_roundedCornerMultipleRadius);
+        updateRoundedCornerDrawable();
         updateRoundedCornerRadii();
         setupDecorations();
         setupCameraListener();
@@ -287,6 +295,14 @@
                         }
                     }
                 }
+                final String newUniqueId = mContext.getDisplay().getUniqueId();
+                if ((newUniqueId != null && !newUniqueId.equals(mDisplayUniqueId))
+                        || (mDisplayUniqueId != null && !mDisplayUniqueId.equals(newUniqueId))) {
+                    mDisplayUniqueId = newUniqueId;
+                    mIsRoundedCornerMultipleRadius =
+                            isRoundedCornerMultipleRadius(mContext, mDisplayUniqueId);
+                    updateRoundedCornerDrawable();
+                }
                 updateOrientation();
             }
         };
@@ -296,24 +312,24 @@
     }
 
     private void setupDecorations() {
-        if (hasRoundedCorners() || shouldDrawCutout()) {
-            updateStatusBarHeight();
+        if (hasRoundedCorners() || shouldDrawCutout() || mIsPrivacyDotEnabled) {
             final DisplayCutout cutout = getCutout();
-            final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll();
-            int rotatedPos;
             for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
-                rotatedPos = getBoundPositionFromRotation(i, mRotation);
-                if ((bounds != null && !bounds[rotatedPos].isEmpty())
-                        || shouldShowRoundedCorner(i)) {
-                    createOverlay(i);
+                if (shouldShowCutout(i, cutout) || shouldShowRoundedCorner(i, cutout)
+                        || shouldShowPrivacyDot(i, cutout)) {
+                    createOverlay(i, cutout);
                 } else {
                     removeOverlay(i);
                 }
             }
-            // Overlays have been created, send the dots to the controller
-            //TODO: need a better way to do this
-            mDotViewController.initialize(
-                    mTopLeftDot, mTopRightDot, mBottomLeftDot, mBottomRightDot);
+
+            if (mTopLeftDot != null && mTopRightDot != null && mBottomLeftDot != null
+                    && mBottomRightDot != null) {
+                // Overlays have been created, send the dots to the controller
+                //TODO: need a better way to do this
+                mDotViewController.initialize(
+                        mTopLeftDot, mTopRightDot, mBottomLeftDot, mBottomRightDot);
+            }
         } else {
             removeAllOverlays();
         }
@@ -400,7 +416,7 @@
         mOverlays[pos] = null;
     }
 
-    private void createOverlay(@BoundsPosition int pos) {
+    private void createOverlay(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
         if (mOverlays == null) {
             mOverlays = new View[BOUNDS_POSITION_LENGTH];
         }
@@ -412,7 +428,7 @@
         if (mOverlays[pos] != null) {
             return;
         }
-        mOverlays[pos] = overlayForPosition(pos);
+        mOverlays[pos] = overlayForPosition(pos, cutout);
 
         mCutoutViews[pos] = new DisplayCutoutView(mContext, pos, this);
         ((ViewGroup) mOverlays[pos]).addView(mCutoutViews[pos]);
@@ -421,7 +437,7 @@
         mOverlays[pos].setAlpha(0);
         mOverlays[pos].setForceDarkAllowed(false);
 
-        updateView(pos);
+        updateView(pos, cutout);
 
         mWindowManager.addView(mOverlays[pos], getWindowLayoutParams(pos));
 
@@ -444,36 +460,58 @@
     /**
      * Allow overrides for top/bottom positions
      */
-    private View overlayForPosition(@BoundsPosition int pos) {
+    private View overlayForPosition(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
+        final int layoutId = (pos == BOUNDS_POSITION_LEFT || pos == BOUNDS_POSITION_TOP)
+                ? R.layout.rounded_corners_top : R.layout.rounded_corners_bottom;
+        final ViewGroup vg = (ViewGroup) LayoutInflater.from(mContext).inflate(layoutId, null);
+        initPrivacyDotView(vg, pos, cutout);
+        return vg;
+    }
+
+    private void initPrivacyDotView(@NonNull ViewGroup viewGroup, @BoundsPosition int pos,
+            @Nullable DisplayCutout cutout) {
+        final View left = viewGroup.findViewById(R.id.privacy_dot_left_container);
+        final View right = viewGroup.findViewById(R.id.privacy_dot_right_container);
+        if (!shouldShowPrivacyDot(pos, cutout)) {
+            viewGroup.removeView(left);
+            viewGroup.removeView(right);
+            return;
+        }
+
         switch (pos) {
-            case BOUNDS_POSITION_TOP:
-            case BOUNDS_POSITION_LEFT:
-                View top = LayoutInflater.from(mContext)
-                        .inflate(R.layout.rounded_corners_top, null);
-                mTopLeftDot = top.findViewById(R.id.privacy_dot_left_container);
-                mTopRightDot = top.findViewById(R.id.privacy_dot_right_container);
-                return top;
-            case BOUNDS_POSITION_BOTTOM:
-            case BOUNDS_POSITION_RIGHT:
-                View bottom =  LayoutInflater.from(mContext)
-                        .inflate(R.layout.rounded_corners_bottom, null);
-                mBottomLeftDot = bottom.findViewById(R.id.privacy_dot_left_container);
-                mBottomRightDot = bottom.findViewById(R.id.privacy_dot_right_container);
-                return bottom;
-            default:
-                throw new IllegalArgumentException("Unknown bounds position");
+            case BOUNDS_POSITION_LEFT: {
+                mTopLeftDot = left;
+                mBottomLeftDot = right;
+                break;
+            }
+            case BOUNDS_POSITION_TOP: {
+                mTopLeftDot = left;
+                mTopRightDot = right;
+                break;
+            }
+            case BOUNDS_POSITION_RIGHT: {
+                mTopRightDot = left;
+                mBottomRightDot = right;
+                break;
+            }
+            case BOUNDS_POSITION_BOTTOM: {
+                mBottomLeftDot = left;
+                mBottomRightDot = right;
+                break;
+            }
         }
     }
 
-    private void updateView(@BoundsPosition int pos) {
+    private void updateView(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
         if (mOverlays == null || mOverlays[pos] == null) {
             return;
         }
 
         // update rounded corner view rotation
-        updateRoundedCornerView(pos, R.id.left);
-        updateRoundedCornerView(pos, R.id.right);
+        updateRoundedCornerView(pos, R.id.left, cutout);
+        updateRoundedCornerView(pos, R.id.right, cutout);
         updateRoundedCornerSize(mRoundedDefault, mRoundedDefaultTop, mRoundedDefaultBottom);
+        updateRoundedCornerImageView();
 
         // update cutout view rotation
         if (mCutoutViews != null && mCutoutViews[pos] != null) {
@@ -551,7 +589,8 @@
         }
     }
 
-    private static int getBoundPositionFromRotation(@BoundsPosition int pos, int rotation) {
+    @VisibleForTesting
+    static int getBoundPositionFromRotation(@BoundsPosition int pos, int rotation) {
         return (pos - rotation) < 0
                 ? pos - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH
                 : pos - rotation;
@@ -653,51 +692,43 @@
 
             if (mOverlays != null) {
                 updateLayoutParams();
+                final DisplayCutout cutout = getCutout();
                 for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
                     if (mOverlays[i] == null) {
                         continue;
                     }
-                    updateView(i);
+                    updateView(i, cutout);
                 }
             }
         }
     }
 
-    private void updateStatusBarHeight() {
-        mStatusBarHeightLandscape = mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.status_bar_height_landscape);
-        mStatusBarHeightPortrait = mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.status_bar_height_portrait);
-        mDotViewController.setStatusBarHeights(mStatusBarHeightPortrait, mStatusBarHeightLandscape);
-    }
-
     private void updateRoundedCornerRadii() {
         // We should eventually move to just using the intrinsic size of the drawables since
         // they should be sized to the exact pixels they want to cover. Therefore I'm purposely not
         // upgrading all of the configs to contain (width, height) pairs. Instead assume that a
         // device configured using the single integer config value is okay with drawing the corners
         // as a square
-        final int newRoundedDefault = mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.rounded_corner_radius);
-        final int newRoundedDefaultTop = mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.rounded_corner_radius_top);
-        final int newRoundedDefaultBottom = mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom);
+        final int newRoundedDefault = RoundedCorners.getRoundedCornerRadius(
+                mContext.getResources(), mDisplayUniqueId);
+        final int newRoundedDefaultTop = RoundedCorners.getRoundedCornerTopRadius(
+                mContext.getResources(), mDisplayUniqueId);
+        final int newRoundedDefaultBottom = RoundedCorners.getRoundedCornerBottomRadius(
+                mContext.getResources(), mDisplayUniqueId);
 
         final boolean changed = mRoundedDefault.x != newRoundedDefault
                         || mRoundedDefaultTop.x != newRoundedDefaultTop
                         || mRoundedDefaultBottom.x != newRoundedDefaultBottom;
-
         if (changed) {
             // If config_roundedCornerMultipleRadius set as true, ScreenDecorations respect the
             // (width, height) size of drawable/rounded.xml instead of rounded_corner_radius
             if (mIsRoundedCornerMultipleRadius) {
-                Drawable d =  mContext.getDrawable(R.drawable.rounded);
-                mRoundedDefault.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
-                d =  mContext.getDrawable(R.drawable.rounded_corner_top);
-                mRoundedDefaultTop.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
-                d =  mContext.getDrawable(R.drawable.rounded_corner_bottom);
-                mRoundedDefaultBottom.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
+                mRoundedDefault.set(mRoundedCornerDrawable.getIntrinsicWidth(),
+                        mRoundedCornerDrawable.getIntrinsicHeight());
+                mRoundedDefaultTop.set(mRoundedCornerDrawableTop.getIntrinsicWidth(),
+                        mRoundedCornerDrawableTop.getIntrinsicHeight());
+                mRoundedDefaultBottom.set(mRoundedCornerDrawableBottom.getIntrinsicWidth(),
+                        mRoundedCornerDrawableBottom.getIntrinsicHeight());
             } else {
                 mRoundedDefault.set(newRoundedDefault, newRoundedDefault);
                 mRoundedDefaultTop.set(newRoundedDefaultTop, newRoundedDefaultTop);
@@ -707,13 +738,97 @@
         }
     }
 
-    private void updateRoundedCornerView(@BoundsPosition int pos, int id) {
+    /**
+     * Gets whether the rounded corners are multiple radii for current display.
+     *
+     * Loads the default config {@link R.bool#config_roundedCornerMultipleRadius} if
+     * {@link com.android.internal.R.array#config_displayUniqueIdArray} is not set.
+     */
+    private static boolean isRoundedCornerMultipleRadius(Context context, String displayUniqueId) {
+        final Resources res = context.getResources();
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(
+                R.array.config_roundedCornerMultipleRadiusArray);
+        boolean isMultipleRadius;
+        if (index >= 0 && index < array.length()) {
+            isMultipleRadius = array.getBoolean(index, false);
+        } else {
+            isMultipleRadius = res.getBoolean(R.bool.config_roundedCornerMultipleRadius);
+        }
+        array.recycle();
+        return isMultipleRadius;
+    }
+
+    /**
+     * Gets the rounded corner drawable for current display.
+     *
+     * Loads the default config {@link R.drawable#rounded} if
+     * {@link com.android.internal.R.array#config_displayUniqueIdArray} is not set.
+     */
+    private static Drawable getRoundedCornerDrawable(Context context, String displayUniqueId) {
+        final Resources res = context.getResources();
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_roundedCornerDrawableArray);
+        Drawable drawable;
+        if (index >= 0 && index < array.length()) {
+            drawable = array.getDrawable(index);
+        } else {
+            drawable = context.getDrawable(R.drawable.rounded);
+        }
+        array.recycle();
+        return drawable;
+    }
+
+    /**
+     * Gets the rounded corner top drawable for current display.
+     *
+     * Loads the default config {@link R.drawable#rounded_corner_top} if
+     * {@link com.android.internal.R.array#config_displayUniqueIdArray} is not set.
+     */
+    private static Drawable getRoundedCornerTopDrawable(Context context, String displayUniqueId) {
+        final Resources res = context.getResources();
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(R.array.config_roundedCornerTopDrawableArray);
+        Drawable drawable;
+        if (index >= 0 && index < array.length()) {
+            drawable = array.getDrawable(index);
+        } else {
+            drawable = context.getDrawable(R.drawable.rounded_corner_top);
+        }
+        array.recycle();
+        return drawable;
+    }
+
+    /**
+     * Gets the rounded corner bottom drawable for current display.
+     *
+     * Loads the default config {@link R.drawable#rounded_corner_bottom} if
+     * {@link com.android.internal.R.array#config_displayUniqueIdArray} is not set.
+     */
+    private static Drawable getRoundedCornerBottomDrawable(
+            Context context, String displayUniqueId) {
+        final Resources res = context.getResources();
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(
+                R.array.config_roundedCornerBottomDrawableArray);
+        Drawable drawable;
+        if (index >= 0 && index < array.length()) {
+            drawable = array.getDrawable(index);
+        } else {
+            drawable = context.getDrawable(R.drawable.rounded_corner_bottom);
+        }
+        array.recycle();
+        return drawable;
+    }
+
+    private void updateRoundedCornerView(@BoundsPosition int pos, int id,
+            @Nullable DisplayCutout cutout) {
         final View rounded = mOverlays[pos].findViewById(id);
         if (rounded == null) {
             return;
         }
         rounded.setVisibility(View.GONE);
-        if (shouldShowRoundedCorner(pos)) {
+        if (shouldShowRoundedCorner(pos, cutout)) {
             final int gravity = getRoundedCornerGravity(pos, id == R.id.left);
             ((FrameLayout.LayoutParams) rounded.getLayoutParams()).gravity = gravity;
             setRoundedCornerOrientation(rounded, gravity);
@@ -772,12 +887,8 @@
                 || mIsRoundedCornerMultipleRadius;
     }
 
-    private boolean shouldShowRoundedCorner(@BoundsPosition int pos) {
-        if (!hasRoundedCorners()) {
-            return false;
-        }
-
-        DisplayCutout cutout = getCutout();
+    private boolean isDefaultShownOverlayPos(@BoundsPosition int pos,
+            @Nullable DisplayCutout cutout) {
         // for cutout is null or cutout with only waterfall.
         final boolean emptyBoundsOrWaterfall = cutout == null || cutout.isBoundsEmpty();
         // Shows rounded corner on left and right overlays only when there is no top or bottom
@@ -792,13 +903,28 @@
         }
     }
 
+    private boolean shouldShowRoundedCorner(@BoundsPosition int pos,
+            @Nullable DisplayCutout cutout) {
+        return hasRoundedCorners() && isDefaultShownOverlayPos(pos, cutout);
+    }
+
+    private boolean shouldShowPrivacyDot(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
+        return mIsPrivacyDotEnabled && isDefaultShownOverlayPos(pos, cutout);
+    }
+
+    private boolean shouldShowCutout(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
+        final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll();
+        final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
+        return (bounds != null && !bounds[rotatedPos].isEmpty());
+    }
+
     private boolean shouldDrawCutout() {
         return shouldDrawCutout(mContext);
     }
 
     static boolean shouldDrawCutout(Context context) {
-        return context.getResources().getBoolean(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
+        return DisplayCutout.getFillBuiltInDisplayCutout(
+                context.getResources(), context.getDisplay().getUniqueId());
     }
 
     private void updateLayoutParams() {
@@ -837,6 +963,52 @@
         });
     }
 
+    private void updateRoundedCornerDrawable() {
+        mRoundedCornerDrawable = getRoundedCornerDrawable(mContext, mDisplayUniqueId);
+        mRoundedCornerDrawableTop = getRoundedCornerTopDrawable(mContext, mDisplayUniqueId);
+        mRoundedCornerDrawableBottom = getRoundedCornerBottomDrawable(mContext, mDisplayUniqueId);
+        updateRoundedCornerImageView();
+    }
+
+    private void updateRoundedCornerImageView() {
+        final Drawable top = mRoundedCornerDrawableTop != null
+                ? mRoundedCornerDrawableTop : mRoundedCornerDrawable;
+        final Drawable bottom = mRoundedCornerDrawableBottom != null
+                ? mRoundedCornerDrawableBottom : mRoundedCornerDrawable;
+
+        if (mOverlays == null) {
+            return;
+        }
+        for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
+            if (mOverlays[i] == null) {
+                continue;
+            }
+            ((ImageView) mOverlays[i].findViewById(R.id.left)).setImageDrawable(
+                    isTopRoundedCorner(i, R.id.left) ? top : bottom);
+            ((ImageView) mOverlays[i].findViewById(R.id.right)).setImageDrawable(
+                    isTopRoundedCorner(i, R.id.right) ? top : bottom);
+        }
+    }
+
+    @VisibleForTesting
+    boolean isTopRoundedCorner(@BoundsPosition int pos, int id) {
+        switch (pos) {
+            case BOUNDS_POSITION_LEFT:
+            case BOUNDS_POSITION_RIGHT:
+                if (mRotation == ROTATION_270) {
+                    return id == R.id.left ? false : true;
+                } else {
+                    return id == R.id.left ? true : false;
+                }
+            case BOUNDS_POSITION_TOP:
+                return true;
+            case BOUNDS_POSITION_BOTTOM:
+                return false;
+            default:
+                throw new IllegalArgumentException("Unknown bounds position");
+        }
+    }
+
     private void updateRoundedCornerSize(
             Point sizeDefault,
             Point sizeTop,
@@ -855,21 +1027,10 @@
             if (mOverlays[i] == null) {
                 continue;
             }
-            if (i == BOUNDS_POSITION_LEFT || i == BOUNDS_POSITION_RIGHT) {
-                if (mRotation == ROTATION_270) {
-                    setSize(mOverlays[i].findViewById(R.id.left), sizeBottom);
-                    setSize(mOverlays[i].findViewById(R.id.right), sizeTop);
-                } else {
-                    setSize(mOverlays[i].findViewById(R.id.left), sizeTop);
-                    setSize(mOverlays[i].findViewById(R.id.right), sizeBottom);
-                }
-            } else if (i == BOUNDS_POSITION_TOP) {
-                setSize(mOverlays[i].findViewById(R.id.left), sizeTop);
-                setSize(mOverlays[i].findViewById(R.id.right), sizeTop);
-            } else if (i == BOUNDS_POSITION_BOTTOM) {
-                setSize(mOverlays[i].findViewById(R.id.left), sizeBottom);
-                setSize(mOverlays[i].findViewById(R.id.right), sizeBottom);
-            }
+            setSize(mOverlays[i].findViewById(R.id.left),
+                    isTopRoundedCorner(i, R.id.left) ? sizeTop : sizeBottom);
+            setSize(mOverlays[i].findViewById(R.id.right),
+                    isTopRoundedCorner(i, R.id.right) ? sizeTop : sizeBottom);
         }
     }
 
@@ -1085,7 +1246,8 @@
             int dw = flipped ? lh : lw;
             int dh = flipped ? lw : lh;
 
-            Path path = DisplayCutout.pathFromResources(getResources(), dw, dh);
+            Path path = DisplayCutout.pathFromResources(
+                    getResources(), getDisplay().getUniqueId(), dw, dh);
             if (path != null) {
                 mBoundingPath.set(path);
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 8c63f7b..3555e8d 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -25,6 +25,8 @@
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.PendingIntent;
 import android.content.res.Resources;
 import android.graphics.RectF;
 import android.os.Handler;
@@ -73,6 +75,9 @@
     private final FlingAnimationUtils mFlingAnimationUtils;
     private float mPagingTouchSlop;
     private final float mSlopMultiplier;
+    private int mTouchSlop;
+    private float mTouchSlopMultiplier;
+
     private final Callback mCallback;
     private final int mSwipeDirection;
     private final VelocityTracker mVelocityTracker;
@@ -105,6 +110,10 @@
                     final int y = (int) mDownLocation[1] - mViewOffset[1];
                     mTouchedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
                     ((ExpandableNotificationRow) mTouchedView).doLongClickCallback(x, y);
+
+                    if (isAvailableToDragAndDrop(mTouchedView)) {
+                        mCallback.onLongPressSent(mTouchedView);
+                    }
                 }
             }
         }
@@ -126,6 +135,8 @@
         mVelocityTracker = VelocityTracker.obtain();
         mPagingTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
         mSlopMultiplier = viewConfiguration.getScaledAmbiguousGestureMultiplier();
+        mTouchSlop = viewConfiguration.getScaledTouchSlop();
+        mTouchSlopMultiplier = viewConfiguration.getAmbiguousGestureMultiplier();
 
         // Extra long-press!
         mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);
@@ -297,7 +308,9 @@
                 mIsSwiping = false;
                 mSnappingChild = false;
                 mLongPressSent = false;
+                mCallback.onLongPressSent(null);
                 mVelocityTracker.clear();
+                cancelLongPress();
                 mTouchedView = mCallback.getChildAtPosition(ev);
 
                 if (mTouchedView != null) {
@@ -349,6 +362,7 @@
                 mIsSwiping = false;
                 mTouchedView = null;
                 mLongPressSent = false;
+                mCallback.onLongPressSent(null);
                 mMenuRowIntercepting = false;
                 cancelLongPress();
                 if (captured) return true;
@@ -599,11 +613,7 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        if (mLongPressSent && !mMenuRowIntercepting) {
-            return true;
-        }
-
-        if (!mIsSwiping && !mMenuRowIntercepting) {
+        if (!mIsSwiping && !mMenuRowIntercepting && !mLongPressSent) {
             if (mCallback.getChildAtPosition(ev) != null) {
                 // We are dragging directly over a card, make sure that we also catch the gesture
                 // even if nobody else wants the touch event.
@@ -629,30 +639,40 @@
                     if (absDelta >= getFalsingThreshold()) {
                         mTouchAboveFalsingThreshold = true;
                     }
-                    // don't let items that can't be dismissed be dragged more than
-                    // maxScrollDistance
-                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection(
-                            mTouchedView,
-                            delta > 0)) {
-                        float size = getSize(mTouchedView);
-                        float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size;
-                        if (absDelta >= size) {
-                            delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
-                        } else {
-                            int startPosition = mCallback.getConstrainSwipeStartPosition();
-                            if (absDelta > startPosition) {
-                                int signedStartPosition =
-                                        (int) (startPosition * Math.signum(delta));
-                                delta = signedStartPosition
-                                        + maxScrollDistance * (float) Math.sin(
-                                        ((delta - signedStartPosition) / size) * (Math.PI / 2));
+
+                    if (mLongPressSent) {
+                        if (absDelta >= getTouchSlop(ev)) {
+                            if (mTouchedView instanceof ExpandableNotificationRow) {
+                                ((ExpandableNotificationRow) mTouchedView)
+                                        .doDragCallback(ev.getX(), ev.getY());
                             }
                         }
-                    }
+                    } else {
+                        // don't let items that can't be dismissed be dragged more than
+                        // maxScrollDistance
+                        if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection(
+                                mTouchedView,
+                                delta > 0)) {
+                            float size = getSize(mTouchedView);
+                            float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size;
+                            if (absDelta >= size) {
+                                delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
+                            } else {
+                                int startPosition = mCallback.getConstrainSwipeStartPosition();
+                                if (absDelta > startPosition) {
+                                    int signedStartPosition =
+                                            (int) (startPosition * Math.signum(delta));
+                                    delta = signedStartPosition
+                                            + maxScrollDistance * (float) Math.sin(
+                                            ((delta - signedStartPosition) / size) * (Math.PI / 2));
+                                }
+                            }
+                        }
 
-                    setTranslation(mTouchedView, mTranslation + delta);
-                    updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed);
-                    onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta);
+                        setTranslation(mTouchedView, mTranslation + delta);
+                        updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed);
+                        onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta);
+                    }
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -753,6 +773,29 @@
         mIsSwiping = false;
     }
 
+    private float getTouchSlop(MotionEvent event) {
+        // Adjust the touch slop if another gesture may be being performed.
+        return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
+                ? mTouchSlop * mTouchSlopMultiplier
+                : mTouchSlop;
+    }
+
+    private boolean isAvailableToDragAndDrop(View v) {
+        if (v.getResources().getBoolean(R.bool.config_notificationToContents)) {
+            if (v instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow enr = (ExpandableNotificationRow) v;
+                boolean canBubble = enr.getEntry().canBubble();
+                Notification notif = enr.getEntry().getSbn().getNotification();
+                PendingIntent dragIntent = notif.contentIntent != null ? notif.contentIntent
+                        : notif.fullScreenIntent;
+                if (dragIntent != null && dragIntent.isActivity() && !canBubble) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     public interface Callback {
         View getChildAtPosition(MotionEvent ev);
 
@@ -777,6 +820,13 @@
         void onDragCancelled(View v);
 
         /**
+         * Called when the child is long pressed and available to start drag and drop.
+         *
+         * @param v the view that was long pressed.
+         */
+        void onLongPressSent(View v);
+
+        /**
          * Called when the child is snapped to a position.
          *
          * @param animView the view that was snapped.
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index a28223d..c64f416 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -118,6 +118,7 @@
                     .setTaskViewFactory(mWMComponent.getTaskViewFactory())
                     .setTransitions(mWMComponent.getTransitions())
                     .setStartingSurface(mWMComponent.getStartingSurface())
+                    .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
                     .setTaskSurfaceHelper(mWMComponent.getTaskSurfaceHelper());
         } else {
             // TODO: Call on prepareSysUIComponentBuilder but not with real components. Other option
@@ -133,6 +134,7 @@
                     .setAppPairs(Optional.ofNullable(null))
                     .setTaskViewFactory(Optional.ofNullable(null))
                     .setTransitions(Transitions.createEmptyForTesting())
+                    .setDisplayAreaHelper(Optional.ofNullable(null))
                     .setStartingSurface(Optional.ofNullable(null))
                     .setTaskSurfaceHelper(Optional.ofNullable(null));
         }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
index 9bedb1e..fbb909f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility;
 
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
+import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
 
 import android.annotation.IntDef;
@@ -49,7 +50,8 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
             ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR,
-            ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU
+            ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
+            ACCESSIBILITY_BUTTON_MODE_GESTURE
     })
     public @interface AccessibilityButtonMode {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java
index 17178fa..e521c90 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java
@@ -310,7 +310,8 @@
     }
 
     void onConfigurationChanged(int configDiff) {
-        if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
+        if ((configDiff & (ActivityInfo.CONFIG_ORIENTATION | ActivityInfo.CONFIG_SCREEN_SIZE))
+                != 0) {
             final Rect previousDraggableBounds = new Rect(mDraggableWindowBounds);
             mDraggableWindowBounds.set(getDraggableWindowBounds());
             // Keep the Y position with the same height ratio before the window bounds and
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index ca2c034..2f88291 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -59,6 +59,7 @@
 import com.android.systemui.util.Assert;
 
 import java.util.Locale;
+import java.util.Optional;
 
 import javax.inject.Inject;
 
@@ -139,10 +140,10 @@
     private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
 
     private final SystemActionsBroadcastReceiver mReceiver;
-    private final Recents mRecents;
+    private final Optional<Recents> mRecentsOptional;
     private Locale mLocale;
     private final AccessibilityManager mA11yManager;
-    private final Lazy<StatusBar> mStatusBar;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private final NotificationShadeWindowController mNotificationShadeController;
     private final StatusBarWindowCallback mNotificationShadeCallback;
     private boolean mDismissNotificationShadeActionRegistered;
@@ -150,10 +151,10 @@
     @Inject
     public SystemActions(Context context,
             NotificationShadeWindowController notificationShadeController,
-            Lazy<StatusBar> statusBar,
-            Recents recents) {
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
+            Optional<Recents> recentsOptional) {
         super(context);
-        mRecents = recents;
+        mRecentsOptional = recentsOptional;
         mReceiver = new SystemActionsBroadcastReceiver();
         mLocale = mContext.getResources().getConfiguration().getLocales().get(0);
         mA11yManager = (AccessibilityManager) mContext.getSystemService(
@@ -161,9 +162,9 @@
         mNotificationShadeController = notificationShadeController;
         // Saving in instance variable since to prevent GC since
         // NotificationShadeWindowController.registerCallback() only keeps weak references.
-        mNotificationShadeCallback = (keyguardShowing, keyguardOccluded, bouncerShowing) ->
+        mNotificationShadeCallback = (keyguardShowing, keyguardOccluded, bouncerShowing, mDozing) ->
                 registerOrUnregisterDismissNotificationShadeAction();
-        mStatusBar = statusBar;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
     }
 
     @Override
@@ -242,8 +243,9 @@
 
         // Saving state in instance variable since this callback is called quite often to avoid
         // binder calls
-        StatusBar statusBar = mStatusBar.get();
-        if (statusBar.isPanelExpanded() && !statusBar.isKeyguardShowing()) {
+        final Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
+        if (statusBarOptional.map(StatusBar::isPanelExpanded).orElse(false)
+                && !statusBarOptional.get().isKeyguardShowing()) {
             if (!mDismissNotificationShadeActionRegistered) {
                 mA11yManager.registerSystemAction(
                         createRemoteAction(
@@ -368,15 +370,16 @@
     }
 
     private void handleRecents() {
-        mRecents.toggleRecentApps();
+        mRecentsOptional.ifPresent(Recents::toggleRecentApps);
     }
 
     private void handleNotifications() {
-        mStatusBar.get().animateExpandNotificationsPanel();
+        mStatusBarOptionalLazy.get().ifPresent(StatusBar::animateExpandNotificationsPanel);
     }
 
     private void handleQuickSettings() {
-        mStatusBar.get().animateExpandSettingsPanel(null);
+        mStatusBarOptionalLazy.get().ifPresent(
+                statusBar -> statusBar.animateExpandSettingsPanel(null));
     }
 
     private void handlePowerDialog() {
@@ -425,7 +428,9 @@
     }
 
     private void handleAccessibilityDismissNotificationShade() {
-        mStatusBar.get().animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, false /* force */);
+        mStatusBarOptionalLazy.get().ifPresent(
+                statusBar -> statusBar.animateCollapsePanels(
+                        CommandQueue.FLAG_EXCLUDE_NONE, false /* force */));
     }
 
     private class SystemActionsBroadcastReceiver extends BroadcastReceiver {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
index cee395b..3281347 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
@@ -91,7 +91,7 @@
             final Context windowContext = mContext.createWindowContext(display,
                     TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
             final WindowMagnificationController controller = new WindowMagnificationController(
-                    mContext,
+                    windowContext,
                     mHandler, new SfVsyncFrameCallbackProvider(), null,
                     new SurfaceControl.Transaction(), mWindowMagnifierCallback, mSysUiState);
             return new WindowMagnificationAnimationController(windowContext, controller);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 717c715..0893e89 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -28,6 +28,7 @@
 import android.annotation.UiContext;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.Matrix;
@@ -35,6 +36,7 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Region;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -76,6 +78,8 @@
         MirrorWindowControl.MirrorWindowDelegate, MagnificationGestureDetector.OnGestureListener {
 
     private static final String TAG = "WindowMagnificationController";
+    @SuppressWarnings("isloggabletaglength")
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE;
     // Delay to avoid updating state description too frequently.
     private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100;
     // It should be consistent with the value defined in WindowMagnificationGestureHandler.
@@ -158,14 +162,15 @@
         mRotation = display.getRotation();
 
         mWm = context.getSystemService(WindowManager.class);
-        mWindowBounds = mWm.getCurrentWindowMetrics().getBounds();
+        mWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds());
 
         mResources = mContext.getResources();
         mScale = mResources.getInteger(R.integer.magnification_default_scale);
         mBounceEffectDuration = mResources.getInteger(
                 com.android.internal.R.integer.config_shortAnimTime);
         updateDimensions();
-        setInitialStartBounds();
+        setMagnificationFrameWith(mWindowBounds, mWindowBounds.width() / 2,
+                mWindowBounds.height() / 2);
         computeBounceAnimationScale();
 
         mMirrorWindowControl = mirrorWindowControl;
@@ -286,18 +291,59 @@
      * @param configDiff a bit mask of the differences between the configurations
      */
     void onConfigurationChanged(int configDiff) {
+        if (DEBUG) {
+            Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString(
+                    configDiff));
+        }
+        if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
+            onRotate();
+        }
+
+        if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) {
+            updateAccessibilityWindowTitleIfNeeded();
+        }
+
+        boolean reCreateWindow = false;
         if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) {
             updateDimensions();
             computeBounceAnimationScale();
-            if (isWindowVisible()) {
-                deleteWindowMagnification();
-                enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN);
-            }
-        } else if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
-            onRotate();
-        } else if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) {
-            updateAccessibilityWindowTitleIfNeeded();
+            reCreateWindow = true;
         }
+
+        if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
+            reCreateWindow |= handleScreenSizeChanged();
+        }
+
+        // Recreate the window again to correct the window appearance due to density or
+        // window size changed not caused by rotation.
+        if (isWindowVisible() && reCreateWindow) {
+            deleteWindowMagnification();
+            enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN);
+        }
+    }
+
+    /**
+     * Calculates the magnification frame if the window bounds is changed.
+     * Note that the orientation also changes the wind bounds, so it should be handled first.
+     *
+     * @return {@code true} if the magnification frame is changed with the new window bounds.
+     */
+    private boolean handleScreenSizeChanged() {
+        final Rect oldWindowBounds = new Rect(mWindowBounds);
+        final Rect currentWindowBounds = mWm.getCurrentWindowMetrics().getBounds();
+
+        if (currentWindowBounds.equals(oldWindowBounds)) {
+            if (DEBUG) {
+                Log.d(TAG, "updateMagnificationFrame -- window bounds is not changed");
+            }
+            return false;
+        }
+        mWindowBounds.set(currentWindowBounds);
+        final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width();
+        final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height();
+        setMagnificationFrameWith(mWindowBounds, (int) newCenterX, (int) newCenterY);
+        calculateMagnificationFrameBoundary();
+        return true;
     }
 
     private void updateSystemUIStateIfNeeded() {
@@ -311,30 +357,42 @@
         mWm.updateViewLayout(mMirrorView, params);
     }
 
-    /** Handles MirrorWindow position when the device rotation changed. */
+    /**
+     * Keep MirrorWindow position on the screen unchanged when device rotates 90° clockwise or
+     * anti-clockwise.
+     */
     private void onRotate() {
         final Display display = mContext.getDisplay();
         final int oldRotation = mRotation;
-        mWindowBounds = mWm.getCurrentWindowMetrics().getBounds();
-
-        setMagnificationFrameBoundary();
         mRotation = display.getRotation();
+        final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation);
+        if (rotationDegree == 0 || rotationDegree == 180) {
+            Log.w(TAG, "onRotate -- rotate with the device. skip it");
+            return;
+        }
+        final Rect currentWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds());
+        if (currentWindowBounds.width() != mWindowBounds.height()
+                || currentWindowBounds.height() != mWindowBounds.width()) {
+            Log.w(TAG, "onRotate -- unexpected window height/width");
+            return;
+        }
+
+        mWindowBounds.set(currentWindowBounds);
+
+        calculateMagnificationFrameBoundary();
 
         if (!isWindowVisible()) {
             return;
         }
         // Keep MirrorWindow position on the screen unchanged when device rotates 90°
         // clockwise or anti-clockwise.
-        final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation);
+
         final Matrix matrix = new Matrix();
         matrix.setRotate(rotationDegree);
         if (rotationDegree == 90) {
             matrix.postTranslate(mWindowBounds.width(), 0);
         } else if (rotationDegree == 270) {
             matrix.postTranslate(0, mWindowBounds.height());
-        } else {
-            Log.w(TAG, "Invalid rotation change. " + rotationDegree);
-            return;
         }
         // The rect of MirrorView is going to be transformed.
         LayoutParams params =
@@ -440,12 +498,15 @@
         }
     }
 
-    private void setInitialStartBounds() {
-        // Sets the initial frame area for the mirror and places it in the center of the display.
-        final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 2
-                + 2 * mMirrorSurfaceMargin;
-        final int initX = mWindowBounds.width() / 2 - initSize / 2;
-        final int initY = mWindowBounds.height() / 2 - initSize / 2;
+    private void setMagnificationFrameWith(Rect windowBounds, int centerX, int centerY) {
+        // Sets the initial frame area for the mirror and place it to the given center on the
+        // display.
+        int initSize = Math.min(windowBounds.width(), windowBounds.height()) / 2;
+        initSize = Math.min(mResources.getDimensionPixelSize(R.dimen.magnification_max_frame_size),
+                initSize);
+        initSize += 2 * mMirrorSurfaceMargin;
+        final int initX = centerX - initSize / 2;
+        final int initY = centerY - initSize / 2;
         mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize);
     }
 
@@ -553,7 +614,7 @@
         mSourceBounds.set(left, top, right, bottom);
     }
 
-    private void setMagnificationFrameBoundary() {
+    private void calculateMagnificationFrameBoundary() {
         // Calculates width and height for magnification frame could exceed out the screen.
         // TODO : re-calculating again when scale is changed.
         // The half width of magnification frame.
@@ -644,7 +705,7 @@
                 : centerY - mMagnificationFrame.exactCenterY();
         mScale = Float.isNaN(scale) ? mScale : scale;
 
-        setMagnificationFrameBoundary();
+        calculateMagnificationFrameBoundary();
         updateMagnificationFramePosition((int) offsetX, (int) offsetY);
         if (!isWindowVisible()) {
             createMirrorWindow();
@@ -764,6 +825,8 @@
         pw.println("      mOverlapWithGestureInsets:" + mOverlapWithGestureInsets);
         pw.println("      mScale:" + mScale);
         pw.println("      mMirrorViewBounds:" + (isWindowVisible() ? mMirrorViewBounds : "empty"));
+        pw.println("      mSourceBounds:"
+                 + (isWindowVisible() ? mSourceBounds : "empty"));
         pw.println("      mSystemGestureTop:" + mSystemGestureTop);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
index 05256e6..f182e77 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
@@ -41,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Prefs;
+import com.android.systemui.shared.system.SysUiStatsLog;
 
 /**
  * Contains logic for an accessibility floating menu view.
@@ -177,6 +178,9 @@
     }
 
     private void onDragEnd(Position position) {
+        SysUiStatsLog.write(SysUiStatsLog.ACCESSIBILITY_FLOATING_MENU_UI_CHANGED,
+                position.getPercentageX(), position.getPercentageY(),
+                mContext.getResources().getConfiguration().orientation);
         savePosition(mContext, position);
         showDockTooltipIfNecessary(mContext);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
index 17818cd..59d9aff 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
@@ -19,8 +19,9 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.util.MathUtils.constrain;
 import static android.util.MathUtils.sq;
+import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
 
 import static java.util.Objects.requireNonNull;
 
@@ -41,7 +42,6 @@
 import android.graphics.drawable.LayerDrawable;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.DisplayMetrics;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -95,7 +95,6 @@
     private boolean mIsShowing;
     private boolean mIsDownInEnlargedTouchArea;
     private boolean mIsDragging = false;
-    private boolean mImeVisibility;
     @Alignment
     private int mAlignment;
     @SizeType
@@ -108,8 +107,10 @@
     private int mRadiusType;
     private int mMargin;
     private int mPadding;
-    private int mScreenHeight;
-    private int mScreenWidth;
+    // The display width excludes the window insets of the system bar and display cutout.
+    private int mDisplayHeight;
+    // The display Height excludes the window insets of the system bar and display cutout.
+    private int mDisplayWidth;
     private int mIconWidth;
     private int mIconHeight;
     private int mInset;
@@ -118,6 +119,8 @@
     private int mRelativeToPointerDownX;
     private int mRelativeToPointerDownY;
     private float mRadius;
+    private final Rect mDisplayInsetsRect = new Rect();
+    private final Rect mImeInsetsRect = new Rect();
     private final Position mPosition;
     private float mSquareScaledTouchSlop;
     private final Configuration mLastConfiguration;
@@ -506,9 +509,21 @@
     }
 
     private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
-        final boolean currentImeVisibility = insets.isVisible(ime());
-        if (currentImeVisibility != mImeVisibility) {
-            mImeVisibility = currentImeVisibility;
+        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+        final Rect displayWindowInsetsRect = getDisplayInsets(windowMetrics).toRect();
+        if (!displayWindowInsetsRect.equals(mDisplayInsetsRect)) {
+            updateDisplaySizeWith(windowMetrics);
+            updateLocationWith(mPosition);
+        }
+
+        final Rect imeInsetsRect = windowMetrics.getWindowInsets().getInsets(ime()).toRect();
+        if (!imeInsetsRect.equals(mImeInsetsRect)) {
+            if (isImeVisible(imeInsetsRect)) {
+                mImeInsetsRect.set(imeInsetsRect);
+            } else {
+                mImeInsetsRect.setEmpty();
+            }
+
             updateLocationWith(mPosition);
         }
 
@@ -520,6 +535,11 @@
                 || (side == Alignment.LEFT && downX > currentRawX);
     }
 
+    private boolean isImeVisible(Rect imeInsetsRect) {
+        return imeInsetsRect.left != 0 || imeInsetsRect.top != 0 || imeInsetsRect.right != 0
+                || imeInsetsRect.bottom != 0;
+    }
+
     private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) {
         return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop;
     }
@@ -546,9 +566,9 @@
 
     private void updateDimensions() {
         final Resources res = getResources();
-        final DisplayMetrics dm = res.getDisplayMetrics();
-        mScreenWidth = dm.widthPixels;
-        mScreenHeight = dm.heightPixels;
+
+        updateDisplaySizeWith(mWindowManager.getCurrentWindowMetrics());
+
         mMargin =
                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
         mInset =
@@ -560,6 +580,15 @@
         updateItemViewDimensionsWith(mSizeType);
     }
 
+    private void updateDisplaySizeWith(WindowMetrics metrics) {
+        final Rect displayBounds = metrics.getBounds();
+        final Insets displayInsets = getDisplayInsets(metrics);
+        mDisplayInsetsRect.set(displayInsets.toRect());
+        displayBounds.inset(displayInsets);
+        mDisplayWidth = displayBounds.width();
+        mDisplayHeight = displayBounds.height();
+    }
+
     private void updateItemViewDimensionsWith(@SizeType int sizeType) {
         final Resources res = getResources();
         final int paddingResId =
@@ -684,11 +713,11 @@
     }
 
     private int getMaxWindowX() {
-        return mScreenWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
+        return mDisplayWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
     }
 
     private int getMaxWindowY() {
-        return mScreenHeight - getWindowHeight();
+        return mDisplayHeight - getWindowHeight();
     }
 
     private InstantInsetLayerDrawable getMenuLayerDrawable() {
@@ -699,8 +728,13 @@
         return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
     }
 
+    private Insets getDisplayInsets(WindowMetrics metrics) {
+        return metrics.getWindowInsets().getInsetsIgnoringVisibility(
+                systemBars() | displayCutout());
+    }
+
     /**
-     * Updates the floating menu to be fixed at the side of the screen.
+     * Updates the floating menu to be fixed at the side of the display.
      */
     private void updateLocationWith(Position position) {
         final @Alignment int alignment = transformToAlignment(position.getPercentageX());
@@ -716,15 +750,9 @@
      * @return the moving interval if they overlap each other, otherwise 0.
      */
     private int getInterval() {
-        if (!mImeVisibility) {
-            return 0;
-        }
-
-        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
-        final Insets imeInsets = windowMetrics.getWindowInsets().getInsets(
-                ime() | navigationBars());
-        final int imeY = mScreenHeight - imeInsets.bottom;
-        final int layoutBottomY = mCurrentLayoutParams.y + getWindowHeight();
+        final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
+        final int imeY = mDisplayHeight - mImeInsetsRect.bottom;
+        final int layoutBottomY = currentLayoutY + getWindowHeight();
 
         return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0;
     }
@@ -855,11 +883,12 @@
 
     @VisibleForTesting
     Rect getAvailableBounds() {
-        return new Rect(0, 0, mScreenWidth - getWindowWidth(), mScreenHeight - getWindowHeight());
+        return new Rect(0, 0, mDisplayWidth - getWindowWidth(),
+                mDisplayHeight - getWindowHeight());
     }
 
     private int getMaxLayoutHeight() {
-        return mScreenHeight - mMargin * 2;
+        return mDisplayHeight - mMargin * 2;
     }
 
     private int getLayoutWidth() {
@@ -875,7 +904,7 @@
     }
 
     private int getWindowHeight() {
-        return Math.min(mScreenHeight, mMargin * 2 + getLayoutHeight());
+        return Math.min(mDisplayHeight, mMargin * 2 + getLayoutHeight());
     }
 
     private void setSystemGestureExclusion() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpan.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpan.java
index 0d7551f..d8e80fe 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpan.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpan.java
@@ -30,7 +30,7 @@
 /**
  * A span that turns the text wrapped by annotation tag into the clickable link text.
  */
-public class AnnotationLinkSpan extends ClickableSpan {
+class AnnotationLinkSpan extends ClickableSpan {
     private final Optional<View.OnClickListener> mClickListener;
 
     private AnnotationLinkSpan(View.OnClickListener listener) {
@@ -50,7 +50,7 @@
      * @param linkInfos used to attach the click action into the corresponding span
      * @return the text attached with the span
      */
-    public static CharSequence linkify(CharSequence text, LinkInfo... linkInfos) {
+    static CharSequence linkify(CharSequence text, LinkInfo... linkInfos) {
         final SpannableString msg = new SpannableString(text);
         final Annotation[] spans =
                 msg.getSpans(/* queryStart= */ 0, msg.length(), Annotation.class);
@@ -78,12 +78,12 @@
     /**
      * Data class to store the annotation and the click action.
      */
-    public static class LinkInfo {
-        public static final String DEFAULT_ANNOTATION = "link";
+    static class LinkInfo {
+        static final String DEFAULT_ANNOTATION = "link";
         private final Optional<String> mAnnotation;
         private final Optional<View.OnClickListener> mListener;
 
-        public LinkInfo(@NonNull String annotation, View.OnClickListener listener) {
+        LinkInfo(@NonNull String annotation, View.OnClickListener listener) {
             mAnnotation = Optional.of(annotation);
             mListener = Optional.ofNullable(listener);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java b/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java
index 169a9c0..f13730e 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java
@@ -70,7 +70,7 @@
     };
 
     private final Context mContext;
-    private final Optional<Lazy<StatusBar>> mStatusBarOptionalLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private final StatusBarStateController mStatusBarStateController;
 
     private boolean mLauncherShowing;
@@ -78,7 +78,7 @@
 
     @Inject
     PhoneStateMonitor(Context context, BroadcastDispatcher broadcastDispatcher,
-            Optional<Lazy<StatusBar>> statusBarOptionalLazy, BootCompleteCache bootCompleteCache) {
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy, BootCompleteCache bootCompleteCache) {
         mContext = context;
         mStatusBarOptionalLazy = statusBarOptionalLazy;
         mStatusBarStateController = Dependency.get(StatusBarStateController.class);
@@ -180,8 +180,7 @@
     }
 
     private boolean isBouncerShowing() {
-        return mStatusBarOptionalLazy.map(
-                statusBarLazy -> statusBarLazy.get().isBouncerShowing()).orElse(false);
+        return mStatusBarOptionalLazy.get().map(StatusBar::isBouncerShowing).orElse(false);
     }
 
     private boolean isKeyguardLocked() {
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
rename to packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index deceb95..39088c3 100644
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,64 +13,51 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui;
+package com.android.systemui.battery;
 
 import static android.provider.Settings.System.SHOW_BATTERY_PERCENT;
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
-import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
 import android.annotation.IntDef;
-import android.app.ActivityManager;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
-import android.database.ContentObserver;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Handler;
+import android.os.UserHandle;
 import android.provider.Settings;
-import android.text.TextUtils;
-import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.LayoutInflater;
-import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import androidx.annotation.StyleRes;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.settingslib.graph.ThemedBatteryDrawable;
+import com.android.systemui.DualToneHandler;
+import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.settings.CurrentUserTracker;
-import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
-import com.android.systemui.tuner.TunerService;
-import com.android.systemui.tuner.TunerService.Tunable;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.text.NumberFormat;
 
-public class BatteryMeterView extends LinearLayout implements
-        BatteryStateChangeCallback, Tunable, DarkReceiver, ConfigurationListener {
-
+public class BatteryMeterView extends LinearLayout implements DarkReceiver {
 
     @Retention(SOURCE)
     @IntDef({MODE_DEFAULT, MODE_ON, MODE_OFF, MODE_ESTIMATE})
@@ -81,21 +68,14 @@
     public static final int MODE_ESTIMATE = 3;
 
     private final ThemedBatteryDrawable mDrawable;
-    private final String mSlotBattery;
     private final ImageView mBatteryIconView;
-    private final CurrentUserTracker mUserTracker;
     private TextView mBatteryPercentView;
 
-    private BatteryController mBatteryController;
-    private SettingObserver mSettingObserver;
     private final @StyleRes int mPercentageStyleId;
     private int mTextColor;
     private int mLevel;
     private int mShowPercentMode = MODE_DEFAULT;
     private boolean mShowPercentAvailable;
-    // Some places may need to show the battery conditionally, and not obey the tuner
-    private boolean mIgnoreTunerUpdates;
-    private boolean mIsSubscribedForTunerUpdates;
     private boolean mCharging;
     // Error state where we know nothing about the current battery state
     private boolean mBatteryStateUnknown;
@@ -103,19 +83,19 @@
     private Drawable mUnknownStateDrawable;
 
     private DualToneHandler mDualToneHandler;
-    private int mUser;
 
     private int mNonAdaptedSingleToneColor;
     private int mNonAdaptedForegroundColor;
     private int mNonAdaptedBackgroundColor;
 
+    private BatteryEstimateFetcher mBatteryEstimateFetcher;
+
     public BatteryMeterView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
     public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        BroadcastDispatcher broadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
 
         setOrientation(LinearLayout.HORIZONTAL);
         setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
@@ -128,14 +108,11 @@
         mDrawable = new ThemedBatteryDrawable(context, frameColor);
         atts.recycle();
 
-        mSettingObserver = new SettingObserver(new Handler(context.getMainLooper()));
         mShowPercentAvailable = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_battery_percentage_setting_available);
 
         setupLayoutTransition();
 
-        mSlotBattery = context.getString(
-                com.android.internal.R.string.status_bar_battery);
         mBatteryIconView = new ImageView(context);
         mBatteryIconView.setImageDrawable(mDrawable);
         final MarginLayoutParams mlp = new MarginLayoutParams(
@@ -150,21 +127,8 @@
         // Init to not dark at all.
         onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
 
-        mUserTracker = new CurrentUserTracker(broadcastDispatcher) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                mUser = newUserId;
-                getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
-                getContext().getContentResolver().registerContentObserver(
-                        Settings.System.getUriFor(SHOW_BATTERY_PERCENT), false, mSettingObserver,
-                        newUserId);
-                updateShowPercent();
-            }
-        };
-
         setClipChildren(false);
         setClipToPadding(false);
-        Dependency.get(ConfigurationController.class).observe(viewAttachLifecycle(this), this);
     }
 
     private void setupLayoutTransition() {
@@ -192,6 +156,7 @@
      * 0 - No preference
      * 1 - Force on
      * 2 - Force off
+     * 3 - Estimate
      * @param mode desired mode (none, on, off)
      */
     public void setPercentShowMode(@BatteryPercentMode int mode) {
@@ -200,42 +165,10 @@
         updateShowPercent();
     }
 
-    /**
-     * Set {@code true} to turn off BatteryMeterView's subscribing to the tuner for updates, and
-     * thus avoid it controlling its own visibility
-     *
-     * @param ignore whether to ignore the tuner or not
-     */
-    public void setIgnoreTunerUpdates(boolean ignore) {
-        mIgnoreTunerUpdates = ignore;
-        updateTunerSubscription();
-    }
-
-    private void updateTunerSubscription() {
-        if (mIgnoreTunerUpdates) {
-            unsubscribeFromTunerUpdates();
-        } else {
-            subscribeForTunerUpdates();
-        }
-    }
-
-    private void subscribeForTunerUpdates() {
-        if (mIsSubscribedForTunerUpdates || mIgnoreTunerUpdates) {
-            return;
-        }
-
-        Dependency.get(TunerService.class)
-                .addTunable(this, StatusBarIconController.ICON_HIDE_LIST);
-        mIsSubscribedForTunerUpdates = true;
-    }
-
-    private void unsubscribeFromTunerUpdates() {
-        if (!mIsSubscribedForTunerUpdates) {
-            return;
-        }
-
-        Dependency.get(TunerService.class).removeTunable(this);
-        mIsSubscribedForTunerUpdates = false;
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updatePercentView();
     }
 
     public void setColorsFromContext(Context context) {
@@ -251,42 +184,7 @@
         return false;
     }
 
-    @Override
-    public void onTuningChanged(String key, String newValue) {
-        if (StatusBarIconController.ICON_HIDE_LIST.equals(key)) {
-            ArraySet<String> icons = StatusBarIconController.getIconHideList(
-                    getContext(), newValue);
-            setVisibility(icons.contains(mSlotBattery) ? View.GONE : View.VISIBLE);
-        }
-    }
-
-    @Override
-    public void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mBatteryController = Dependency.get(BatteryController.class);
-        mBatteryController.addCallback(this);
-        mUser = ActivityManager.getCurrentUser();
-        getContext().getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(SHOW_BATTERY_PERCENT), false, mSettingObserver, mUser);
-        getContext().getContentResolver().registerContentObserver(
-                Settings.Global.getUriFor(Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME),
-                false, mSettingObserver);
-        updateShowPercent();
-        subscribeForTunerUpdates();
-        mUserTracker.startTracking();
-    }
-
-    @Override
-    public void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        mUserTracker.stopTracking();
-        mBatteryController.removeCallback(this);
-        getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
-        unsubscribeFromTunerUpdates();
-    }
-
-    @Override
-    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+    void onBatteryLevelChanged(int level, boolean pluggedIn) {
         mDrawable.setCharging(pluggedIn);
         mDrawable.setBatteryLevel(level);
         mCharging = pluggedIn;
@@ -294,8 +192,7 @@
         updatePercentText();
     }
 
-    @Override
-    public void onPowerSaveChanged(boolean isPowerSave) {
+    void onPowerSaveChanged(boolean isPowerSave) {
         mDrawable.setPowerSaveEnabled(isPowerSave);
     }
 
@@ -315,19 +212,28 @@
         updateShowPercent();
     }
 
-    private void updatePercentText() {
+    /**
+     * Sets the fetcher that should be used to get the estimated time remaining for the user's
+     * battery.
+     */
+    void setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher) {
+        mBatteryEstimateFetcher = fetcher;
+    }
+
+    void updatePercentText() {
         if (mBatteryStateUnknown) {
             setContentDescription(getContext().getString(R.string.accessibility_battery_unknown));
             return;
         }
 
-        if (mBatteryController == null) {
+        if (mBatteryEstimateFetcher == null) {
             return;
         }
 
         if (mBatteryPercentView != null) {
             if (mShowPercentMode == MODE_ESTIMATE && !mCharging) {
-                mBatteryController.getEstimatedTimeRemainingString((String estimate) -> {
+                mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate(
+                        (String estimate) -> {
                     if (mBatteryPercentView == null) {
                         return;
                     }
@@ -361,12 +267,12 @@
                         : R.string.accessibility_battery_level, mLevel));
     }
 
-    private void updateShowPercent() {
+    void updateShowPercent() {
         final boolean showing = mBatteryPercentView != null;
         // TODO(b/140051051)
         final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System
                 .getIntForUser(getContext().getContentResolver(),
-                SHOW_BATTERY_PERCENT, 0, mUser));
+                SHOW_BATTERY_PERCENT, 0, UserHandle.USER_CURRENT));
         boolean shouldShow =
                 (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF)
                 || mShowPercentMode == MODE_ON
@@ -394,11 +300,6 @@
         }
     }
 
-    @Override
-    public void onDensityOrFontScaleChanged() {
-        scaleBatteryMeterViews();
-    }
-
     private Drawable getUnknownStateDrawable() {
         if (mUnknownStateDrawable == null) {
             mUnknownStateDrawable = mContext.getDrawable(R.drawable.ic_battery_unknown);
@@ -408,8 +309,7 @@
         return mUnknownStateDrawable;
     }
 
-    @Override
-    public void onBatteryUnknownStateChanged(boolean isUnknown) {
+    void onBatteryUnknownStateChanged(boolean isUnknown) {
         if (mBatteryStateUnknown == isUnknown) {
             return;
         }
@@ -428,7 +328,7 @@
     /**
      * Looks up the scale factor for status bar icons and scales the battery view by that amount.
      */
-    private void scaleBatteryMeterViews() {
+    void scaleBatteryMeterViews() {
         Resources res = getContext().getResources();
         TypedValue typedValue = new TypedValue();
 
@@ -489,20 +389,15 @@
         pw.println("    mMode: " + mShowPercentMode);
     }
 
-    private final class SettingObserver extends ContentObserver {
-        public SettingObserver(Handler handler) {
-            super(handler);
-        }
+    @VisibleForTesting
+    CharSequence getBatteryPercentViewText() {
+        return mBatteryPercentView.getText();
+    }
 
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            super.onChange(selfChange, uri);
-            updateShowPercent();
-            if (TextUtils.equals(uri.getLastPathSegment(),
-                    Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME)) {
-                // update the text for sure if the estimate in the cache was updated
-                updatePercentText();
-            }
-        }
+    /** An interface that will fetch the estimated time remaining for the user's battery. */
+    public interface BatteryEstimateFetcher {
+        void fetchBatteryTimeRemainingEstimate(
+                BatteryController.EstimateFetchCompletion completion);
     }
 }
+
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
new file mode 100644
index 0000000..ae9a323
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.battery;
+
+import static android.provider.Settings.System.SHOW_BATTERY_PERCENT;
+
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.View;
+
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.ViewController;
+
+import javax.inject.Inject;
+
+/** Controller for {@link BatteryMeterView}. **/
+public class BatteryMeterViewController extends ViewController<BatteryMeterView> {
+    private final ConfigurationController mConfigurationController;
+    private final TunerService mTunerService;
+    private final ContentResolver mContentResolver;
+    private final BatteryController mBatteryController;
+
+    private final String mSlotBattery;
+    private final SettingObserver mSettingObserver;
+    private final CurrentUserTracker mCurrentUserTracker;
+
+    private final ConfigurationController.ConfigurationListener mConfigurationListener =
+            new ConfigurationController.ConfigurationListener() {
+                @Override
+                public void onDensityOrFontScaleChanged() {
+                    mView.scaleBatteryMeterViews();
+                }
+            };
+
+    private final TunerService.Tunable mTunable = new TunerService.Tunable() {
+        @Override
+        public void onTuningChanged(String key, String newValue) {
+            if (StatusBarIconController.ICON_HIDE_LIST.equals(key)) {
+                ArraySet<String> icons = StatusBarIconController.getIconHideList(
+                        getContext(), newValue);
+                mView.setVisibility(icons.contains(mSlotBattery) ? View.GONE : View.VISIBLE);
+            }
+        }
+    };
+
+    private final BatteryController.BatteryStateChangeCallback mBatteryStateChangeCallback =
+            new BatteryController.BatteryStateChangeCallback() {
+                @Override
+                public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+                    mView.onBatteryLevelChanged(level, pluggedIn);
+                }
+
+                @Override
+                public void onPowerSaveChanged(boolean isPowerSave) {
+                    mView.onPowerSaveChanged(isPowerSave);
+                }
+
+                @Override
+                public void onBatteryUnknownStateChanged(boolean isUnknown) {
+                    mView.onBatteryUnknownStateChanged(isUnknown);
+                }
+            };
+
+    // Some places may need to show the battery conditionally, and not obey the tuner
+    private boolean mIgnoreTunerUpdates;
+    private boolean mIsSubscribedForTunerUpdates;
+
+    @Inject
+    public BatteryMeterViewController(
+            BatteryMeterView view,
+            ConfigurationController configurationController,
+            TunerService tunerService,
+            BroadcastDispatcher broadcastDispatcher,
+            @Main Handler mainHandler,
+            ContentResolver contentResolver,
+            BatteryController batteryController) {
+        super(view);
+        mConfigurationController = configurationController;
+        mTunerService = tunerService;
+        mContentResolver = contentResolver;
+        mBatteryController = batteryController;
+
+        mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
+
+        mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
+        mSettingObserver = new SettingObserver(mainHandler);
+        mCurrentUserTracker = new CurrentUserTracker(broadcastDispatcher) {
+            @Override
+            public void onUserSwitched(int newUserId) {
+                contentResolver.unregisterContentObserver(mSettingObserver);
+                registerShowBatteryPercentObserver(newUserId);
+                mView.updateShowPercent();
+            }
+        };
+    }
+
+    @Override
+    protected void onViewAttached() {
+        mConfigurationController.addCallback(mConfigurationListener);
+        subscribeForTunerUpdates();
+        mBatteryController.addCallback(mBatteryStateChangeCallback);
+
+        registerShowBatteryPercentObserver(ActivityManager.getCurrentUser());
+        registerGlobalBatteryUpdateObserver();
+        mCurrentUserTracker.startTracking();
+
+        mView.updateShowPercent();
+    }
+
+    @Override
+    protected void onViewDetached() {
+        mConfigurationController.removeCallback(mConfigurationListener);
+        unsubscribeFromTunerUpdates();
+        mBatteryController.removeCallback(mBatteryStateChangeCallback);
+
+        mCurrentUserTracker.stopTracking();
+        mContentResolver.unregisterContentObserver(mSettingObserver);
+    }
+
+    /**
+     * Turn off {@link BatteryMeterView}'s subscribing to the tuner for updates, and thus avoid it
+     * controlling its own visibility.
+     */
+    public void ignoreTunerUpdates() {
+        mIgnoreTunerUpdates = true;
+        unsubscribeFromTunerUpdates();
+    }
+
+    private void subscribeForTunerUpdates() {
+        if (mIsSubscribedForTunerUpdates || mIgnoreTunerUpdates) {
+            return;
+        }
+
+        mTunerService.addTunable(mTunable, StatusBarIconController.ICON_HIDE_LIST);
+        mIsSubscribedForTunerUpdates = true;
+    }
+
+    private void unsubscribeFromTunerUpdates() {
+        if (!mIsSubscribedForTunerUpdates) {
+            return;
+        }
+
+        mTunerService.removeTunable(mTunable);
+        mIsSubscribedForTunerUpdates = false;
+    }
+
+    private void registerShowBatteryPercentObserver(int user) {
+        mContentResolver.registerContentObserver(
+                Settings.System.getUriFor(SHOW_BATTERY_PERCENT),
+                false,
+                mSettingObserver,
+                user);
+    }
+
+    private void registerGlobalBatteryUpdateObserver() {
+        mContentResolver.registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME),
+                false,
+                mSettingObserver);
+    }
+
+    private final class SettingObserver extends ContentObserver {
+        public SettingObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            super.onChange(selfChange, uri);
+            mView.updateShowPercent();
+            if (TextUtils.equals(uri.getLastPathSegment(),
+                    Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME)) {
+                // update the text for sure if the estimate in the cache was updated
+                mView.updatePercentText();
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
index c32c1a2..48f6431 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.biometrics;
 
-import android.annotation.NonNull;
 import android.content.Context;
 import android.graphics.drawable.Animatable2;
 import android.graphics.drawable.AnimatedVectorDrawable;
@@ -30,6 +29,7 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -163,6 +163,18 @@
     }
 
     @Nullable @VisibleForTesting IconController mFaceIconController;
+    @NonNull private final OnAttachStateChangeListener mOnAttachStateChangeListener =
+            new OnAttachStateChangeListener() {
+        @Override
+        public void onViewAttachedToWindow(View v) {
+
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View v) {
+            mFaceIconController.deactivate();
+        }
+    };
 
     public AuthBiometricFaceView(Context context) {
         this(context, null);
@@ -181,6 +193,8 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mFaceIconController = new IconController(mContext, mIconView, mIndicatorView);
+
+        addOnAttachStateChangeListener(mOnAttachStateChangeListener);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 60b0637..f11dc93 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -824,11 +824,25 @@
         return new AuthDialog.LayoutParams(width, totalHeight);
     }
 
+    private boolean isLargeDisplay() {
+        return com.android.systemui.util.Utils.shouldUseSplitNotificationShade(getResources());
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         final int width = MeasureSpec.getSize(widthMeasureSpec);
         final int height = MeasureSpec.getSize(heightMeasureSpec);
-        final int newWidth = Math.min(width, height);
+
+        final boolean isLargeDisplay = isLargeDisplay();
+
+        final int newWidth;
+        if (isLargeDisplay) {
+            // TODO(b/201811580): Unless we can come up with a one-size-fits-all equation, we may
+            //  want to consider moving this to an overlay.
+            newWidth = 2 * Math.min(width, height) / 3;
+        } else {
+            newWidth = Math.min(width, height);
+        }
 
         // Use "newWidth" instead, so the landscape dialog width is the same as the portrait
         // width.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 0790af9..f4b446b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -29,6 +29,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.PointF;
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
 import android.hardware.biometrics.BiometricConstants;
@@ -97,7 +98,7 @@
     private final Provider<UdfpsController> mUdfpsControllerFactory;
     private final Provider<SidefpsController> mSidefpsControllerFactory;
     @Nullable private final PointF mFaceAuthSensorLocation;
-    @Nullable private final PointF mFingerprintLocation;
+    @Nullable private PointF mFingerprintLocation;
     private final Set<Callback> mCallbacks = new HashSet<>();
 
     // TODO: These should just be saved from onSaveState
@@ -114,7 +115,7 @@
     @VisibleForTesting
     IBiometricSysuiReceiver mReceiver;
     @VisibleForTesting
-    @NonNull final BiometricOrientationEventListener mOrientationListener;
+    @NonNull final BiometricDisplayListener mOrientationListener;
     @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
     @Nullable private List<FingerprintSensorPropertiesInternal> mFpProps;
     @Nullable private List<FingerprintSensorPropertiesInternal> mUdfpsProps;
@@ -459,13 +460,15 @@
         mSidefpsControllerFactory = sidefpsControllerFactory;
         mWindowManager = windowManager;
         mUdfpsEnrolledForUser = new SparseBooleanArray();
-        mOrientationListener = new BiometricOrientationEventListener(context,
+        mOrientationListener = new BiometricDisplayListener(
+                context,
+                displayManager,
+                handler,
+                BiometricDisplayListener.SensorType.Generic.INSTANCE,
                 () -> {
                     onOrientationChanged();
                     return Unit.INSTANCE;
-                },
-                displayManager,
-                handler);
+                });
 
         mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null;
 
@@ -479,9 +482,7 @@
                     (float) faceAuthLocation[1]);
         }
 
-        mFingerprintLocation = new PointF(DisplayUtils.getWidth(mContext) / 2,
-                mContext.getResources().getDimensionPixelSize(
-                com.android.systemui.R.dimen.physical_fingerprint_sensor_center_screen_location_y));
+        updateFingerprintLocation();
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
@@ -489,6 +490,21 @@
         context.registerReceiver(mBroadcastReceiver, filter);
     }
 
+    private void updateFingerprintLocation() {
+        int xLocation = DisplayUtils.getWidth(mContext) / 2;
+        try {
+            xLocation = mContext.getResources().getDimensionPixelSize(
+                    com.android.systemui.R.dimen
+                            .physical_fingerprint_sensor_center_screen_location_x);
+        } catch (Resources.NotFoundException e) {
+        }
+        int yLocation = mContext.getResources().getDimensionPixelSize(
+                com.android.systemui.R.dimen.physical_fingerprint_sensor_center_screen_location_y);
+        mFingerprintLocation = new PointF(
+                xLocation,
+                yLocation);
+    }
+
     @SuppressWarnings("deprecation")
     @Override
     public void start() {
@@ -765,6 +781,7 @@
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
+        updateFingerprintLocation();
 
         // Save the state of the current dialog (buttons showing, etc)
         if (mCurrentDialog != null) {
@@ -794,6 +811,7 @@
     }
 
     private void onOrientationChanged() {
+        updateFingerprintLocation();
         if (mCurrentDialog != null) {
             mCurrentDialog.onOrientationChanged();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index fa50f89..f1e42e0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -117,24 +117,6 @@
         mUseFullScreen = fullScreen;
     }
 
-    public ValueAnimator getTranslationAnimator(float relativeTranslationY) {
-        final ValueAnimator animator = ValueAnimator.ofFloat(
-                mPanelView.getY(), mPanelView.getY() - relativeTranslationY);
-        animator.addUpdateListener(animation -> {
-            final float translation = (float) animation.getAnimatedValue();
-            mPanelView.setTranslationY(translation);
-        });
-        return animator;
-    }
-
-    public ValueAnimator getAlphaAnimator(float alpha) {
-        final ValueAnimator animator = ValueAnimator.ofFloat(mPanelView.getAlpha(), alpha);
-        animator.addUpdateListener(animation -> {
-            mPanelView.setAlpha((float) animation.getAnimatedValue());
-        });
-        return animator;
-    }
-
     public void updateForContentDimensions(int contentWidth, int contentHeight,
             int animateDurationMs) {
         if (DEBUG) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 2630f119..8b04bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -18,9 +18,9 @@
 
 import android.animation.ValueAnimator
 import android.content.Context
-import android.content.res.Configuration
 import android.graphics.PointF
 import android.hardware.biometrics.BiometricSourceType
+import android.util.DisplayMetrics
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -45,6 +45,7 @@
 import javax.inject.Inject
 import javax.inject.Provider
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.util.leak.RotationUtils
 
 private const val WAKE_AND_UNLOCK_FADE_DURATION = 180L
 
@@ -123,6 +124,7 @@
             return
         }
 
+        updateSensorLocation()
         if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
             fingerprintSensorLocation != null) {
             mView.setSensorLocation(fingerprintSensorLocation!!)
@@ -182,7 +184,7 @@
     }
 
     fun updateSensorLocation() {
-        fingerprintSensorLocation = authController.fingerprintSensorLocation
+        updateFingerprintLocation()
         faceSensorLocation = authController.faceAuthSensorLocation
         fingerprintSensorLocation?.let {
             circleReveal = CircleReveal(
@@ -197,6 +199,35 @@
         }
     }
 
+    private fun updateFingerprintLocation() {
+        val displayMetrics = DisplayMetrics()
+        sysuiContext.display?.getRealMetrics(displayMetrics)
+        val width = displayMetrics.widthPixels
+        val height = displayMetrics.heightPixels
+
+        authController.fingerprintSensorLocation?.let {
+            fingerprintSensorLocation = when (RotationUtils.getRotation(sysuiContext)) {
+                RotationUtils.ROTATION_LANDSCAPE -> {
+                    val normalizedYPos: Float = it.y / width
+                    val normalizedXPos: Float = it.x / height
+                    PointF(width * normalizedYPos, height * (1 - normalizedXPos))
+                }
+                RotationUtils.ROTATION_UPSIDE_DOWN -> {
+                    PointF(width - it.x, height - it.y)
+                }
+                RotationUtils.ROTATION_SEASCAPE -> {
+                    val normalizedYPos: Float = it.y / width
+                    val normalizedXPos: Float = it.x / height
+                    PointF(width * (1 - normalizedYPos), height * normalizedXPos)
+                }
+                else -> {
+                    // ROTATION_NONE
+                    PointF(it.x, it.y)
+                }
+            }
+        }
+    }
+
     private fun updateRippleColor() {
         mView.setColor(
             Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor)
@@ -235,18 +266,12 @@
 
     private val configurationChangedListener =
         object : ConfigurationController.ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                updateSensorLocation()
-            }
             override fun onUiModeChanged() {
                 updateRippleColor()
             }
             override fun onThemeChanged() {
                 updateRippleColor()
             }
-            override fun onOverlayChanged() {
-                updateRippleColor()
-            }
     }
 
     private val udfpsControllerCallback =
@@ -275,7 +300,7 @@
     private fun updateUdfpsDependentParams() {
         authController.udfpsProps?.let {
             if (it.size > 0) {
-                udfpsRadius = it[0].sensorRadius.toFloat()
+                udfpsRadius = it[0].location.sensorRadius.toFloat()
                 udfpsController = udfpsControllerProvider.get()
 
                 if (mView.isAttachedToWindow) {
@@ -314,10 +339,12 @@
                         }
                     }
                     "fingerprint" -> {
+                        updateSensorLocation()
                         pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation")
                         showRipple(BiometricSourceType.FINGERPRINT)
                     }
                     "face" -> {
+                        updateSensorLocation()
                         pw.println("face ripple sensorLocation=$faceSensorLocation")
                         showRipple(BiometricSourceType.FACE)
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt
new file mode 100644
index 0000000..b7404df
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.os.Handler
+import android.view.Surface
+import com.android.systemui.biometrics.BiometricDisplayListener.SensorType.Generic
+
+/**
+ * A listener for keeping overlays for biometric sensors aligned with the physical device
+ * device's screen. The [onChanged] will be dispatched on the [handler]
+ * whenever a relevant change to the device's configuration (orientation, fold, display change,
+ * etc.) may require the UI to change for the given [sensorType].
+ */
+class BiometricDisplayListener(
+    private val context: Context,
+    private val displayManager: DisplayManager,
+    private val handler: Handler,
+    private val sensorType: SensorType = SensorType.Generic,
+    private val onChanged: () -> Unit
+) : DisplayManager.DisplayListener {
+
+    private var lastRotation = context.display?.rotation ?: Surface.ROTATION_0
+
+    override fun onDisplayAdded(displayId: Int) {}
+    override fun onDisplayRemoved(displayId: Int) {}
+    override fun onDisplayChanged(displayId: Int) {
+        val rotationChanged = didRotationChange()
+
+        when (sensorType) {
+            is SensorType.SideFingerprint -> onChanged()
+            else -> {
+                if (rotationChanged) {
+                    onChanged()
+                }
+            }
+        }
+    }
+
+    private fun didRotationChange(): Boolean {
+        val rotation = context.display?.rotation ?: return false
+        val last = lastRotation
+        lastRotation = rotation
+        return last != rotation
+    }
+
+    /** Listen for changes. */
+    fun enable() {
+        displayManager.registerDisplayListener(this, handler)
+    }
+
+    /** Stop listening for changes. */
+    fun disable() {
+        displayManager.unregisterDisplayListener(this)
+    }
+
+    /**
+     * Type of sensor to determine what kind of display changes require layouts.
+     *
+     * The [Generic] type should be used in cases where the modality can vary, such as
+     * biometric prompt (and this object will likely change as multi-mode auth is added).
+     */
+    sealed class SensorType {
+        object Generic : SensorType()
+        data class UnderDisplayFingerprint(
+            val properties: FingerprintSensorPropertiesInternal
+        ) : SensorType()
+        data class SideFingerprint(
+            val properties: FingerprintSensorPropertiesInternal
+        ) : SensorType()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricOrientationEventListener.kt b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricOrientationEventListener.kt
deleted file mode 100644
index 98a03a1..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricOrientationEventListener.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics
-
-import android.content.Context
-import android.hardware.display.DisplayManager
-import android.os.Handler
-import android.view.OrientationEventListener
-import android.view.Surface
-
-/**
- * An [OrientationEventListener] that invokes the [onOrientationChanged] callback whenever
- * the orientation of the device has changed in order to keep overlays for biometric sensors
- * aligned with the device's screen.
- */
-class BiometricOrientationEventListener(
-    private val context: Context,
-    private val onOrientationChanged: () -> Unit,
-    private val displayManager: DisplayManager,
-    private val handler: Handler
-) : DisplayManager.DisplayListener {
-
-    private var lastRotation = context.display?.rotation ?: Surface.ROTATION_0
-
-    override fun onDisplayAdded(displayId: Int) {}
-    override fun onDisplayRemoved(displayId: Int) {}
-    override fun onDisplayChanged(displayId: Int) {
-        val rotation = context.display?.rotation ?: return
-        if (lastRotation != rotation) {
-            lastRotation = rotation
-
-            onOrientationChanged()
-        }
-    }
-
-    fun enable() {
-        displayManager.registerDisplayListener(this, handler)
-    }
-
-    fun disable() {
-        displayManager.unregisterDisplayListener(this)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS b/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS
index 947466f..adb10f0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS
@@ -1,3 +1,4 @@
 set noparent
 
 include /services/core/java/com/android/server/biometrics/OWNERS
+beverlyt@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.java
deleted file mode 100644
index 8f6e249..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics;
-
-import static com.android.internal.util.Preconditions.checkArgument;
-import static com.android.internal.util.Preconditions.checkNotNull;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.PixelFormat;
-import android.hardware.display.DisplayManager;
-import android.hardware.fingerprint.FingerprintManager;
-import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
-import android.hardware.fingerprint.ISidefpsController;
-import android.os.Handler;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.Surface;
-import android.view.WindowManager;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.R;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-
-import javax.inject.Inject;
-
-import kotlin.Unit;
-
-/**
- * Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events.
- */
-@SysUISingleton
-public class SidefpsController {
-    private static final String TAG = "SidefpsController";
-    @NonNull private final Context mContext;
-    @NonNull private final LayoutInflater mInflater;
-    private final FingerprintManager mFingerprintManager;
-    private final WindowManager mWindowManager;
-    private final DelayableExecutor mFgExecutor;
-    @VisibleForTesting @NonNull final BiometricOrientationEventListener mOrientationListener;
-
-    // TODO: update mDisplayHeight and mDisplayWidth for multi-display devices
-    private final int mDisplayHeight;
-    private final int mDisplayWidth;
-
-    private boolean mIsVisible = false;
-    @Nullable private SidefpsView mView;
-
-    static final int SFPS_AFFORDANCE_WIDTH = 50; // in default portrait mode
-
-    @NonNull
-    private final ISidefpsController mSidefpsControllerImpl = new ISidefpsController.Stub() {
-        @Override
-        public void show() {
-            mFgExecutor.execute(() -> {
-                SidefpsController.this.show();
-                mIsVisible = true;
-            });
-        }
-
-        @Override
-        public void hide() {
-            mFgExecutor.execute(() -> {
-                SidefpsController.this.hide();
-                mIsVisible = false;
-            });
-        }
-    };
-
-    @VisibleForTesting
-    final FingerprintSensorPropertiesInternal mSensorProps;
-    private final WindowManager.LayoutParams mCoreLayoutParams;
-
-    @Inject
-    public SidefpsController(@NonNull Context context,
-            @NonNull LayoutInflater inflater,
-            @Nullable FingerprintManager fingerprintManager,
-            @NonNull WindowManager windowManager,
-            @Main DelayableExecutor fgExecutor,
-            @NonNull DisplayManager displayManager,
-            @Main Handler handler) {
-        mContext = context;
-        mInflater = inflater;
-        mFingerprintManager = checkNotNull(fingerprintManager);
-        mWindowManager = windowManager;
-        mFgExecutor = fgExecutor;
-        mOrientationListener = new BiometricOrientationEventListener(
-                context,
-                () -> {
-                    onOrientationChanged();
-                    return Unit.INSTANCE;
-                },
-                displayManager,
-                handler);
-
-        mSensorProps = findFirstSidefps();
-        checkArgument(mSensorProps != null);
-
-        mCoreLayoutParams = new WindowManager.LayoutParams(
-                WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
-                getCoreLayoutParamFlags(),
-                PixelFormat.TRANSLUCENT);
-        mCoreLayoutParams.setTitle(TAG);
-        // Overrides default, avoiding status bars during layout
-        mCoreLayoutParams.setFitInsetsTypes(0);
-        mCoreLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
-        mCoreLayoutParams.layoutInDisplayCutoutMode =
-                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-        mCoreLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
-
-        DisplayMetrics displayMetrics = new DisplayMetrics();
-        windowManager.getDefaultDisplay().getMetrics(displayMetrics);
-        mDisplayHeight = displayMetrics.heightPixels;
-        mDisplayWidth = displayMetrics.widthPixels;
-
-        mFingerprintManager.setSidefpsController(mSidefpsControllerImpl);
-    }
-
-    private void show() {
-        mView = (SidefpsView) mInflater.inflate(R.layout.sidefps_view, null, false);
-        mView.setSensorProperties(mSensorProps);
-        mWindowManager.addView(mView, computeLayoutParams());
-
-        mOrientationListener.enable();
-    }
-
-    private void hide() {
-        if (mView != null) {
-            mWindowManager.removeView(mView);
-            mView.setOnTouchListener(null);
-            mView.setOnHoverListener(null);
-            mView = null;
-        } else {
-            Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden");
-        }
-
-        mOrientationListener.disable();
-    }
-
-    private void onOrientationChanged() {
-        // If mView is null or if view is hidden, then return.
-        if (mView == null || !mIsVisible) {
-            return;
-        }
-
-        // If the overlay needs to be displayed with a new configuration, destroy the current
-        // overlay, and re-create and show the overlay with the updated LayoutParams.
-        hide();
-        show();
-    }
-
-    @Nullable
-    private FingerprintSensorPropertiesInternal findFirstSidefps() {
-        for (FingerprintSensorPropertiesInternal props :
-                mFingerprintManager.getSensorPropertiesInternal()) {
-            if (props.isAnySidefpsType()) {
-                // TODO(b/188690214): L155-L173 can be removed once sensorLocationX,
-                //  sensorLocationY, and sensorRadius are defined in sensorProps by the HAL
-                int sensorLocationX = 25;
-                int sensorLocationY = 610;
-                int sensorRadius = 112;
-
-                FingerprintSensorPropertiesInternal tempProps =
-                        new FingerprintSensorPropertiesInternal(
-                                props.sensorId,
-                                props.sensorStrength,
-                                props.maxEnrollmentsPerUser,
-                                props.componentInfo,
-                                props.sensorType,
-                                props.resetLockoutRequiresHardwareAuthToken,
-                                sensorLocationX,
-                                sensorLocationY,
-                                sensorRadius
-                        );
-                props = tempProps;
-                return props;
-            }
-        }
-        return null;
-    }
-
-    private int getCoreLayoutParamFlags() {
-        return WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
-                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
-    }
-
-    /**
-     * Computes layout params depending on orientation & folding configuration of device
-     */
-    private WindowManager.LayoutParams computeLayoutParams() {
-        mCoreLayoutParams.flags = getCoreLayoutParamFlags();
-        // Y value of top of affordance in portrait mode, X value of left of affordance in landscape
-        int sfpsLocationY = mSensorProps.sensorLocationY - mSensorProps.sensorRadius;
-        int sfpsAffordanceHeight = mSensorProps.sensorRadius * 2;
-
-        // Calculate coordinates of drawable area for the fps affordance, accounting for orientation
-        switch (mContext.getDisplay().getRotation()) {
-            case Surface.ROTATION_90:
-                mCoreLayoutParams.x = sfpsLocationY;
-                mCoreLayoutParams.y = 0;
-                mCoreLayoutParams.height = SFPS_AFFORDANCE_WIDTH;
-                mCoreLayoutParams.width = sfpsAffordanceHeight;
-                break;
-            case Surface.ROTATION_270:
-                mCoreLayoutParams.x = mDisplayHeight - sfpsLocationY - sfpsAffordanceHeight;
-                mCoreLayoutParams.y = mDisplayWidth - SFPS_AFFORDANCE_WIDTH;
-                mCoreLayoutParams.height = SFPS_AFFORDANCE_WIDTH;
-                mCoreLayoutParams.width = sfpsAffordanceHeight;
-                break;
-            default: // Portrait
-                mCoreLayoutParams.x = mDisplayWidth - SFPS_AFFORDANCE_WIDTH;
-                mCoreLayoutParams.y = sfpsLocationY;
-                mCoreLayoutParams.height = sfpsAffordanceHeight;
-                mCoreLayoutParams.width = SFPS_AFFORDANCE_WIDTH;
-        }
-        return mCoreLayoutParams;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
new file mode 100644
index 0000000..b7398d8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Rect
+import android.hardware.biometrics.BiometricOverlayConstants
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.display.DisplayManager
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.hardware.fingerprint.ISidefpsController
+import android.os.Handler
+import android.util.Log
+import android.view.Display
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.Surface
+import android.view.View
+import android.view.WindowManager
+import androidx.annotation.RawRes
+import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.model.KeyPath
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.concurrency.DelayableExecutor
+import javax.inject.Inject
+
+private const val TAG = "SidefpsController"
+
+/**
+ * Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events.
+ */
+@SysUISingleton
+class SidefpsController @Inject constructor(
+    private val context: Context,
+    private val layoutInflater: LayoutInflater,
+    fingerprintManager: FingerprintManager?,
+    private val windowManager: WindowManager,
+    @Main mainExecutor: DelayableExecutor,
+    displayManager: DisplayManager,
+    @Main handler: Handler
+) {
+    @VisibleForTesting
+    val sensorProps: FingerprintSensorPropertiesInternal = fingerprintManager
+        ?.sensorPropertiesInternal
+        ?.firstOrNull { it.isAnySidefpsType }
+        ?: throw IllegalStateException("no side fingerprint sensor")
+
+    @VisibleForTesting
+    val orientationListener = BiometricDisplayListener(
+        context,
+        displayManager,
+        handler,
+        BiometricDisplayListener.SensorType.SideFingerprint(sensorProps)
+    ) { onOrientationChanged() }
+
+    private var overlayView: View? = null
+        set(value) {
+            field?.let { oldView ->
+                windowManager.removeView(oldView)
+                orientationListener.disable()
+            }
+            field = value
+            field?.let { newView ->
+                windowManager.addView(newView, overlayViewParams)
+                orientationListener.enable()
+            }
+        }
+
+    private val overlayViewParams = WindowManager.LayoutParams(
+        WindowManager.LayoutParams.WRAP_CONTENT,
+        WindowManager.LayoutParams.WRAP_CONTENT,
+        WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
+        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+        PixelFormat.TRANSLUCENT
+    ).apply {
+        title = TAG
+        fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
+        gravity = Gravity.TOP or Gravity.LEFT
+        layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+        privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+    }
+
+    init {
+        fingerprintManager?.setSidefpsController(object : ISidefpsController.Stub() {
+            override fun show(
+                sensorId: Int,
+                @BiometricOverlayConstants.ShowReason reason: Int
+            ) = if (reason.isReasonToShow()) doShow() else hide(sensorId)
+
+            private fun doShow() = mainExecutor.execute {
+                if (overlayView == null) {
+                    overlayView = createOverlayForDisplay()
+                } else {
+                    Log.v(TAG, "overlay already shown")
+                }
+            }
+
+            override fun hide(sensorId: Int) = mainExecutor.execute { overlayView = null }
+        })
+    }
+
+    private fun onOrientationChanged() {
+        if (overlayView != null) {
+            overlayView = createOverlayForDisplay()
+        }
+    }
+
+    private fun createOverlayForDisplay(): View {
+        val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
+        val display = context.display!!
+
+        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
+        lottie.setAnimation(display.asSideFpsAnimation())
+        view.rotation = display.asSideFpsAnimationRotation()
+
+        updateOverlayParams(display, lottie.composition?.bounds ?: Rect())
+        lottie.addLottieOnCompositionLoadedListener {
+            if (overlayView == view) {
+                updateOverlayParams(display, it.bounds)
+                windowManager.updateViewLayout(overlayView, overlayViewParams)
+            }
+        }
+        lottie.addOverlayDynamicColor(context)
+
+        return view
+    }
+
+    private fun updateOverlayParams(display: Display, bounds: Rect) {
+        val isPortrait = display.isPortrait()
+        val size = windowManager.maximumWindowMetrics.bounds
+        val displayWidth = if (isPortrait) size.width() else size.height()
+        val displayHeight = if (isPortrait) size.height() else size.width()
+        val offsets = sensorProps.getLocation(display.uniqueId).let { location ->
+            if (location == null) {
+                Log.w(TAG, "No location specified for display: ${display.uniqueId}")
+            }
+            location ?: sensorProps.location
+        }
+
+        // ignore sensorLocationX and sensorRadius since it's assumed to be on the side
+        // of the device and centered at sensorLocationY
+        val (x, y) = when (display.rotation) {
+            Surface.ROTATION_90 ->
+                Pair(offsets.sensorLocationY, 0)
+            Surface.ROTATION_270 ->
+                Pair(displayHeight - offsets.sensorLocationY - bounds.width(), displayWidth)
+            Surface.ROTATION_180 ->
+                Pair(0, displayHeight - offsets.sensorLocationY - bounds.height())
+            else ->
+                Pair(displayWidth, offsets.sensorLocationY)
+        }
+        overlayViewParams.x = x
+        overlayViewParams.y = y
+    }
+}
+
+@BiometricOverlayConstants.ShowReason
+private fun Int.isReasonToShow(): Boolean = when (this) {
+    REASON_AUTH_KEYGUARD -> false
+    else -> true
+}
+
+@RawRes
+private fun Display.asSideFpsAnimation(): Int = when (rotation) {
+    Surface.ROTATION_0 -> R.raw.sfps_pulse
+    Surface.ROTATION_180 -> R.raw.sfps_pulse
+    else -> R.raw.sfps_pulse_landscape
+}
+
+private fun Display.asSideFpsAnimationRotation(): Float = when (rotation) {
+    Surface.ROTATION_180 -> 180f
+    Surface.ROTATION_270 -> 180f
+    else -> 0f
+}
+
+private fun Display.isPortrait(): Boolean =
+    rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+
+private fun LottieAnimationView.addOverlayDynamicColor(context: Context) {
+    fun update() {
+        val c = context.getColor(R.color.biometric_dialog_accent)
+        for (key in listOf(".blue600", ".blue400")) {
+            addValueCallback(
+                KeyPath(key, "**"),
+                LottieProperty.COLOR_FILTER
+            ) { PorterDuffColorFilter(c, PorterDuff.Mode.SRC_ATOP) }
+        }
+    }
+
+    if (composition != null) {
+        update()
+    } else {
+        addLottieOnCompositionLoadedListener { update() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsView.java
deleted file mode 100644
index 4fc59d6..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsView.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics;
-
-import static com.android.systemui.biometrics.SidefpsController.SFPS_AFFORDANCE_WIDTH;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
-import android.util.AttributeSet;
-import android.view.Surface;
-import android.widget.FrameLayout;
-
-/**
- * A view containing a normal drawable view for sidefps events.
- */
-public class SidefpsView extends FrameLayout {
-    private static final String TAG = "SidefpsView";
-    private static final int POINTER_SIZE_PX = 50;
-    private static final int ROUND_RADIUS = 15;
-
-    @NonNull private final RectF mSensorRect;
-    @NonNull private final Paint mSensorRectPaint;
-    @NonNull private final Paint mPointerText;
-    @NonNull private final Context mContext;
-
-    // Used to obtain the sensor location.
-    @NonNull private FingerprintSensorPropertiesInternal mSensorProps;
-    @Surface.Rotation private int mOrientation;
-
-    public SidefpsView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        super.setWillNotDraw(false);
-        mContext = context;
-        mPointerText = new Paint(0 /* flags */);
-        mPointerText.setAntiAlias(true);
-        mPointerText.setColor(Color.WHITE);
-        mPointerText.setTextSize(POINTER_SIZE_PX);
-
-        mSensorRect = new RectF();
-        mSensorRectPaint = new Paint(0 /* flags */);
-        mSensorRectPaint.setAntiAlias(true);
-        mSensorRectPaint.setColor(Color.BLUE); // TODO: Fix Color
-        mSensorRectPaint.setStyle(Paint.Style.FILL);
-    }
-
-    void setSensorProperties(@NonNull FingerprintSensorPropertiesInternal properties) {
-        mSensorProps = properties;
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-        canvas.drawRoundRect(mSensorRect, ROUND_RADIUS, ROUND_RADIUS, mSensorRectPaint);
-        int x, y;
-        if (mOrientation == Surface.ROTATION_90 || mOrientation == Surface.ROTATION_270) {
-            x = mSensorProps.sensorRadius + 10;
-            y = SFPS_AFFORDANCE_WIDTH / 2 + 15;
-        } else {
-            x = SFPS_AFFORDANCE_WIDTH / 2 - 10;
-            y = mSensorProps.sensorRadius + 30;
-        }
-        canvas.drawText(
-                ">",
-                x,
-                y,
-                mPointerText
-        );
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        mOrientation = mContext.getDisplay().getRotation();
-        if (mOrientation == Surface.ROTATION_90 || mOrientation == Surface.ROTATION_270) {
-            right = mSensorProps.sensorRadius * 2;
-            bottom = SFPS_AFFORDANCE_WIDTH;
-        } else {
-            right = SFPS_AFFORDANCE_WIDTH;
-            bottom = mSensorProps.sensorRadius * 2;
-        }
-
-        mSensorRect.set(
-                0,
-                0,
-                right,
-                bottom);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java
index b7344fb..a2e55c0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java
@@ -28,6 +28,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.Optional;
 
 /**
  * Handles:
@@ -42,7 +43,7 @@
 abstract class UdfpsAnimationViewController<T extends UdfpsAnimationView>
         extends ViewController<T> implements Dumpable {
     @NonNull final StatusBarStateController mStatusBarStateController;
-    @NonNull final StatusBar mStatusBar;
+    @NonNull final Optional<StatusBar> mStatusBarOptional;
     @NonNull final DumpManager mDumpManger;
 
     boolean mNotificationShadeExpanded;
@@ -50,11 +51,11 @@
     protected UdfpsAnimationViewController(
             T view,
             @NonNull StatusBarStateController statusBarStateController,
-            @NonNull StatusBar statusBar,
+            @NonNull Optional<StatusBar> statusBarOptional,
             @NonNull DumpManager dumpManager) {
         super(view);
         mStatusBarStateController = statusBarStateController;
-        mStatusBar = statusBar;
+        mStatusBarOptional = statusBarOptional;
         mDumpManger = dumpManager;
     }
 
@@ -62,13 +63,17 @@
 
     @Override
     protected void onViewAttached() {
-        mStatusBar.addExpansionChangedListener(mStatusBarExpansionChangedListener);
+        mStatusBarOptional.ifPresent(
+                statusBar -> statusBar.addExpansionChangedListener(
+                        mStatusBarExpansionChangedListener));
         mDumpManger.registerDumpable(getDumpTag(), this);
     }
 
     @Override
     protected void onViewDetached() {
-        mStatusBar.removeExpansionChangedListener(mStatusBarExpansionChangedListener);
+        mStatusBarOptional.ifPresent(
+                statusBar -> statusBar.removeExpansionChangedListener(
+                        mStatusBarExpansionChangedListener));
         mDumpManger.unregisterDumpable(getDumpTag());
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.java
index 93d80e2..85955e1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.java
@@ -22,6 +22,8 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.StatusBar;
 
+import java.util.Optional;
+
 /**
  * Class that coordinates non-HBM animations for biometric prompt.
  */
@@ -29,9 +31,9 @@
     protected UdfpsBpViewController(
             @NonNull UdfpsBpView view,
             @NonNull StatusBarStateController statusBarStateController,
-            @NonNull StatusBar statusBar,
+            @NonNull Optional<StatusBar> statusBarOptional,
             @NonNull DumpManager dumpManager) {
-        super(view, statusBarStateController, statusBar, dumpManager);
+        super(view, statusBarStateController, statusBarOptional, dumpManager);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 420ae53..069a1b2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.biometrics;
 
-import static android.hardware.fingerprint.IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD;
+import static android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
 
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.internal.util.Preconditions.checkNotNull;
@@ -32,6 +32,8 @@
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.RectF;
+import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.display.DisplayManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
@@ -110,7 +112,7 @@
     @NonNull private final LayoutInflater mInflater;
     private final WindowManager mWindowManager;
     private final DelayableExecutor mFgExecutor;
-    @NonNull private final StatusBar mStatusBar;
+    @NonNull private final Optional<StatusBar> mStatusBarOptional;
     @NonNull private final StatusBarStateController mStatusBarStateController;
     @NonNull private final KeyguardStateController mKeyguardStateController;
     @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager;
@@ -127,7 +129,7 @@
     @NonNull private final SystemClock mSystemClock;
     @NonNull private final UnlockedScreenOffAnimationController
             mUnlockedScreenOffAnimationController;
-    @VisibleForTesting @NonNull final BiometricOrientationEventListener mOrientationListener;
+    @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener;
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
     // sensors, this, in addition to a lot of the code here, will be updated.
     @VisibleForTesting final FingerprintSensorPropertiesInternal mSensorProps;
@@ -240,8 +242,8 @@
                 @NonNull IUdfpsOverlayControllerCallback callback) {
             mFgExecutor.execute(() -> {
                 final UdfpsEnrollHelper enrollHelper;
-                if (reason == IUdfpsOverlayController.REASON_ENROLL_FIND_SENSOR
-                        || reason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING) {
+                if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
+                        || reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING) {
                     enrollHelper = new UdfpsEnrollHelper(mContext, mFingerprintManager, reason);
                 } else {
                     enrollHelper = null;
@@ -254,6 +256,13 @@
         @Override
         public void hideUdfpsOverlay(int sensorId) {
             mFgExecutor.execute(() -> {
+                if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
+                    // if we get here, we expect keyguardUpdateMonitor's fingerprintRunningState
+                    // to be updated shortly afterwards
+                    Log.d(TAG, "hiding udfps overlay when "
+                            + "mKeyguardUpdateMonitor.isFingerprintDetectionRunning()=true");
+                }
+
                 mServerRequest = null;
                 updateOverlay();
             });
@@ -319,7 +328,7 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             if (mServerRequest != null
-                    && mServerRequest.mRequestReason != REASON_AUTH_FPM_KEYGUARD
+                    && mServerRequest.mRequestReason != REASON_AUTH_KEYGUARD
                     && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
                 Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, mRequestReason: "
                         + mServerRequest.mRequestReason);
@@ -512,7 +521,7 @@
             @NonNull WindowManager windowManager,
             @NonNull StatusBarStateController statusBarStateController,
             @Main DelayableExecutor fgExecutor,
-            @NonNull StatusBar statusBar,
+            @NonNull Optional<StatusBar> statusBarOptional,
             @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             @NonNull DumpManager dumpManager,
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -541,7 +550,7 @@
         mFingerprintManager = checkNotNull(fingerprintManager);
         mWindowManager = windowManager;
         mFgExecutor = fgExecutor;
-        mStatusBar = statusBar;
+        mStatusBarOptional = statusBarOptional;
         mStatusBarStateController = statusBarStateController;
         mKeyguardStateController = keyguardStateController;
         mKeyguardViewManager = statusBarKeyguardViewManager;
@@ -554,14 +563,6 @@
         mHbmProvider = hbmProvider.orElse(null);
         screenLifecycle.addObserver(mScreenObserver);
         mScreenOn = screenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON;
-        mOrientationListener = new BiometricOrientationEventListener(
-                context,
-                () -> {
-                    onOrientationChanged();
-                    return Unit.INSTANCE;
-                },
-                displayManager,
-                mainHandler);
         mKeyguardBypassController = keyguardBypassController;
         mConfigurationController = configurationController;
         mSystemClock = systemClock;
@@ -570,6 +571,15 @@
         mSensorProps = findFirstUdfps();
         // At least one UDFPS sensor exists
         checkArgument(mSensorProps != null);
+        mOrientationListener = new BiometricDisplayListener(
+                context,
+                displayManager,
+                mainHandler,
+                new BiometricDisplayListener.SensorType.UnderDisplayFingerprint(mSensorProps),
+                () -> {
+                    onOrientationChanged();
+                    return Unit.INSTANCE;
+                });
 
         mCoreLayoutParams = new WindowManager.LayoutParams(
                 WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
@@ -639,10 +649,11 @@
         // on lockscreen and for the udfps light reveal animation on keyguard.
         // Keyguard is only shown in portrait mode for now, so this will need to
         // be updated if that ever changes.
-        return new RectF(mSensorProps.sensorLocationX - mSensorProps.sensorRadius,
-                mSensorProps.sensorLocationY - mSensorProps.sensorRadius,
-                mSensorProps.sensorLocationX + mSensorProps.sensorRadius,
-                mSensorProps.sensorLocationY + mSensorProps.sensorRadius);
+        final SensorLocationInternal location = mSensorProps.getLocation();
+        return new RectF(location.sensorLocationX - location.sensorRadius,
+                location.sensorLocationY - location.sensorRadius,
+                location.sensorLocationX + location.sensorRadius,
+                location.sensorLocationY + location.sensorRadius);
     }
 
     private void updateOverlay() {
@@ -680,10 +691,11 @@
         }
 
         // Default dimensions assume portrait mode.
-        mCoreLayoutParams.x = mSensorProps.sensorLocationX - mSensorProps.sensorRadius - paddingX;
-        mCoreLayoutParams.y = mSensorProps.sensorLocationY - mSensorProps.sensorRadius - paddingY;
-        mCoreLayoutParams.height = 2 * mSensorProps.sensorRadius + 2 * paddingX;
-        mCoreLayoutParams.width = 2 * mSensorProps.sensorRadius + 2 * paddingY;
+        final SensorLocationInternal location = mSensorProps.getLocation();
+        mCoreLayoutParams.x = location.sensorLocationX - location.sensorRadius - paddingX;
+        mCoreLayoutParams.y = location.sensorLocationY - location.sensorRadius - paddingY;
+        mCoreLayoutParams.height = 2 * location.sensorRadius + 2 * paddingX;
+        mCoreLayoutParams.width = 2 * location.sensorRadius + 2 * paddingY;
 
         Point p = new Point();
         // Gets the size based on the current rotation of the display.
@@ -698,9 +710,9 @@
                 } else {
                     Log.v(TAG, "rotate udfps location ROTATION_90");
                 }
-                mCoreLayoutParams.x = mSensorProps.sensorLocationY - mSensorProps.sensorRadius
+                mCoreLayoutParams.x = location.sensorLocationY - location.sensorRadius
                         - paddingX;
-                mCoreLayoutParams.y = p.y - mSensorProps.sensorLocationX - mSensorProps.sensorRadius
+                mCoreLayoutParams.y = p.y - location.sensorLocationX - location.sensorRadius
                         - paddingY;
                 break;
 
@@ -711,9 +723,9 @@
                 } else {
                     Log.v(TAG, "rotate udfps location ROTATION_270");
                 }
-                mCoreLayoutParams.x = p.x - mSensorProps.sensorLocationY - mSensorProps.sensorRadius
+                mCoreLayoutParams.x = p.x - location.sensorLocationY - location.sensorRadius
                         - paddingX;
-                mCoreLayoutParams.y = mSensorProps.sensorLocationX - mSensorProps.sensorRadius
+                mCoreLayoutParams.y = location.sensorLocationX - location.sensorRadius
                         - paddingY;
                 break;
 
@@ -763,9 +775,9 @@
 
                 // This view overlaps the sensor area, so prevent it from being selectable
                 // during a11y.
-                if (reason == IUdfpsOverlayController.REASON_ENROLL_FIND_SENSOR
-                        || reason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING
-                        || reason == IUdfpsOverlayController.REASON_AUTH_BP) {
+                if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
+                        || reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING
+                        || reason == BiometricOverlayConstants.REASON_AUTH_BP) {
                     mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
                 }
 
@@ -783,8 +795,8 @@
 
     private UdfpsAnimationViewController inflateUdfpsAnimation(int reason) {
         switch (reason) {
-            case IUdfpsOverlayController.REASON_ENROLL_FIND_SENSOR:
-            case IUdfpsOverlayController.REASON_ENROLL_ENROLLING:
+            case BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR:
+            case BiometricOverlayConstants.REASON_ENROLL_ENROLLING:
                 UdfpsEnrollView enrollView = (UdfpsEnrollView) mInflater.inflate(
                         R.layout.udfps_enroll_view, null);
                 mView.addView(enrollView);
@@ -793,17 +805,17 @@
                         enrollView,
                         mServerRequest.mEnrollHelper,
                         mStatusBarStateController,
-                        mStatusBar,
+                        mStatusBarOptional,
                         mDumpManager
                 );
-            case IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD:
+            case BiometricOverlayConstants.REASON_AUTH_KEYGUARD:
                 UdfpsKeyguardView keyguardView = (UdfpsKeyguardView)
                         mInflater.inflate(R.layout.udfps_keyguard_view, null);
                 mView.addView(keyguardView);
                 return new UdfpsKeyguardViewController(
                         keyguardView,
                         mStatusBarStateController,
-                        mStatusBar,
+                        mStatusBarOptional,
                         mKeyguardViewManager,
                         mKeyguardUpdateMonitor,
                         mDumpManager,
@@ -814,24 +826,24 @@
                         mUnlockedScreenOffAnimationController,
                         this
                 );
-            case IUdfpsOverlayController.REASON_AUTH_BP:
+            case BiometricOverlayConstants.REASON_AUTH_BP:
                 // note: empty controller, currently shows no visual affordance
                 UdfpsBpView bpView = (UdfpsBpView) mInflater.inflate(R.layout.udfps_bp_view, null);
                 mView.addView(bpView);
                 return new UdfpsBpViewController(
                         bpView,
                         mStatusBarStateController,
-                        mStatusBar,
+                        mStatusBarOptional,
                         mDumpManager
                 );
-            case IUdfpsOverlayController.REASON_AUTH_FPM_OTHER:
+            case BiometricOverlayConstants.REASON_AUTH_OTHER:
                 UdfpsFpmOtherView authOtherView = (UdfpsFpmOtherView)
                         mInflater.inflate(R.layout.udfps_fpm_other_view, null);
                 mView.addView(authOtherView);
                 return new UdfpsFpmOtherViewController(
                         authOtherView,
                         mStatusBarStateController,
-                        mStatusBar,
+                        mStatusBarOptional,
                         mDumpManager
                 );
             default:
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
index 7ccfb86..6cc8acf 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.graphics.Insets;
 import android.graphics.Rect;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.util.Log;
 import android.view.Surface;
@@ -93,7 +94,7 @@
         // Go through each of the children and do the custom measurement.
         int totalHeight = 0;
         final int numChildren = mView.getChildCount();
-        final int sensorDiameter = mSensorProps.sensorRadius * 2;
+        final int sensorDiameter = mSensorProps.getLocation().sensorRadius * 2;
         for (int i = 0; i < numChildren; i++) {
             final View child = mView.getChildAt(i);
             if (child.getId() == R.id.biometric_icon_frame) {
@@ -160,7 +161,7 @@
         final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape(
                 mSensorProps, displayWidth, dialogMargin, horizontalInset);
 
-        final int sensorDiameter = mSensorProps.sensorRadius * 2;
+        final int sensorDiameter = mSensorProps.getLocation().sensorRadius * 2;
         final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth;
 
         int remeasuredHeight = 0;
@@ -257,10 +258,10 @@
             @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx,
             int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx,
             int navbarBottomInsetPx) {
-
+        final SensorLocationInternal location = sensorProperties.getLocation();
         final int sensorDistanceFromBottom = displayHeightPx
-                - sensorProperties.sensorLocationY
-                - sensorProperties.sensorRadius;
+                - location.sensorLocationY
+                - location.sensorRadius;
 
         final int spacerHeight = sensorDistanceFromBottom
                 - textIndicatorHeightPx
@@ -318,10 +319,10 @@
     static int calculateHorizontalSpacerWidthForLandscape(
             @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx,
             int dialogMarginPx, int navbarHorizontalInsetPx) {
-
+        final SensorLocationInternal location = sensorProperties.getLocation();
         final int sensorDistanceFromEdge = displayWidthPx
-                - sensorProperties.sensorLocationY
-                - sensorProperties.sensorRadius;
+                - location.sensorLocationY
+                - location.sensorRadius;
 
         final int horizontalPadding = sensorDistanceFromEdge
                 - dialogMarginPx
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
index caec1d5..d5c763d3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
@@ -20,8 +20,8 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.PointF;
+import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.fingerprint.FingerprintManager;
-import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Build;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -131,7 +131,7 @@
     }
 
     boolean shouldShowProgressBar() {
-        return mEnrollReason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING;
+        return mEnrollReason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
     }
 
     void onEnrollmentProgress(int remaining) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
index 64b0968..729838e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
@@ -63,7 +63,7 @@
 
     void updateSensorLocation(@NonNull FingerprintSensorPropertiesInternal sensorProps) {
         View fingerprintAccessibilityView = findViewById(R.id.udfps_enroll_accessibility_view);
-        final int sensorHeight = sensorProps.sensorRadius * 2;
+        final int sensorHeight = sensorProps.getLocation().sensorRadius * 2;
         final int sensorWidth = sensorHeight;
         ViewGroup.LayoutParams params = fingerprintAccessibilityView.getLayoutParams();
         params.width = sensorWidth;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 6cdd1c8b..af7c352 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -24,6 +24,8 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.StatusBar;
 
+import java.util.Optional;
+
 /**
  * Class that coordinates non-HBM animations during enrollment.
  */
@@ -53,9 +55,9 @@
             @NonNull UdfpsEnrollView view,
             @NonNull UdfpsEnrollHelper enrollHelper,
             @NonNull StatusBarStateController statusBarStateController,
-            @NonNull StatusBar statusBar,
+            @NonNull Optional<StatusBar> statusBarOptional,
             @NonNull DumpManager dumpManager) {
-        super(view, statusBarStateController, statusBar, dumpManager);
+        super(view, statusBarStateController, statusBarOptional, dumpManager);
         mEnrollProgressBarRadius = getContext().getResources()
                 .getInteger(R.integer.config_udfpsEnrollProgressBar);
         mEnrollHelper = enrollHelper;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.java
index 6e2e4ba..dcb5aef 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.java
@@ -22,6 +22,8 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.StatusBar;
 
+import java.util.Optional;
+
 /**
  * Class that coordinates non-HBM animations for non keyguard, enrollment or biometric prompt
  * states.
@@ -32,9 +34,9 @@
     protected UdfpsFpmOtherViewController(
             @NonNull UdfpsFpmOtherView view,
             @NonNull StatusBarStateController statusBarStateController,
-            @NonNull StatusBar statusBar,
+            @NonNull Optional<StatusBar> statusBarOptional,
             @NonNull DumpManager dumpManager) {
-        super(view, statusBarStateController, statusBar, dumpManager);
+        super(view, statusBarStateController, statusBarOptional, dumpManager);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt
index ea2bbfa..e231310 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt
@@ -58,9 +58,6 @@
                 "start" -> {
                     udfpsController?.playStartHaptic()
                 }
-                "acquired" -> {
-                    keyguardUpdateMonitor.playAcquiredHaptic()
-                }
                 "success" -> {
                     // needs to be kept up to date with AcquisitionClient#SUCCESS_VIBRATION_EFFECT
                     vibrator?.vibrate(
@@ -82,7 +79,6 @@
         pw.println("Usage: adb shell cmd statusbar udfps-haptic <haptic>")
         pw.println("Available commands:")
         pw.println("  start")
-        pw.println("  acquired")
         pw.println("  success, always plays CLICK haptic")
         pw.println("  error, always plays DOUBLE_CLICK haptic")
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index c128e5e..7a28c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -39,6 +39,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.Optional;
 
 /**
  * Class that coordinates non-HBM animations during keyguard authentication.
@@ -76,7 +77,7 @@
     protected UdfpsKeyguardViewController(
             @NonNull UdfpsKeyguardView view,
             @NonNull StatusBarStateController statusBarStateController,
-            @NonNull StatusBar statusBar,
+            @NonNull Optional<StatusBar> statusBarOptional,
             @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
             @NonNull DumpManager dumpManager,
@@ -86,7 +87,7 @@
             @NonNull KeyguardStateController keyguardStateController,
             @NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             @NonNull UdfpsController udfpsController) {
-        super(view, statusBarStateController, statusBar, dumpManager);
+        super(view, statusBarStateController, statusBarOptional, dumpManager);
         mKeyguardViewManager = statusBarKeyguardViewManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mLockScreenShadeTransitionController = transitionController;
@@ -125,7 +126,9 @@
         mInputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN;
         mIsBouncerVisible = mKeyguardViewManager.bouncerIsOrWillBeShowing();
         mConfigurationController.addCallback(mConfigurationListener);
-        mStatusBar.addExpansionChangedListener(mStatusBarExpansionChangedListener);
+        mStatusBarOptional.ifPresent(
+                statusBar -> statusBar.addExpansionChangedListener(
+                        mStatusBarExpansionChangedListener));
         updateAlpha();
         updatePauseAuth();
 
@@ -144,7 +147,9 @@
         mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor);
         mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
         mConfigurationController.removeCallback(mConfigurationListener);
-        mStatusBar.removeExpansionChangedListener(mStatusBarExpansionChangedListener);
+        mStatusBarOptional.ifPresent(
+                statusBar -> statusBar.removeExpansionChangedListener(
+                        mStatusBarExpansionChangedListener));
         if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) {
             mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null);
         }
@@ -393,11 +398,6 @@
                 }
 
                 @Override
-                public void onOverlayChanged() {
-                    mView.updateColor();
-                }
-
-                @Override
                 public void onConfigChanged(Configuration newConfig) {
                     mView.updateColor();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
index 15f77ff..6d31ef0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
@@ -25,6 +25,7 @@
 import android.graphics.Paint;
 import android.graphics.PointF;
 import android.graphics.RectF;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Build;
 import android.os.UserHandle;
@@ -141,11 +142,12 @@
                 : mAnimationViewController.getPaddingX();
         int paddingY = mAnimationViewController == null ? 0
                 : mAnimationViewController.getPaddingY();
+        final SensorLocationInternal location = mSensorProps.getLocation();
         mSensorRect.set(
                 paddingX,
                 paddingY,
-                2 * mSensorProps.sensorRadius + paddingX,
-                2 * mSensorProps.sensorRadius + paddingY);
+                2 * location.sensorRadius + paddingX,
+                2 * location.sensorRadius + paddingY);
 
         if (mAnimationViewController != null) {
             mAnimationViewController.onSensorRectUpdated(new RectF(mSensorRect));
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index 14e5991..be326da 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -37,6 +37,7 @@
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.sensors.ProximitySensor;
 import com.android.systemui.util.sensors.ThresholdSensor;
+import com.android.systemui.util.sensors.ThresholdSensorEvent;
 import com.android.systemui.util.time.SystemClock;
 
 import java.util.Collections;
@@ -405,7 +406,7 @@
         mProximitySensor.unregister(mSensorEventListener);
     }
 
-    private void onProximityEvent(ThresholdSensor.ThresholdSensorEvent proximityEvent) {
+    private void onProximityEvent(ThresholdSensorEvent proximityEvent) {
         // TODO: some of these classifiers might allow us to abort early, meaning we don't have to
         // make these calls.
         mFalsingManager.onProximityEvent(new ProximityEventImpl(proximityEvent));
@@ -423,9 +424,9 @@
     }
 
     private static class ProximityEventImpl implements FalsingManager.ProximityEvent {
-        private ThresholdSensor.ThresholdSensorEvent mThresholdSensorEvent;
+        private ThresholdSensorEvent mThresholdSensorEvent;
 
-        ProximityEventImpl(ThresholdSensor.ThresholdSensorEvent thresholdSensorEvent) {
+        ProximityEventImpl(ThresholdSensorEvent thresholdSensorEvent) {
             mThresholdSensorEvent = thresholdSensorEvent;
         }
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java b/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java
index 5b33428..7c281f9 100644
--- a/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java
+++ b/packages/SystemUI/src/com/android/systemui/colorextraction/SysuiColorExtractor.java
@@ -29,6 +29,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import java.io.FileDescriptor;
@@ -50,19 +51,32 @@
     private final GradientColors mBackdropColors;
 
     @Inject
-    public SysuiColorExtractor(Context context, ConfigurationController configurationController) {
-        this(context, new Tonal(context), configurationController,
-                context.getSystemService(WallpaperManager.class), false /* immediately */);
+    public SysuiColorExtractor(
+            Context context,
+            ConfigurationController configurationController,
+            DumpManager dumpManager) {
+        this(
+                context,
+                new Tonal(context),
+                configurationController,
+                context.getSystemService(WallpaperManager.class),
+                dumpManager,
+                false /* immediately */);
     }
 
     @VisibleForTesting
-    public SysuiColorExtractor(Context context, ExtractionType type,
+    public SysuiColorExtractor(
+            Context context,
+            ExtractionType type,
             ConfigurationController configurationController,
-            WallpaperManager wallpaperManager, boolean immediately) {
+            WallpaperManager wallpaperManager,
+            DumpManager dumpManager,
+            boolean immediately) {
         super(context, type, immediately, wallpaperManager);
         mTonal = type instanceof Tonal ? (Tonal) type : new Tonal(context);
         mNeutralColorsLock = new GradientColors();
         configurationController.addCallback(this);
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
 
         mBackdropColors = new GradientColors();
         mBackdropColors.setMainColor(Color.BLACK);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
index 79ee6a8..fa23842 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
@@ -34,11 +34,8 @@
 import android.view.Choreographer;
 import android.view.IWindowManager;
 import android.view.LayoutInflater;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
 import com.android.internal.util.NotificationMessagingUtil;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardUpdateMonitor;
@@ -49,9 +46,7 @@
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
 import com.android.systemui.accessibility.ModeSwitchesController;
-import com.android.systemui.accessibility.SystemActions;
 import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
-import com.android.systemui.assist.AssistManager;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -59,46 +54,25 @@
 import com.android.systemui.doze.AlwaysOnDisplayPolicy;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.model.SysUiState;
-import com.android.systemui.navigationbar.NavigationBarController;
-import com.android.systemui.navigationbar.NavigationBarOverlayController;
-import com.android.systemui.navigationbar.NavigationModeController;
-import com.android.systemui.plugins.PluginInitializerImpl;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.ReduceBrightColorsController;
-import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.recents.Recents;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.shared.plugins.PluginManagerImpl;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.shared.system.WindowManagerWrapper;
-import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
-import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DataSaverController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.theme.ThemeOverlayApplier;
 import com.android.systemui.util.leak.LeakDetector;
 import com.android.systemui.util.settings.SecureSettings;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
-import com.android.wm.shell.pip.Pip;
 
-import java.util.Optional;
 import java.util.concurrent.Executor;
 
 import javax.inject.Named;
 
-import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
@@ -166,9 +140,8 @@
     /** */
     @Provides
     @SysUISingleton
-    public LeakDetector provideLeakDetector() {
-        return LeakDetector.create();
-
+    public LeakDetector provideLeakDetector(DumpManager dumpManager) {
+        return LeakDetector.create(dumpManager);
     }
 
     @SuppressLint("MissingPermission")
@@ -188,13 +161,6 @@
     }
 
     /** */
-    @Provides
-    @SysUISingleton
-    public PluginManager providePluginManager(Context context) {
-        return new PluginManagerImpl(context, new PluginInitializerImpl());
-    }
-
-    /** */
     @SysUISingleton
     @Provides
     static ThemeOverlayApplier provideThemeOverlayManager(Context context,
@@ -210,65 +176,6 @@
     /** */
     @Provides
     @SysUISingleton
-    public NavigationBarController provideNavigationBarController(Context context,
-            WindowManager windowManager,
-            Lazy<AssistManager> assistManagerLazy,
-            AccessibilityManager accessibilityManager,
-            AccessibilityManagerWrapper accessibilityManagerWrapper,
-            DeviceProvisionedController deviceProvisionedController,
-            MetricsLogger metricsLogger,
-            OverviewProxyService overviewProxyService,
-            NavigationModeController navigationModeController,
-            AccessibilityButtonModeObserver accessibilityButtonModeObserver,
-            StatusBarStateController statusBarStateController,
-            SysUiState sysUiFlagsContainer,
-            BroadcastDispatcher broadcastDispatcher,
-            CommandQueue commandQueue,
-            Optional<Pip> pipOptional,
-            Optional<LegacySplitScreen> splitScreenOptional,
-            Optional<Recents> recentsOptional,
-            Lazy<StatusBar> statusBarLazy,
-            ShadeController shadeController,
-            NotificationRemoteInputManager notificationRemoteInputManager,
-            NotificationShadeDepthController notificationShadeDepthController,
-            SystemActions systemActions,
-            @Main Handler mainHandler,
-            UiEventLogger uiEventLogger,
-            NavigationBarOverlayController navBarOverlayController,
-            ConfigurationController configurationController,
-            UserTracker userTracker) {
-        return new NavigationBarController(context,
-                windowManager,
-                assistManagerLazy,
-                accessibilityManager,
-                accessibilityManagerWrapper,
-                deviceProvisionedController,
-                metricsLogger,
-                overviewProxyService,
-                navigationModeController,
-                accessibilityButtonModeObserver,
-                statusBarStateController,
-                sysUiFlagsContainer,
-                broadcastDispatcher,
-                commandQueue,
-                pipOptional,
-                splitScreenOptional,
-                recentsOptional,
-                statusBarLazy,
-                shadeController,
-                notificationRemoteInputManager,
-                notificationShadeDepthController,
-                systemActions,
-                mainHandler,
-                uiEventLogger,
-                navBarOverlayController,
-                configurationController,
-                userTracker);
-    }
-
-    /** */
-    @Provides
-    @SysUISingleton
     public AccessibilityFloatingMenuController provideAccessibilityFloatingMenuController(
             Context context, AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver,
             AccessibilityButtonModeObserver accessibilityButtonModeObserver,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 954ba79..7972318 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -41,6 +41,7 @@
 import android.content.res.Resources;
 import android.hardware.SensorManager;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.ColorDisplayManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.face.FaceManager;
@@ -73,6 +74,7 @@
 
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.appwidget.IAppWidgetService;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.LatencyTracker;
 import com.android.systemui.dagger.qualifiers.DisplayId;
@@ -159,6 +161,12 @@
 
     @Provides
     @Singleton
+    static DeviceStateManager provideDeviceStateManager(Context context) {
+        return context.getSystemService(DeviceStateManager.class);
+    }
+
+    @Provides
+    @Singleton
     static IActivityManager provideIActivityManager() {
         return ActivityManager.getService();
     }
@@ -213,6 +221,12 @@
 
     @Provides
     @Singleton
+    static InteractionJankMonitor provideInteractionJankMonitor() {
+        return InteractionJankMonitor.getInstance();
+    }
+
+    @Provides
+    @Singleton
     static InputMethodManager provideInputMethodManager(Context context) {
         return context.getSystemService(InputMethodManager.class);
     }
@@ -360,6 +374,12 @@
 
     @Provides
     @Singleton
+    static Optional<TelecomManager> provideOptionalTelecomManager(Context context) {
+        return Optional.ofNullable(context.getSystemService(TelecomManager.class));
+    }
+
+    @Provides
+    @Singleton
     static TelephonyManager provideTelephonyManager(Context context) {
         return context.getSystemService(TelephonyManager.class);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
index a89c7ac..18f8519 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
@@ -23,6 +23,8 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.UiEventLoggerImpl;
 import com.android.systemui.dagger.qualifiers.TestHarness;
+import com.android.systemui.plugins.PluginsModule;
+import com.android.systemui.unfold.UnfoldTransitionModule;
 import com.android.systemui.util.concurrency.GlobalConcurrencyModule;
 
 import javax.inject.Singleton;
@@ -47,7 +49,10 @@
  */
 @Module(includes = {
         FrameworkServicesModule.class,
-        GlobalConcurrencyModule.class})
+        GlobalConcurrencyModule.class,
+        UnfoldTransitionModule.class,
+        PluginsModule.class,
+})
 public class GlobalModule {
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
index d648c94..a3a45fe 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
@@ -18,8 +18,6 @@
 
 import android.content.Context;
 
-import com.android.systemui.util.concurrency.ThreadFactory;
-
 import javax.inject.Singleton;
 
 import dagger.BindsInstance;
@@ -55,9 +53,4 @@
      * Builder for a SysUIComponent.
      */
     SysUIComponent.Builder getSysUIComponent();
-
-    /**
-     * Build a {@link ThreadFactory}.
-     */
-    ThreadFactory createThreadFactory();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java
index 406981d..67ad3db 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java
@@ -24,6 +24,7 @@
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.GlobalActions;
+import com.android.systemui.plugins.PluginDependencyProvider;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
@@ -32,6 +33,7 @@
 
 import dagger.Binds;
 import dagger.Module;
+import dagger.Provides;
 
 /**
  * Module for binding Plugin implementations.
@@ -39,36 +41,40 @@
  * TODO(b/166258224): Many of these should be moved closer to their implementations.
  */
 @Module
-public interface PluginModule {
+public abstract class PluginModule {
+
+    /** */
+    @Provides
+    static ActivityStarter provideActivityStarter(ActivityStarterDelegate delegate,
+            PluginDependencyProvider dependencyProvider) {
+        dependencyProvider.allowPluginDependency(ActivityStarter.class, delegate);
+        return delegate;
+    }
 
     /** */
     @Binds
-    ActivityStarter provideActivityStarter(ActivityStarterDelegate delegate);
+    abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl);
 
     /** */
     @Binds
-    DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl);
+    abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);
 
     /** */
     @Binds
-    FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);
+    abstract GlobalActions provideGlobalActions(GlobalActionsImpl controllerImpl);
 
     /** */
     @Binds
-    GlobalActions provideGlobalActions(GlobalActionsImpl controllerImpl);
-
-    /** */
-    @Binds
-    GlobalActions.GlobalActionsManager provideGlobalActionsManager(
+    abstract GlobalActions.GlobalActionsManager provideGlobalActionsManager(
             GlobalActionsComponent controllerImpl);
 
     /** */
     @Binds
-    StatusBarStateController provideStatusBarStateController(
+    abstract StatusBarStateController provideStatusBarStateController(
             StatusBarStateControllerImpl controllerImpl);
 
     /** */
     @Binds
-    VolumeDialogController provideVolumeDialogController(VolumeDialogControllerImpl controllerImpl);
-
+    abstract VolumeDialogController provideVolumeDialogController(
+            VolumeDialogControllerImpl controllerImpl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 0fdc4d8..aac03f8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -30,6 +30,7 @@
 import com.android.wm.shell.TaskViewFactory;
 import com.android.wm.shell.apppairs.AppPairs;
 import com.android.wm.shell.bubbles.Bubbles;
+import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
 import com.android.wm.shell.onehanded.OneHanded;
@@ -96,6 +97,9 @@
         Builder setStartingSurface(Optional<StartingSurface> s);
 
         @BindsInstance
+        Builder setDisplayAreaHelper(Optional<DisplayAreaHelper> h);
+
+        @BindsInstance
         Builder setTaskSurfaceHelper(Optional<TaskSurfaceHelper> t);
 
         SysUIComponent build();
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
index 9cb9a36..0923caaf 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
@@ -68,6 +68,7 @@
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerImpl;
 import com.android.systemui.statusbar.policy.SensorPrivacyController;
 import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
+import com.android.systemui.volume.dagger.VolumeModule;
 
 import javax.inject.Named;
 
@@ -82,7 +83,8 @@
 @Module(includes = {
         MediaModule.class,
         PowerModule.class,
-        QSModule.class
+        QSModule.class,
+        VolumeModule.class
 })
 public abstract class SystemUIDefaultModule {
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 46ab5f6..f933128 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -18,6 +18,7 @@
 
 import android.app.INotificationManager;
 import android.content.Context;
+import android.view.LayoutInflater;
 
 import androidx.annotation.Nullable;
 
@@ -26,6 +27,7 @@
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.BootCompleteCache;
 import com.android.systemui.BootCompleteCacheImpl;
+import com.android.systemui.R;
 import com.android.systemui.SystemUIFactory;
 import com.android.systemui.appops.dagger.AppOpsModule;
 import com.android.systemui.assist.AssistModule;
@@ -36,6 +38,10 @@
 import com.android.systemui.demomode.dagger.DemoModeModule;
 import com.android.systemui.doze.dagger.DozeComponent;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlagManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FlagReader;
+import com.android.systemui.flags.FlagWriter;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.model.SysUiState;
@@ -46,7 +52,6 @@
 import com.android.systemui.settings.dagger.SettingsModule;
 import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -61,6 +66,7 @@
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.phone.StatusBarWindowView;
 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -69,13 +75,13 @@
 import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
 import com.android.systemui.tuner.dagger.TunerModule;
 import com.android.systemui.user.UserModule;
+import com.android.systemui.util.InjectionInflationController;
 import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
 import com.android.systemui.util.dagger.UtilModule;
 import com.android.systemui.util.sensors.SensorModule;
 import com.android.systemui.util.settings.SettingsUtilModule;
 import com.android.systemui.util.time.SystemClock;
 import com.android.systemui.util.time.SystemClockImpl;
-import com.android.systemui.volume.dagger.VolumeModule;
 import com.android.systemui.wallet.dagger.WalletModule;
 import com.android.systemui.wmshell.BubblesManager;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -112,7 +118,6 @@
             TunerModule.class,
             UserModule.class,
             UtilModule.class,
-            VolumeModule.class,
             WalletModule.class
         },
         subcomponents = {
@@ -141,10 +146,18 @@
 
     @SysUISingleton
     @Provides
-    static SysUiState provideSysUiState() {
-        return new SysUiState();
+    static SysUiState provideSysUiState(DumpManager dumpManager) {
+        final SysUiState state = new SysUiState();
+        dumpManager.registerDumpable(state);
+        return state;
     }
 
+    @Binds
+    abstract FlagReader provideFlagReader(FeatureFlagManager impl);
+
+    @Binds
+    abstract FlagWriter provideFlagWriter(FeatureFlagManager impl);
+
     @BindsOptionalOf
     abstract CommandQueue optionalCommandQueue();
 
@@ -200,4 +213,19 @@
                 groupManager, entryManager, notifPipeline, sysUiState, featureFlags, dumpManager,
                 sysuiMainExecutor));
     }
+
+    @Provides
+    @SysUISingleton
+    static StatusBarWindowView providesStatusBarWindowView(Context context,
+            InjectionInflationController injectionInflationController) {
+        StatusBarWindowView view =
+                (StatusBarWindowView) injectionInflationController.injectable(
+                        LayoutInflater.from(context)).inflate(R.layout.super_status_bar,
+                        /* root= */ null);
+        if (view == null) {
+            throw new IllegalStateException(
+                    "R.layout.super_status_bar could not be properly inflated");
+        }
+        return view;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index 442d351..618c26b 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -27,6 +27,7 @@
 import com.android.wm.shell.TaskViewFactory;
 import com.android.wm.shell.apppairs.AppPairs;
 import com.android.wm.shell.bubbles.Bubbles;
+import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
 import com.android.wm.shell.onehanded.OneHanded;
@@ -105,5 +106,8 @@
     Optional<StartingSurface> getStartingSurface();
 
     @WMSingleton
+    Optional<DisplayAreaHelper> getDisplayAreaHelper();
+
+    @WMSingleton
     Optional<TaskSurfaceHelper> getTaskSurfaceHelper();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 845d7dc..669965b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -26,6 +26,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -268,6 +269,16 @@
     }
 
     /**
+     * Appends doze updates due to a posture change.
+     */
+    public void tracePostureChanged(
+            @DevicePostureController.DevicePostureInt int posture,
+            String partUpdated
+    ) {
+        mLogger.logPostureChanged(posture, partUpdated);
+    }
+
+    /**
      * Appends pulse dropped event to logs
      */
     public void tracePulseDropped(boolean pulsePending, DozeMachine.State state, boolean blocked) {
@@ -391,8 +402,8 @@
             case REASON_SENSOR_DOUBLE_TAP: return "doubletap";
             case PULSE_REASON_SENSOR_LONG_PRESS: return "longpress";
             case PULSE_REASON_DOCKING: return "docking";
-            case PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN: return "reach-wakelockscreen";
-            case REASON_SENSOR_WAKE_UP: return "presence-wakeup";
+            case PULSE_REASON_SENSOR_WAKE_REACH: return "reach-wakelockscreen";
+            case REASON_SENSOR_WAKE_UP_PRESENCE: return "presence-wakeup";
             case REASON_SENSOR_TAP: return "tap";
             case REASON_SENSOR_UDFPS_LONG_PRESS: return "udfps";
             case REASON_SENSOR_QUICK_PICKUP: return "quickPickup";
@@ -403,8 +414,8 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({PULSE_REASON_NONE, PULSE_REASON_INTENT, PULSE_REASON_NOTIFICATION,
             PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP,
-            PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP,
-            PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN, REASON_SENSOR_TAP,
+            PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP_PRESENCE,
+            PULSE_REASON_SENSOR_WAKE_REACH, REASON_SENSOR_TAP,
             REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP})
     public @interface Reason {}
     public static final int PULSE_REASON_NONE = -1;
@@ -415,8 +426,8 @@
     public static final int REASON_SENSOR_DOUBLE_TAP = 4;
     public static final int PULSE_REASON_SENSOR_LONG_PRESS = 5;
     public static final int PULSE_REASON_DOCKING = 6;
-    public static final int REASON_SENSOR_WAKE_UP = 7;
-    public static final int PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN = 8;
+    public static final int REASON_SENSOR_WAKE_UP_PRESENCE = 7;
+    public static final int PULSE_REASON_SENSOR_WAKE_REACH = 8;
     public static final int REASON_SENSOR_TAP = 9;
     public static final int REASON_SENSOR_UDFPS_LONG_PRESS = 10;
     public static final int REASON_SENSOR_QUICK_PICKUP = 11;
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index dc18618..d79bf22 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.log.LogLevel.ERROR
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.DozeLog
+import com.android.systemui.statusbar.policy.DevicePostureController
 import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
@@ -195,6 +196,16 @@
         })
     }
 
+    fun logPostureChanged(posture: Int, partUpdated: String) {
+        buffer.log(TAG, INFO, {
+            int1 = posture
+            str1 = partUpdated
+        }, {
+            "Posture changed, posture=${DevicePostureController.devicePostureToString(int1)}" +
+                    " partUpdated=$str1"
+        })
+    }
+
     fun logPulseDropped(pulsePending: Boolean, state: DozeMachine.State, blocked: Boolean) {
         buffer.log(TAG, INFO, {
             bool1 = pulsePending
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
index 058f37a..765c245 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
@@ -29,17 +29,18 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.IndentingPrintWriter;
 
-import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.dagger.BrightnessSensor;
 import com.android.systemui.doze.dagger.DozeScope;
 import com.android.systemui.doze.dagger.WrappedService;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 
 import java.io.PrintWriter;
+import java.util.Objects;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -56,29 +57,22 @@
             "com.android.systemui.doze.AOD_BRIGHTNESS";
     protected static final String BRIGHTNESS_BUCKET = "brightness_bucket";
 
-    /**
-     * Just before the screen times out from user inactivity, DisplayPowerController dims the screen
-     * brightness to the lower of {@link #mScreenBrightnessDim}, or the current brightness minus
-     * DisplayPowerController#SCREEN_DIM_MINIMUM_REDUCTION_FLOAT.
-     *
-     * This value is 0.04f * 255, which converts SCREEN_DIM_MINIMUM_REDUCTION_FLOAT to the integer
-     * brightness values used by doze.
-     */
-    private static final int SCREEN_DIM_MINIMUM_REDUCTION_INT = 10;
-
     private final Context mContext;
     private final DozeMachine.Service mDozeService;
     private final DozeHost mDozeHost;
     private final Handler mHandler;
     private final SensorManager mSensorManager;
-    private final Optional<Sensor> mLightSensorOptional;
+    private final Optional<Sensor>[] mLightSensorOptional; // light sensors to use per posture
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final DozeParameters mDozeParameters;
-    private final DockManager mDockManager;
+    private final DevicePostureController mDevicePostureController;
+    private final DozeLog mDozeLog;
     private final int[] mSensorToBrightness;
     private final int[] mSensorToScrimOpacity;
     private final int mScreenBrightnessDim;
 
+    @DevicePostureController.DevicePostureInt
+    private int mDevicePosture;
     private boolean mRegistered;
     private int mDefaultDozeBrightness;
     private boolean mPaused = false;
@@ -93,32 +87,37 @@
      */
     private int mDebugBrightnessBucket = -1;
 
-    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-
     @Inject
-    public DozeScreenBrightness(Context context, @WrappedService DozeMachine.Service service,
+    public DozeScreenBrightness(
+            Context context,
+            @WrappedService DozeMachine.Service service,
             AsyncSensorManager sensorManager,
-            @BrightnessSensor Optional<Sensor> lightSensorOptional, DozeHost host, Handler handler,
+            @BrightnessSensor Optional<Sensor>[] lightSensorOptional,
+            DozeHost host, Handler handler,
             AlwaysOnDisplayPolicy alwaysOnDisplayPolicy,
             WakefulnessLifecycle wakefulnessLifecycle,
             DozeParameters dozeParameters,
-            DockManager dockManager,
-            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
+            DevicePostureController devicePostureController,
+            DozeLog dozeLog
+    ) {
         mContext = context;
         mDozeService = service;
         mSensorManager = sensorManager;
         mLightSensorOptional = lightSensorOptional;
+        mDevicePostureController = devicePostureController;
+        mDevicePosture = mDevicePostureController.getDevicePosture();
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mDozeParameters = dozeParameters;
         mDozeHost = host;
         mHandler = handler;
-        mDockManager = dockManager;
-        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
+        mDozeLog = dozeLog;
 
         mDefaultDozeBrightness = alwaysOnDisplayPolicy.defaultDozeBrightness;
         mScreenBrightnessDim = alwaysOnDisplayPolicy.dimBrightness;
         mSensorToBrightness = alwaysOnDisplayPolicy.screenBrightnessArray;
         mSensorToScrimOpacity = alwaysOnDisplayPolicy.dimmingScrimArray;
+
+        mDevicePostureController.addCallback(mDevicePostureCallback);
     }
 
     @Override
@@ -148,6 +147,7 @@
 
     private void onDestroy() {
         setLightSensorEnabled(false);
+        mDevicePostureController.removeCallback(mDevicePostureCallback);
     }
 
     @Override
@@ -163,19 +163,18 @@
         }
     }
 
-    public void updateBrightnessAndReady(boolean force) {
+    private void updateBrightnessAndReady(boolean force) {
         if (force || mRegistered || mDebugBrightnessBucket != -1) {
             int sensorValue = mDebugBrightnessBucket == -1
                     ? mLastSensorValue : mDebugBrightnessBucket;
             int brightness = computeBrightness(sensorValue);
             boolean brightnessReady = brightness > 0;
             if (brightnessReady) {
-                mDozeService.setDozeScreenBrightness(
-                        clampToDimBrightnessForScreenOff(clampToUserSetting(brightness)));
+                mDozeService.setDozeScreenBrightness(clampToUserSetting(brightness));
             }
 
             int scrimOpacity = -1;
-            if (!mLightSensorOptional.isPresent()) {
+            if (!isLightSensorPresent()) {
                 // No light sensor, scrims are always transparent.
                 scrimOpacity = 0;
             } else if (brightnessReady) {
@@ -188,6 +187,27 @@
         }
     }
 
+    private boolean lightSensorSupportsCurrentPosture() {
+        return mLightSensorOptional != null
+                && mDevicePosture < mLightSensorOptional.length;
+    }
+
+    private boolean isLightSensorPresent() {
+        if (!lightSensorSupportsCurrentPosture()) {
+            return mLightSensorOptional != null && mLightSensorOptional[0].isPresent();
+        }
+
+        return mLightSensorOptional[mDevicePosture].isPresent();
+    }
+
+    private Sensor getLightSensor() {
+        if (!lightSensorSupportsCurrentPosture()) {
+            return null;
+        }
+
+        return mLightSensorOptional[mDevicePosture].get();
+    }
+
     private int computeScrimOpacity(int sensorValue) {
         if (sensorValue < 0 || sensorValue >= mSensorToScrimOpacity.length) {
             return -1;
@@ -223,30 +243,22 @@
     /**
      * Clamp the brightness to the dim brightness value used by PowerManagerService just before the
      * device times out and goes to sleep, if we are sleeping from a timeout. This ensures that we
-     * don't raise the brightness back to the user setting before or during the screen off
-     * animation.
+     * don't raise the brightness back to the user setting before playing the screen off animation.
      */
     private int clampToDimBrightnessForScreenOff(int brightness) {
-        if (mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()
+        if (mDozeParameters.shouldControlUnlockedScreenOff()
                 && mWakefulnessLifecycle.getLastSleepReason()
                 == PowerManager.GO_TO_SLEEP_REASON_TIMEOUT) {
-            return Math.max(
-                    PowerManager.BRIGHTNESS_OFF,
-                    // Use the lower of either the dim brightness, or the current brightness reduced
-                    // by the minimum dim amount. This is the same logic used in
-                    // DisplayPowerController#updatePowerState to apply a minimum dim amount.
-                    Math.min(
-                            brightness - SCREEN_DIM_MINIMUM_REDUCTION_INT,
-                            mScreenBrightnessDim));
+            return Math.min(mScreenBrightnessDim, brightness);
         } else {
             return brightness;
         }
     }
 
     private void setLightSensorEnabled(boolean enabled) {
-        if (enabled && !mRegistered && mLightSensorOptional.isPresent()) {
+        if (enabled && !mRegistered && isLightSensorPresent()) {
             // Wait until we get an event from the sensor until indicating ready.
-            mRegistered = mSensorManager.registerListener(this, mLightSensorOptional.get(),
+            mRegistered = mSensorManager.registerListener(this, getLightSensor(),
                     SensorManager.SENSOR_DELAY_NORMAL, mHandler);
             mLastSensorValue = -1;
         } else if (!enabled && mRegistered) {
@@ -279,6 +291,41 @@
 
     /** Dump current state */
     public void dump(PrintWriter pw) {
-        pw.println("DozeScreenBrightnessSensorRegistered=" + mRegistered);
+        pw.println("DozeScreenBrightness:");
+        IndentingPrintWriter idpw = new IndentingPrintWriter(pw);
+        idpw.increaseIndent();
+        idpw.println("registered=" + mRegistered);
+        idpw.println("posture=" + DevicePostureController.devicePostureToString(mDevicePosture));
     }
+
+    private final DevicePostureController.Callback mDevicePostureCallback =
+            new DevicePostureController.Callback() {
+        @Override
+        public void onPostureChanged(int posture) {
+            if (mDevicePosture == posture
+                    || mLightSensorOptional.length < 2
+                    || posture >= mLightSensorOptional.length) {
+                return;
+            }
+
+            final Sensor oldSensor = mLightSensorOptional[mDevicePosture].get();
+            final Sensor newSensor = mLightSensorOptional[posture].get();
+            if (Objects.equals(oldSensor, newSensor)) {
+                mDevicePosture = posture;
+                // uses the same sensor for the new posture
+                return;
+            }
+
+            // cancel the previous sensor:
+            if (mRegistered) {
+                setLightSensorEnabled(false);
+                mDevicePosture = posture;
+                setLightSensorEnabled(true);
+            } else {
+                mDevicePosture = posture;
+            }
+            mDozeLog.tracePostureChanged(mDevicePosture, "DozeScreenBrightness swap "
+                    + "{" + oldSensor + "} => {" + newSensor + "}, mRegistered=" + mRegistered);
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
index 038be48..8f1486b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
@@ -77,7 +77,6 @@
     private final Provider<UdfpsController> mUdfpsControllerProvider;
     @Nullable private UdfpsController mUdfpsController;
     private final DozeLog mDozeLog;
-    private final DozeScreenBrightness mDozeScreenBrightness;
 
     private int mPendingScreenState = Display.STATE_UNKNOWN;
     private SettableWakeLock mWakeLock;
@@ -91,8 +90,7 @@
             WakeLock wakeLock,
             AuthController authController,
             Provider<UdfpsController> udfpsControllerProvider,
-            DozeLog dozeLog,
-            DozeScreenBrightness dozeScreenBrightness) {
+            DozeLog dozeLog) {
         mDozeService = service;
         mHandler = handler;
         mParameters = parameters;
@@ -101,7 +99,6 @@
         mAuthController = authController;
         mUdfpsControllerProvider = udfpsControllerProvider;
         mDozeLog = dozeLog;
-        mDozeScreenBrightness = dozeScreenBrightness;
 
         updateUdfpsController();
         if (mUdfpsController == null) {
@@ -207,12 +204,6 @@
         if (screenState != Display.STATE_UNKNOWN) {
             if (DEBUG) Log.d(TAG, "setDozeScreenState(" + screenState + ")");
             mDozeService.setDozeScreenState(screenState);
-            if (screenState == Display.STATE_DOZE) {
-                // If we're entering doze, update the doze screen brightness. We might have been
-                // clamping it to the dim brightness during the screen off animation, and we should
-                // now change it to the brightness we actually want according to the sensor.
-                mDozeScreenBrightness.updateBrightnessAndReady(false /* force */);
-            }
             mPendingScreenState = Display.STATE_UNKNOWN;
             mWakeLock.setAcquired(false);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 55e6154..9d0591e 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -38,6 +38,7 @@
 import android.util.Log;
 import android.view.Display;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.logging.UiEvent;
@@ -47,16 +48,43 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.plugins.SensorManagerPlugin;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 import com.android.systemui.util.sensors.ProximitySensor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.wakelock.WakeLock;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.function.Consumer;
 
+/**
+ * Tracks and registers/unregisters sensors while the device is dozing based on the config
+ * provided by {@link AmbientDisplayConfiguration} and parameters provided by {@link DozeParameters}
+ *
+ * Sensors registration depends on:
+ *    - sensor existence/availability
+ *    - user configuration (some can be toggled on/off via settings)
+ *    - use of the proximity sensor (sometimes prox cannot be registered in certain display states)
+ *    - touch state
+ *    - device posture
+ *
+ * Sensors will trigger the provided Callback's {@link Callback#onSensorPulse} method.
+ * These sensors include:
+ *    - pickup gesture
+ *    - single and double tap gestures
+ *    - udfps long-press gesture
+ *    - reach and presence gestures
+ *    - quick pickup gesture (low-threshold pickup gesture)
+ *
+ * This class also registers a ProximitySensor that reports near/far events and will
+ * trigger callbacks on the provided {@link mProxCallback}.
+ */
 public class DozeSensors {
 
     private static final boolean DEBUG = DozeService.DEBUG;
@@ -67,21 +95,30 @@
     private final AsyncSensorManager mSensorManager;
     private final AmbientDisplayConfiguration mConfig;
     private final WakeLock mWakeLock;
-    private final Consumer<Boolean> mProxCallback;
+    private final DozeLog mDozeLog;
     private final SecureSettings mSecureSettings;
-    private final Callback mCallback;
+    private final DevicePostureController mDevicePostureController;
     private final boolean mScreenOffUdfpsEnabled;
+
+    // Sensors
     @VisibleForTesting
-    protected TriggerSensor[] mSensors;
+    protected TriggerSensor[] mTriggerSensors;
+    private final ProximitySensor mProximitySensor;
+
+    // Sensor callbacks
+    private final Callback mSensorCallback; // receives callbacks on registered sensor events
+    private final Consumer<Boolean> mProxCallback; // receives callbacks on near/far updates
 
     private final Handler mHandler = new Handler();
-    private final ProximitySensor mProximitySensor;
     private long mDebounceFrom;
     private boolean mSettingRegistered;
     private boolean mListening;
     private boolean mListeningTouchScreenSensors;
     private boolean mListeningProxSensors;
 
+    @DevicePostureController.DevicePostureInt
+    private int mDevicePosture;
+
     // whether to only register sensors that use prox when the display state is dozing or off
     private boolean mSelectivelyRegisterProxSensors;
 
@@ -102,35 +139,47 @@
         }
     }
 
-    DozeSensors(Context context, AsyncSensorManager sensorManager,
-            DozeParameters dozeParameters, AmbientDisplayConfiguration config, WakeLock wakeLock,
-            Callback callback, Consumer<Boolean> proxCallback, DozeLog dozeLog,
-            ProximitySensor proximitySensor, SecureSettings secureSettings,
-            AuthController authController) {
+    DozeSensors(
+            Context context,
+            AsyncSensorManager sensorManager,
+            DozeParameters dozeParameters,
+            AmbientDisplayConfiguration config,
+            WakeLock wakeLock,
+            Callback sensorCallback,
+            Consumer<Boolean> proxCallback,
+            DozeLog dozeLog,
+            ProximitySensor proximitySensor,
+            SecureSettings secureSettings,
+            AuthController authController,
+            DevicePostureController devicePostureController
+    ) {
         mContext = context;
         mSensorManager = sensorManager;
         mConfig = config;
         mWakeLock = wakeLock;
         mProxCallback = proxCallback;
         mSecureSettings = secureSettings;
-        mCallback = callback;
+        mSensorCallback = sensorCallback;
+        mDozeLog = dozeLog;
         mProximitySensor = proximitySensor;
         mProximitySensor.setTag(TAG);
         mSelectivelyRegisterProxSensors = dozeParameters.getSelectivelyRegisterSensorsUsingProx();
         mListeningProxSensors = !mSelectivelyRegisterProxSensors;
         mScreenOffUdfpsEnabled =
                 config.screenOffUdfpsEnabled(KeyguardUpdateMonitor.getCurrentUser());
+        mDevicePostureController = devicePostureController;
+        mDevicePosture = mDevicePostureController.getDevicePosture();
 
         boolean udfpsEnrolled =
                 authController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser());
         boolean alwaysOn = mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT);
-        mSensors = new TriggerSensor[] {
+        mTriggerSensors = new TriggerSensor[] {
                 new TriggerSensor(
                         mSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION),
                         null /* setting */,
                         dozeParameters.getPulseOnSigMotion(),
                         DozeLog.PULSE_REASON_SENSOR_SIGMOTION, false /* touchCoords */,
-                        false /* touchscreen */, dozeLog),
+                        false /* touchscreen */),
                 new TriggerSensor(
                         mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
                         Settings.Secure.DOZE_PICK_UP_GESTURE,
@@ -139,18 +188,16 @@
                         DozeLog.REASON_SENSOR_PICKUP, false /* touchCoords */,
                         false /* touchscreen */,
                         false /* ignoresSetting */,
-                        false /* requires prox */,
-                        dozeLog),
+                        false /* requires prox */),
                 new TriggerSensor(
-                        findSensorWithType(config.doubleTapSensorType()),
+                        findSensor(config.doubleTapSensorType()),
                         Settings.Secure.DOZE_DOUBLE_TAP_GESTURE,
                         true /* configured */,
                         DozeLog.REASON_SENSOR_DOUBLE_TAP,
                         dozeParameters.doubleTapReportsTouchCoordinates(),
-                        true /* touchscreen */,
-                        dozeLog),
+                        true /* touchscreen */),
                 new TriggerSensor(
-                        findSensorWithType(config.tapSensorType()),
+                        findSensors(config.tapSensorTypeMapping()),
                         Settings.Secure.DOZE_TAP_SCREEN_GESTURE,
                         true /* settingDef */,
                         true /* configured */,
@@ -158,10 +205,10 @@
                         false /* reports touch coordinates */,
                         true /* touchscreen */,
                         false /* ignoresSetting */,
-                        dozeParameters.singleTapUsesProx() /* requiresProx */,
-                        dozeLog),
+                        dozeParameters.singleTapUsesProx(mDevicePosture) /* requiresProx */,
+                        mDevicePosture),
                 new TriggerSensor(
-                        findSensorWithType(config.longPressSensorType()),
+                        findSensor(config.longPressSensorType()),
                         Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
                         false /* settingDef */,
                         true /* configured */,
@@ -169,10 +216,9 @@
                         true /* reports touch coordinates */,
                         true /* touchscreen */,
                         false /* ignoresSetting */,
-                        dozeParameters.longPressUsesProx() /* requiresProx */,
-                        dozeLog),
+                        dozeParameters.longPressUsesProx() /* requiresProx */),
                 new TriggerSensor(
-                        findSensorWithType(config.udfpsLongPressSensorType()),
+                        findSensor(config.udfpsLongPressSensorType()),
                         "doze_pulse_on_auth",
                         true /* settingDef */,
                         udfpsEnrolled && (alwaysOn || mScreenOffUdfpsEnabled),
@@ -180,36 +226,34 @@
                         true /* reports touch coordinates */,
                         true /* touchscreen */,
                         false /* ignoresSetting */,
-                        dozeParameters.longPressUsesProx(),
-                        dozeLog),
+                        dozeParameters.longPressUsesProx()),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY),
                         Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE,
                         mConfig.wakeScreenGestureAvailable() && alwaysOn,
-                        DozeLog.REASON_SENSOR_WAKE_UP,
+                        DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE,
                         false /* reports touch coordinates */,
-                        false /* touchscreen */,
-                        dozeLog),
+                        false /* touchscreen */),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_LOCK_SCREEN),
                         Settings.Secure.DOZE_WAKE_LOCK_SCREEN_GESTURE,
                         mConfig.wakeScreenGestureAvailable(),
-                        DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN,
+                        DozeLog.PULSE_REASON_SENSOR_WAKE_REACH,
                         false /* reports touch coordinates */,
                         false /* touchscreen */,
-                        mConfig.getWakeLockScreenDebounce(),
-                        dozeLog),
+                        mConfig.getWakeLockScreenDebounce()),
                 new TriggerSensor(
-                        findSensorWithType(config.quickPickupSensorType()),
+                        findSensor(config.quickPickupSensorType()),
                         Settings.Secure.DOZE_QUICK_PICKUP_GESTURE,
                         true /* setting default */,
                         config.quickPickupSensorEnabled(KeyguardUpdateMonitor.getCurrentUser())
                                 && udfpsEnrolled,
                         DozeLog.REASON_SENSOR_QUICK_PICKUP,
-                        false /* touchCoords */,
-                        false /* touchscreen */, dozeLog),
+                        false /* requiresTouchCoordinates */,
+                        false /* requiresTouchscreen */,
+                        false /* ignoresSetting */,
+                        false /* requiresProx */),
         };
-
         setProxListening(false);  // Don't immediately start listening when we register.
         mProximitySensor.register(
                 proximityEvent -> {
@@ -217,17 +261,21 @@
                         mProxCallback.accept(!proximityEvent.getBelow());
                     }
                 });
+
+        mDevicePostureController.addCallback(mDevicePostureCallback);
     }
 
     /**
-     *  Unregister any sensors.
+     *  Unregister all sensors and callbacks.
      */
     public void destroy() {
         // Unregisters everything, which is enough to allow gc.
-        for (TriggerSensor triggerSensor : mSensors) {
+        for (TriggerSensor triggerSensor : mTriggerSensors) {
             triggerSensor.setListening(false);
         }
         mProximitySensor.pause();
+
+        mDevicePostureController.removeCallback(mDevicePostureCallback);
     }
 
     /**
@@ -238,21 +286,47 @@
         mDebounceFrom = SystemClock.uptimeMillis();
     }
 
-    private Sensor findSensorWithType(String type) {
-        return findSensorWithType(mSensorManager, type);
+    private Sensor findSensor(String type) {
+        return findSensor(mSensorManager, type, null);
+    }
+
+    @NonNull
+    private Sensor[] findSensors(@NonNull String[] types) {
+        Sensor[] sensorMap = new Sensor[DevicePostureController.SUPPORTED_POSTURES_SIZE];
+
+        // Map of sensorType => Sensor, so we reuse the same sensor if it's the same between
+        // postures
+        Map<String, Sensor> typeToSensorMap = new HashMap<>();
+        for (int i = 0; i < types.length; i++) {
+            String sensorType = types[i];
+            if (!typeToSensorMap.containsKey(sensorType)) {
+                typeToSensorMap.put(sensorType, findSensor(sensorType));
+            }
+            sensorMap[i] = typeToSensorMap.get(sensorType);
+        }
+
+        return sensorMap;
     }
 
     /**
-     * Utility method to find a {@link Sensor} for the supplied string type.
+     * Utility method to find a {@link Sensor} for the supplied string type and string name.
+     *
+     * Return the first sensor in the list that matches the specified inputs. Ignores type or name
+     * if the input is null or empty.
+     *
+     * @param type sensorType
+     * @parm name sensorName, to differentiate between sensors with the same type
      */
-    public static Sensor findSensorWithType(SensorManager sensorManager, String type) {
-        if (TextUtils.isEmpty(type)) {
-            return null;
-        }
-        List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
-        for (Sensor s : sensorList) {
-            if (type.equals(s.getStringType())) {
-                return s;
+    public static Sensor findSensor(SensorManager sensorManager, String type, String name) {
+        final boolean isNameSpecified = !TextUtils.isEmpty(name);
+        final boolean isTypeSpecified = !TextUtils.isEmpty(type);
+        if (isNameSpecified || isTypeSpecified) {
+            final List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
+            for (Sensor sensor : sensors) {
+                if ((!isNameSpecified || name.equals(sensor.getName()))
+                        && (!isTypeSpecified || type.equals(sensor.getStringType()))) {
+                    return sensor;
+                }
             }
         }
         return null;
@@ -292,7 +366,7 @@
      */
     private void updateListening() {
         boolean anyListening = false;
-        for (TriggerSensor s : mSensors) {
+        for (TriggerSensor s : mTriggerSensors) {
             boolean listen = mListening
                     && (!s.mRequiresTouchscreen || mListeningTouchScreenSensors)
                     && (!s.mRequiresProx || mListeningProxSensors);
@@ -305,7 +379,7 @@
         if (!anyListening) {
             mSecureSettings.unregisterContentObserver(mSettingsObserver);
         } else if (!mSettingRegistered) {
-            for (TriggerSensor s : mSensors) {
+            for (TriggerSensor s : mTriggerSensors) {
                 s.registerSettingsObserver(mSettingsObserver);
             }
         }
@@ -314,7 +388,7 @@
 
     /** Set the listening state of only the sensors that require the touchscreen. */
     public void setTouchscreenSensorsListening(boolean listening) {
-        for (TriggerSensor sensor : mSensors) {
+        for (TriggerSensor sensor : mTriggerSensors) {
             if (sensor.mRequiresTouchscreen) {
                 sensor.setListening(listening);
             }
@@ -322,7 +396,7 @@
     }
 
     public void onUserSwitched() {
-        for (TriggerSensor s : mSensors) {
+        for (TriggerSensor s : mTriggerSensors) {
             s.updateListening();
         }
     }
@@ -352,7 +426,7 @@
             if (userId != ActivityManager.getCurrentUser()) {
                 return;
             }
-            for (TriggerSensor s : mSensors) {
+            for (TriggerSensor s : mTriggerSensors) {
                 s.updateListening();
             }
         }
@@ -360,7 +434,7 @@
 
     /** Ignore the setting value of only the sensors that require the touchscreen. */
     public void ignoreTouchScreenSensorsSettingInterferingWithDocking(boolean ignore) {
-        for (TriggerSensor sensor : mSensors) {
+        for (TriggerSensor sensor : mTriggerSensors) {
             if (sensor.mRequiresTouchscreen) {
                 sensor.ignoreSetting(ignore);
             }
@@ -370,13 +444,15 @@
     /** Dump current state */
     public void dump(PrintWriter pw) {
         pw.println("mListening=" + mListening);
+        pw.println("mDevicePosture="
+                + DevicePostureController.devicePostureToString(mDevicePosture));
         pw.println("mListeningTouchScreenSensors=" + mListeningTouchScreenSensors);
         pw.println("mSelectivelyRegisterProxSensors=" + mSelectivelyRegisterProxSensors);
         pw.println("mListeningProxSensors=" + mListeningProxSensors);
         pw.println("mScreenOffUdfpsEnabled=" + mScreenOffUdfpsEnabled);
         IndentingPrintWriter idpw = new IndentingPrintWriter(pw);
         idpw.increaseIndent();
-        for (TriggerSensor s : mSensors) {
+        for (TriggerSensor s : mTriggerSensors) {
             idpw.println("Sensor: " + s.toString());
         }
         idpw.println("ProxSensor: " + mProximitySensor.toString());
@@ -391,7 +467,7 @@
 
     @VisibleForTesting
     class TriggerSensor extends TriggerEventListener {
-        final Sensor mSensor;
+        @NonNull final Sensor[] mSensors; // index = posture, value = sensor
         final boolean mConfigured;
         final int mPulseReason;
         private final String mSetting;
@@ -404,27 +480,67 @@
         protected boolean mRegistered;
         protected boolean mDisabled;
         protected boolean mIgnoresSetting;
-        protected final DozeLog mDozeLog;
+        private @DevicePostureController.DevicePostureInt int mPosture;
 
-        public TriggerSensor(Sensor sensor, String setting, boolean configured, int pulseReason,
-                boolean reportsTouchCoordinates, boolean requiresTouchscreen, DozeLog dozeLog) {
-            this(sensor, setting, true /* settingDef */, configured, pulseReason,
-                    reportsTouchCoordinates, requiresTouchscreen, dozeLog);
+        TriggerSensor(
+                Sensor sensor,
+                String setting,
+                boolean configured,
+                int pulseReason,
+                boolean reportsTouchCoordinates,
+                boolean requiresTouchscreen
+        ) {
+            this(
+                    sensor,
+                    setting,
+                    true /* settingDef */,
+                    configured,
+                    pulseReason,
+                    false /* ignoresSetting */,
+                    false /* requiresProx */,
+                    reportsTouchCoordinates,
+                    requiresTouchscreen
+            );
         }
 
-        public TriggerSensor(Sensor sensor, String setting, boolean settingDef,
-                boolean configured, int pulseReason, boolean reportsTouchCoordinates,
-                boolean requiresTouchscreen, DozeLog dozeLog) {
-            this(sensor, setting, settingDef, configured, pulseReason, reportsTouchCoordinates,
-                    requiresTouchscreen, false /* ignoresSetting */,
-                    false /* requiresProx */, dozeLog);
+        TriggerSensor(
+                Sensor sensor,
+                String setting,
+                boolean settingDef,
+                boolean configured,
+                int pulseReason,
+                boolean reportsTouchCoordinates,
+                boolean requiresTouchscreen,
+                boolean ignoresSetting,
+                boolean requiresProx
+        ) {
+            this(
+                    new Sensor[]{ sensor },
+                    setting,
+                    settingDef,
+                    configured,
+                    pulseReason,
+                    reportsTouchCoordinates,
+                    requiresTouchscreen,
+                    ignoresSetting,
+                    requiresProx,
+                    DevicePostureController.DEVICE_POSTURE_UNKNOWN
+            );
         }
 
-        private TriggerSensor(Sensor sensor, String setting, boolean settingDef,
-                boolean configured, int pulseReason, boolean reportsTouchCoordinates,
-                boolean requiresTouchscreen, boolean ignoresSetting, boolean requiresProx,
-                DozeLog dozeLog) {
-            mSensor = sensor;
+        TriggerSensor(
+                @NonNull Sensor[] sensors,
+                String setting,
+                boolean settingDef,
+                boolean configured,
+                int pulseReason,
+                boolean reportsTouchCoordinates,
+                boolean requiresTouchscreen,
+                boolean ignoresSetting,
+                boolean requiresProx,
+                @DevicePostureController.DevicePostureInt int posture
+        ) {
+            mSensors = sensors;
             mSetting = setting;
             mSettingDefault = settingDef;
             mConfigured = configured;
@@ -433,7 +549,43 @@
             mRequiresTouchscreen = requiresTouchscreen;
             mIgnoresSetting = ignoresSetting;
             mRequiresProx = requiresProx;
-            mDozeLog = dozeLog;
+            mPosture = posture;
+        }
+
+        /**
+         * @return true if the sensor changed based for the new posture
+         */
+        public boolean setPosture(@DevicePostureController.DevicePostureInt int posture) {
+            if (mPosture == posture
+                    || mSensors.length < 2
+                    || posture >= mSensors.length) {
+                return false;
+            }
+
+            Sensor oldSensor = mSensors[mPosture];
+            Sensor newSensor = mSensors[posture];
+            if (Objects.equals(oldSensor, newSensor)) {
+                mPosture = posture;
+                // uses the same sensor for the new posture
+                return false;
+            }
+
+            // cancel the previous sensor:
+            if (mRegistered) {
+                final boolean rt = mSensorManager.cancelTriggerSensor(this, oldSensor);
+                if (DEBUG) {
+                    Log.d(TAG, "posture changed, cancelTriggerSensor[" + oldSensor + "] "
+                            + rt);
+                }
+                mRegistered = false;
+            }
+
+            // update the new sensor:
+            mPosture = posture;
+            updateListening();
+            mDozeLog.tracePostureChanged(mPosture, "DozeSensors swap "
+                    + "{" + oldSensor + "} => {" + newSensor + "}, mRegistered=" + mRegistered);
+            return true;
         }
 
         public void setListening(boolean listen) {
@@ -455,24 +607,23 @@
         }
 
         public void updateListening() {
-            if (!mConfigured || mSensor == null) return;
+            final Sensor sensor = mSensors[mPosture];
+            if (!mConfigured || sensor == null) return;
             if (mRequested && !mDisabled && (enabledBySetting() || mIgnoresSetting)) {
                 if (!mRegistered) {
-                    mRegistered = mSensorManager.requestTriggerSensor(this, mSensor);
+                    mRegistered = mSensorManager.requestTriggerSensor(this, sensor);
                     if (DEBUG) {
-                        Log.d(TAG, "requestTriggerSensor[" + mSensor
-                                + "] " + mRegistered);
+                        Log.d(TAG, "requestTriggerSensor[" + sensor + "] " + mRegistered);
                     }
                 } else {
                     if (DEBUG) {
-                        Log.d(TAG, "requestTriggerSensor[" + mSensor
-                                + "] already registered");
+                        Log.d(TAG, "requestTriggerSensor[" + sensor + "] already registered");
                     }
                 }
             } else if (mRegistered) {
-                final boolean rt = mSensorManager.cancelTriggerSensor(this, mSensor);
+                final boolean rt = mSensorManager.cancelTriggerSensor(this, sensor);
                 if (DEBUG) {
-                    Log.d(TAG, "cancelTriggerSensor[" + mSensor + "] " + rt);
+                    Log.d(TAG, "cancelTriggerSensor[" + sensor + "] " + rt);
                 }
                 mRegistered = false;
             }
@@ -490,21 +641,29 @@
 
         @Override
         public String toString() {
-            return new StringBuilder("{mRegistered=").append(mRegistered)
+            StringBuilder sb = new StringBuilder();
+            sb.append("{")
+                    .append("mRegistered=").append(mRegistered)
                     .append(", mRequested=").append(mRequested)
                     .append(", mDisabled=").append(mDisabled)
                     .append(", mConfigured=").append(mConfigured)
                     .append(", mIgnoresSetting=").append(mIgnoresSetting)
-                    .append(", mSensor=").append(mSensor).append("}").toString();
+                    .append(", mSensors=").append(Arrays.toString(mSensors));
+            if (mSensors.length > 2) {
+                sb.append(", mPosture=")
+                        .append(DevicePostureController.devicePostureToString(mDevicePosture));
+            }
+            return sb.append("}").toString();
         }
 
         @Override
         @AnyThread
         public void onTrigger(TriggerEvent event) {
+            final Sensor sensor = mSensors[mDevicePosture];
             mDozeLog.traceSensor(mPulseReason);
             mHandler.post(mWakeLock.wrap(() -> {
                 if (DEBUG) Log.d(TAG, "onTrigger: " + triggerEventToString(event));
-                if (mSensor != null && mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
+                if (sensor != null && sensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
                     UI_EVENT_LOGGER.log(DozeSensorsUiEvent.ACTION_AMBIENT_GESTURE_PICKUP);
                 }
 
@@ -515,7 +674,7 @@
                     screenX = event.values[0];
                     screenY = event.values[1];
                 }
-                mCallback.onSensorPulse(mPulseReason, screenX, screenY, event.values);
+                mSensorCallback.onSensorPulse(mPulseReason, screenX, screenY, event.values);
                 if (!mRegistered) {
                     updateListening();  // reregister, this sensor only fires once
                 }
@@ -553,17 +712,16 @@
         private long mDebounce;
 
         PluginSensor(SensorManagerPlugin.Sensor sensor, String setting, boolean configured,
-                int pulseReason, boolean reportsTouchCoordinates, boolean requiresTouchscreen,
-                DozeLog dozeLog) {
+                int pulseReason, boolean reportsTouchCoordinates, boolean requiresTouchscreen) {
             this(sensor, setting, configured, pulseReason, reportsTouchCoordinates,
-                    requiresTouchscreen, 0L /* debounce */, dozeLog);
+                    requiresTouchscreen, 0L /* debounce */);
         }
 
         PluginSensor(SensorManagerPlugin.Sensor sensor, String setting, boolean configured,
                 int pulseReason, boolean reportsTouchCoordinates, boolean requiresTouchscreen,
-                long debounce, DozeLog dozeLog) {
+                long debounce) {
             super(null, setting, configured, pulseReason, reportsTouchCoordinates,
-                    requiresTouchscreen, dozeLog);
+                    requiresTouchscreen);
             mPluginSensor = sensor;
             mDebounce = debounce;
         }
@@ -617,11 +775,22 @@
                     return;
                 }
                 if (DEBUG) Log.d(TAG, "onSensorEvent: " + triggerEventToString(event));
-                mCallback.onSensorPulse(mPulseReason, -1, -1, event.getValues());
+                mSensorCallback.onSensorPulse(mPulseReason, -1, -1, event.getValues());
             }));
         }
     }
 
+    private final DevicePostureController.Callback mDevicePostureCallback = posture -> {
+        if (mDevicePosture == posture) {
+            return;
+        }
+        mDevicePosture = posture;
+
+        for (TriggerSensor triggerSensor : mTriggerSensors) {
+            triggerSensor.setPosture(mDevicePosture);
+        }
+    };
+
     public interface Callback {
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 756adca..20cd5b9 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -41,10 +41,12 @@
 import com.android.systemui.doze.DozeMachine.State;
 import com.android.systemui.doze.dagger.DozeScope;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.Assert;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.sensors.AsyncSensorManager;
+import com.android.systemui.util.sensors.ProximityCheck;
 import com.android.systemui.util.sensors.ProximitySensor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.wakelock.WakeLock;
@@ -89,12 +91,13 @@
     private final TriggerReceiver mBroadcastReceiver = new TriggerReceiver();
     private final DockEventListener mDockEventListener = new DockEventListener();
     private final DockManager mDockManager;
-    private final ProximitySensor.ProximityCheck mProxCheck;
+    private final ProximityCheck mProxCheck;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final AuthController mAuthController;
     private final DelayableExecutor mMainExecutor;
     private final KeyguardStateController mKeyguardStateController;
     private final UiEventLogger mUiEventLogger;
+    private final DevicePostureController mDevicePostureController;
 
     private long mNotificationPulseTime;
     private boolean mPulsePending;
@@ -177,12 +180,14 @@
             AmbientDisplayConfiguration config,
             DozeParameters dozeParameters, AsyncSensorManager sensorManager,
             WakeLock wakeLock, DockManager dockManager,
-            ProximitySensor proximitySensor, ProximitySensor.ProximityCheck proxCheck,
+            ProximitySensor proximitySensor,
+            ProximityCheck proxCheck,
             DozeLog dozeLog, BroadcastDispatcher broadcastDispatcher,
             SecureSettings secureSettings, AuthController authController,
             @Main DelayableExecutor mainExecutor,
             UiEventLogger uiEventLogger,
-            KeyguardStateController keyguardStateController) {
+            KeyguardStateController keyguardStateController,
+            DevicePostureController devicePostureController) {
         mContext = context;
         mDozeHost = dozeHost;
         mConfig = config;
@@ -190,9 +195,12 @@
         mSensorManager = sensorManager;
         mWakeLock = wakeLock;
         mAllowPulseTriggers = true;
+
+        mDevicePostureController = devicePostureController;
         mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters,
                 config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor,
-                secureSettings, authController);
+                secureSettings, authController, devicePostureController);
+
         mUiModeManager = mContext.getSystemService(UiModeManager.class);
         mDockManager = dockManager;
         mProxCheck = proxCheck;
@@ -203,6 +211,10 @@
         mUiEventLogger = uiEventLogger;
         mKeyguardStateController = keyguardStateController;
     }
+    private final DevicePostureController.Callback mDevicePostureCallback =
+            posture -> {
+
+            };
 
     @Override
     public void setDozeMachine(DozeMachine dozeMachine) {
@@ -275,8 +287,8 @@
         boolean isTap = pulseReason == DozeLog.REASON_SENSOR_TAP;
         boolean isPickup = pulseReason == DozeLog.REASON_SENSOR_PICKUP;
         boolean isLongPress = pulseReason == DozeLog.PULSE_REASON_SENSOR_LONG_PRESS;
-        boolean isWakeOnPresence = pulseReason == DozeLog.REASON_SENSOR_WAKE_UP;
-        boolean isWakeOnReach = pulseReason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN;
+        boolean isWakeOnPresence = pulseReason == DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE;
+        boolean isWakeOnReach = pulseReason == DozeLog.PULSE_REASON_SENSOR_WAKE_REACH;
         boolean isUdfpsLongPress = pulseReason == DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS;
         boolean isQuickPickup = pulseReason == DozeLog.REASON_SENSOR_QUICK_PICKUP;
         boolean isWakeDisplayEvent = isQuickPickup || ((isWakeOnPresence || isWakeOnReach)
@@ -446,7 +458,7 @@
                 mWantSensors = true;
                 mWantTouchScreenSensors = true;
                 if (newState == DozeMachine.State.DOZE_AOD && !sWakeDisplaySensorState) {
-                    onWakeScreen(false, newState, DozeLog.REASON_SENSOR_WAKE_UP);
+                    onWakeScreen(false, newState, DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE);
                 }
                 break;
             case DOZE_AOD_PAUSED:
@@ -515,7 +527,7 @@
         // When already pulsing we're allowed to show the wallpaper directly without
         // requesting a new pulse.
         if (dozeState == DozeMachine.State.DOZE_PULSING
-                && reason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
+                && reason == DozeLog.PULSE_REASON_SENSOR_WAKE_REACH) {
             mMachine.requestState(DozeMachine.State.DOZE_PULSING_BRIGHT);
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
index fbe06b0..374bed3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
@@ -155,7 +155,7 @@
                     public void onPulseStarted() {
                         try {
                             mMachine.requestState(
-                                    reason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN
+                                    reason == DozeLog.PULSE_REASON_SENSOR_WAKE_REACH
                                             ? DozeMachine.State.DOZE_PULSING_BRIGHT
                                             : DozeMachine.State.DOZE_PULSING);
                         } catch (IllegalStateException e) {
diff --git a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
index 9c6e02a..32b7658 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
@@ -38,10 +38,14 @@
 import com.android.systemui.doze.DozeUi;
 import com.android.systemui.doze.DozeWallpaperState;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 import com.android.systemui.util.wakelock.DelayedWakeLock;
 import com.android.systemui.util.wakelock.WakeLock;
 
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Optional;
 
 import dagger.Module;
@@ -93,9 +97,43 @@
 
     @Provides
     @BrightnessSensor
-    static Optional<Sensor> providesBrightnessSensor(
-            AsyncSensorManager sensorManager, Context context) {
-        return Optional.ofNullable(DozeSensors.findSensorWithType(sensorManager,
-                context.getString(R.string.doze_brightness_sensor_type)));
+    static Optional<Sensor>[] providesBrightnessSensors(
+            AsyncSensorManager sensorManager,
+            Context context,
+            DozeParameters dozeParameters) {
+        String[] sensorNames = dozeParameters.brightnessNames();
+        if (sensorNames.length == 0 || sensorNames == null) {
+            // if no brightness names are specified, just use the brightness sensor type
+            return new Optional[]{
+                    Optional.ofNullable(DozeSensors.findSensor(
+                            sensorManager,
+                            context.getString(R.string.doze_brightness_sensor_type),
+                            null
+                    ))
+            };
+        }
+
+        // length and index of brightnessMap correspond to DevicePostureController.DevicePostureInt:
+        final Optional<Sensor>[] brightnessSensorMap =
+                new Optional[DevicePostureController.SUPPORTED_POSTURES_SIZE];
+        Arrays.fill(brightnessSensorMap, Optional.empty());
+
+        // Map of sensorName => Sensor, so we reuse the same sensor if it's the same between
+        // postures
+        Map<String, Optional<Sensor>> nameToSensorMap = new HashMap<>();
+        for (int i = 0; i < sensorNames.length; i++) {
+            final String sensorName = sensorNames[i];
+            if (!nameToSensorMap.containsKey(sensorName)) {
+                nameToSensorMap.put(sensorName,
+                        Optional.ofNullable(
+                                DozeSensors.findSensor(
+                                        sensorManager,
+                                        context.getString(R.string.doze_brightness_sensor_type),
+                                        sensorNames[i]
+                                )));
+            }
+            brightnessSensorMap[i] = nameToSensorMap.get(sensorName);
+        }
+        return brightnessSensorMap;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index 5b327bd..21a1b75 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,11 +18,11 @@
 
 import android.util.ArrayMap
 import com.android.systemui.Dumpable
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
 import java.io.FileDescriptor
 import java.io.PrintWriter
 import javax.inject.Inject
+import javax.inject.Singleton
 
 /**
  * Maintains a registry of things that should be dumped when a bug report is taken
@@ -33,7 +33,7 @@
  *
  * See [DumpHandler] for more information on how and when this information is dumped.
  */
-@SysUISingleton
+@Singleton
 open class DumpManager @Inject constructor() {
     private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = ArrayMap()
     private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = ArrayMap()
@@ -56,6 +56,15 @@
     }
 
     /**
+     * Same as the above override, but automatically uses the simple class name as the dumpable
+     * name.
+     */
+    @Synchronized
+    fun registerDumpable(module: Dumpable) {
+        registerDumpable(module::class.java.simpleName, module)
+    }
+
+    /**
      * Unregisters a previously-registered dumpable.
      */
     @Synchronized
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagManager.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagManager.java
new file mode 100644
index 0000000..85baed4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagManager.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import com.android.systemui.dagger.SysUISingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Default implementation of the a Flag manager that returns default values for release builds
+ */
+@SysUISingleton
+public class FeatureFlagManager implements FlagReader, FlagWriter {
+    @Inject
+    public FeatureFlagManager() {}
+    public boolean isEnabled(String key, boolean defaultValue) {
+        return defaultValue;
+    }
+    public boolean isEnabled(int key, boolean defaultValue) {
+        return defaultValue;
+    }
+    public void setEnabled(String key, boolean value) {}
+    public void setEnabled(int key, boolean value) {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagReader.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagReader.java
index 6fbf81c..e78646a 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagReader.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagReader.java
@@ -20,19 +20,25 @@
 import android.util.SparseArray;
 
 import androidx.annotation.BoolRes;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.util.wrapper.BuildInfo;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
 import javax.inject.Inject;
 /**
  * Reads and caches feature flags for quick access
  *
- * Feature flags must be defined as boolean resources. For example:
+ * Feature flags must be defined as boolean resources. For example:t
  *
  * {@code
  *  <bool name="flag_foo_bar_baz">false</bool>
@@ -52,21 +58,39 @@
  * Calls to this class should probably be wrapped by a method in {@link FeatureFlags}.
  */
 @SysUISingleton
-public class FeatureFlagReader {
+public class FeatureFlagReader implements Dumpable {
     private final Resources mResources;
     private final boolean mAreFlagsOverrideable;
     private final SystemPropertiesHelper mSystemPropertiesHelper;
     private final SparseArray<CachedFlag> mCachedFlags = new SparseArray<>();
 
+    private final FlagReader mFlagReader;
+
     @Inject
     public FeatureFlagReader(
             @Main Resources resources,
             BuildInfo build,
-            SystemPropertiesHelper systemPropertiesHelper) {
+            DumpManager dumpManager,
+            SystemPropertiesHelper systemPropertiesHelper,
+            FlagReader reader) {
         mResources = resources;
+        mFlagReader = reader;
         mSystemPropertiesHelper = systemPropertiesHelper;
         mAreFlagsOverrideable =
                 build.isDebuggable() && mResources.getBoolean(R.bool.are_flags_overrideable);
+        dumpManager.registerDumpable("FeatureFlags", this);
+    }
+
+    boolean isEnabled(BooleanFlag flag) {
+        return mFlagReader.isEnabled(flag.getId(), flag.getDefault());
+    }
+
+    void addListener(FlagReader.Listener listener) {
+        mFlagReader.addListener(listener);
+    }
+
+    void removeListener(FlagReader.Listener listener) {
+        mFlagReader.removeListener(listener);
     }
 
     /**
@@ -119,6 +143,23 @@
         }
     }
 
+    @Override
+    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
+        ArrayList<String> flagStrings = new ArrayList<>(mCachedFlags.size());
+        for (int i = 0; i < mCachedFlags.size(); i++) {
+            int key = mCachedFlags.keyAt(i);
+            // get the object by the key.
+            CachedFlag flag = mCachedFlags.get(key);
+            flagStrings.add("  " + RESNAME_PREFIX + flag.name + ": " + flag.value + "\n");
+        }
+        flagStrings.sort(String.CASE_INSENSITIVE_ORDER);
+        pw.println("AreFlagsOverrideable: " + mAreFlagsOverrideable);
+        pw.println("Cached FeatureFlags:");
+        for (String flagString : flagStrings) {
+            pw.print(flagString);
+        }
+    }
+
     private static class CachedFlag {
         public final String name;
         public final boolean value;
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
new file mode 100644
index 0000000..947a39a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import android.content.Context;
+import android.util.FeatureFlagUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+/**
+ * Class to manage simple DeviceConfig-based feature flags.
+ *
+ * See {@link Flags} for instructions on defining new flags.
+ */
+@SysUISingleton
+public class FeatureFlags {
+    private final FeatureFlagReader mFlagReader;
+    private final Context mContext;
+    private final Map<Integer, Flag<?>> mFlagMap = new HashMap<>();
+    private final Map<Integer, List<Listener>> mListeners = new HashMap<>();
+
+    @Inject
+    public FeatureFlags(FeatureFlagReader flagReader, Context context) {
+        mFlagReader = flagReader;
+        mContext = context;
+
+        flagReader.addListener(mListener);
+    }
+
+    private final FlagReader.Listener mListener = id -> {
+        if (mListeners.containsKey(id) && mFlagMap.containsKey(id)) {
+            mListeners.get(id).forEach(listener -> listener.onFlagChanged(mFlagMap.get(id)));
+        }
+    };
+
+    @VisibleForTesting
+    void addFlag(Flag flag) {
+        mFlagMap.put(flag.getId(), flag);
+    }
+
+    /**
+     * @param flag The {@link BooleanFlag} of interest.
+     * @return The value of the flag.
+     */
+    public boolean isEnabled(BooleanFlag flag) {
+        return mFlagReader.isEnabled(flag);
+    }
+
+    /**
+     * @param flag The {@link IntFlag} of interest.
+
+    /** Add a listener for a specific flag. */
+    public void addFlagListener(Flag<?> flag, Listener listener) {
+        mListeners.putIfAbsent(flag.getId(), new ArrayList<>());
+        mListeners.get(flag.getId()).add(listener);
+        mFlagMap.putIfAbsent(flag.getId(), flag);
+    }
+
+    /** Remove a listener for a specific flag. */
+    public void removeFlagListener(Flag<?> flag, Listener listener) {
+        if (mListeners.containsKey(flag.getId())) {
+            mListeners.get(flag.getId()).remove(listener);
+        }
+    }
+
+    public boolean isNewNotifPipelineEnabled() {
+        return isEnabled(Flags.NEW_NOTIFICATION_PIPELINE);
+    }
+
+    public boolean isNewNotifPipelineRenderingEnabled() {
+        return isEnabled(Flags.NEW_NOTIFICATION_PIPELINE_RENDERING);
+    }
+
+    /** */
+    public boolean useNewLockscreenAnimations() {
+        return isEnabled(Flags.LOCKSCREEN_ANIMATIONS);
+    }
+
+    public boolean isPeopleTileEnabled() {
+        // TODO(b/202860494): different resource overlays have different values.
+        return mFlagReader.isEnabled(R.bool.flag_conversations);
+    }
+
+    public boolean isMonetEnabled() {
+        // TODO(b/202860494): used in wallpaper picker. Always true, maybe delete.
+        return mFlagReader.isEnabled(R.bool.flag_monet);
+    }
+
+    public boolean isPMLiteEnabled() {
+        return isEnabled(Flags.POWER_MENU_LITE);
+    }
+
+    public boolean isChargingRippleEnabled() {
+        // TODO(b/202860494): different resource overlays have different values.
+        return mFlagReader.isEnabled(R.bool.flag_charging_ripple);
+    }
+
+    public boolean isOngoingCallStatusBarChipEnabled() {
+        return isEnabled(Flags.ONGOING_CALL_STATUS_BAR_CHIP);
+    }
+
+    public boolean isOngoingCallInImmersiveEnabled() {
+        return isOngoingCallStatusBarChipEnabled() && isEnabled(Flags.ONGOING_CALL_IN_IMMERSIVE);
+    }
+
+    public boolean isOngoingCallInImmersiveChipTapEnabled() {
+        return isOngoingCallInImmersiveEnabled()
+                && isEnabled(Flags.ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP);
+    }
+
+    public boolean isSmartspaceEnabled() {
+        // TODO(b/202860494): different resource overlays have different values.
+        return mFlagReader.isEnabled(R.bool.flag_smartspace);
+    }
+
+    public boolean isSmartspaceDedupingEnabled() {
+        return isSmartspaceEnabled() && isEnabled(Flags.SMARTSPACE_DEDUPING);
+    }
+
+    public boolean isNewKeyguardSwipeAnimationEnabled() {
+        return isEnabled(Flags.NEW_UNLOCK_SWIPE_ANIMATION);
+    }
+
+    public boolean isSmartSpaceSharedElementTransitionEnabled() {
+        return isEnabled(Flags.SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED);
+    }
+
+    /** Whether or not to use the provider model behavior for the status bar icons */
+    public boolean isCombinedStatusBarSignalIconsEnabled() {
+        return isEnabled(Flags.COMBINED_STATUS_BAR_SIGNAL_ICONS);
+    }
+
+    /** System setting for provider model behavior */
+    public boolean isProviderModelSettingEnabled() {
+        return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL);
+    }
+
+    /**
+     * Use the new version of the user switcher
+     */
+    public boolean useNewUserSwitcher() {
+        return isEnabled(Flags.NEW_USER_SWITCHER);
+    }
+
+    /** static method for the system setting */
+    public static boolean isProviderModelSettingEnabled(Context context) {
+        return FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL);
+    }
+
+    /** Simple interface for beinga alerted when a specific flag changes value. */
+    public interface Listener {
+        /** */
+        void onFlagChanged(Flag<?> flag);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagReader.java b/packages/SystemUI/src/com/android/systemui/flags/FlagReader.java
new file mode 100644
index 0000000..1ae8c1f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagReader.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+
+/**
+ * Plugin for loading flag values
+ */
+public interface FlagReader {
+    /** Returns a boolean value for the given flag. */
+    default boolean isEnabled(int id, boolean def) {
+        return def;
+    }
+
+    /** Add a listener to be alerted when any flag changes. */
+    default void addListener(Listener listener) {}
+
+    /** Remove a listener to be alerted when any flag changes. */
+    default void removeListener(Listener listener) {}
+
+    /** A simple listener to be alerted when a flag changes. */
+    interface Listener {
+        /** */
+        void onFlagChanged(int id);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/packages/SystemUI/src/com/android/systemui/flags/FlagWriter.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to packages/SystemUI/src/com/android/systemui/flags/FlagWriter.kt
index b391bd9..bacc66b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagWriter.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.flags
 
-import com.android.systemui.R;
-
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
-}
+interface FlagWriter {
+    fun setEnabled(key: Int, value: Boolean) {}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt
index 6561bd5..1dc5a9f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt
@@ -26,11 +26,19 @@
  */
 @SysUISingleton
 open class SystemPropertiesHelper @Inject constructor() {
+    fun get(name: String): String {
+        return SystemProperties.get(name)
+    }
+
     fun getBoolean(name: String, default: Boolean): Boolean {
         return SystemProperties.getBoolean(name, default)
     }
 
+    fun set(name: String, value: String) {
+        SystemProperties.set(name, value)
+    }
+
     fun set(name: String, value: Int) {
-        SystemProperties.set(name, value.toString())
+        set(name, value.toString())
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
index 37b8a2c..4f5a969 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
@@ -22,6 +22,7 @@
 
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -60,11 +61,15 @@
             };
 
     @Inject
-    public FragmentService(FragmentCreator.Factory fragmentCreatorFactory,
-            ConfigurationController configurationController) {
+    public FragmentService(
+            FragmentCreator.Factory fragmentCreatorFactory,
+            ConfigurationController configurationController,
+            DumpManager dumpManager) {
         mFragmentCreator = fragmentCreatorFactory.build();
         initInjectionMap();
         configurationController.addCallback(mConfigurationListener);
+
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     ArrayMap<String, Method> getInjectionMap() {
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
deleted file mode 100644
index 1b4a47e..0000000
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ /dev/null
@@ -1,599 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.globalactions;
-
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.annotation.Nullable;
-import android.app.IActivityManager;
-import android.app.PendingIntent;
-import android.app.admin.DevicePolicyManager;
-import android.app.trust.TrustManager;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.graphics.drawable.Drawable;
-import android.media.AudioManager;
-import android.os.Handler;
-import android.os.UserManager;
-import android.os.Vibrator;
-import android.provider.Settings;
-import android.service.dreams.IDreamManager;
-import android.telecom.TelecomManager;
-import android.transition.AutoTransition;
-import android.transition.TransitionManager;
-import android.transition.TransitionSet;
-import android.view.IWindowManager;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.lifecycle.LifecycleOwner;
-
-import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.view.RotationPolicy;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.model.SysUiState;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.GlobalActions.GlobalActionsManager;
-import com.android.systemui.plugins.GlobalActionsPanelPlugin;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.telephony.TelephonyListenerManager;
-import com.android.systemui.util.RingerModeTracker;
-import com.android.systemui.util.leak.RotationUtils;
-import com.android.systemui.util.settings.GlobalSettings;
-import com.android.systemui.util.settings.SecureSettings;
-
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-import javax.inject.Provider;
-
-/**
- * Helper to show the global actions dialog.  Each item is an {@link Action} that may show depending
- * on whether the keyguard is showing, and whether the device is provisioned.
- * This version includes wallet.
- */
-public class GlobalActionsDialog extends GlobalActionsDialogLite
-        implements DialogInterface.OnDismissListener,
-        DialogInterface.OnShowListener,
-        ConfigurationController.ConfigurationListener,
-        GlobalActionsPanelPlugin.Callbacks,
-        LifecycleOwner {
-
-    private static final String TAG = "GlobalActionsDialog";
-
-    private final LockPatternUtils mLockPatternUtils;
-    private final KeyguardStateController mKeyguardStateController;
-    private final SysUiState mSysUiState;
-    private final ActivityStarter mActivityStarter;
-    private final SysuiColorExtractor mSysuiColorExtractor;
-    private final IStatusBarService mStatusBarService;
-    private final NotificationShadeWindowController mNotificationShadeWindowController;
-    private GlobalActionsPanelPlugin mWalletPlugin;
-
-    @VisibleForTesting
-    boolean mShowLockScreenCards = false;
-
-    private final KeyguardStateController.Callback mKeyguardStateControllerListener =
-            new KeyguardStateController.Callback() {
-        @Override
-        public void onUnlockedChanged() {
-            if (mDialog != null) {
-                ActionsDialog dialog = (ActionsDialog) mDialog;
-                boolean unlocked = mKeyguardStateController.isUnlocked();
-                if (dialog.mWalletViewController != null) {
-                    dialog.mWalletViewController.onDeviceLockStateChanged(!unlocked);
-                }
-
-                if (unlocked) {
-                    dialog.hideLockMessage();
-                }
-            }
-        }
-    };
-
-    private final ContentObserver mSettingsObserver = new ContentObserver(mMainHandler) {
-        @Override
-        public void onChange(boolean selfChange) {
-            onPowerMenuLockScreenSettingsChanged();
-        }
-    };
-
-    /**
-     * @param context everything needs a context :(
-     */
-    @Inject
-    public GlobalActionsDialog(
-            Context context,
-            GlobalActionsManager windowManagerFuncs,
-            AudioManager audioManager,
-            IDreamManager iDreamManager,
-            DevicePolicyManager devicePolicyManager,
-            LockPatternUtils lockPatternUtils,
-            BroadcastDispatcher broadcastDispatcher,
-            TelephonyListenerManager telephonyListenerManager,
-            GlobalSettings globalSettings,
-            SecureSettings secureSettings,
-            @Nullable Vibrator vibrator,
-            @Main Resources resources,
-            ConfigurationController configurationController,
-            ActivityStarter activityStarter,
-            KeyguardStateController keyguardStateController,
-            UserManager userManager,
-            TrustManager trustManager,
-            IActivityManager iActivityManager,
-            @Nullable TelecomManager telecomManager,
-            MetricsLogger metricsLogger,
-            SysuiColorExtractor colorExtractor,
-            IStatusBarService statusBarService,
-            NotificationShadeWindowController notificationShadeWindowController,
-            IWindowManager iWindowManager,
-            @Background Executor backgroundExecutor,
-            UiEventLogger uiEventLogger,
-            RingerModeTracker ringerModeTracker,
-            SysUiState sysUiState,
-            @Main Handler handler,
-            PackageManager packageManager,
-            StatusBar statusBar,
-            KeyguardUpdateMonitor keyguardUpdateMonitor) {
-
-        super(context,
-                windowManagerFuncs,
-                audioManager,
-                iDreamManager,
-                devicePolicyManager,
-                lockPatternUtils,
-                broadcastDispatcher,
-                telephonyListenerManager,
-                globalSettings,
-                secureSettings,
-                vibrator,
-                resources,
-                configurationController,
-                keyguardStateController,
-                userManager,
-                trustManager,
-                iActivityManager,
-                telecomManager,
-                metricsLogger,
-                colorExtractor,
-                statusBarService,
-                notificationShadeWindowController,
-                iWindowManager,
-                backgroundExecutor,
-                uiEventLogger,
-                null,
-                ringerModeTracker,
-                sysUiState,
-                handler,
-                packageManager,
-                statusBar,
-                keyguardUpdateMonitor);
-
-        mLockPatternUtils = lockPatternUtils;
-        mKeyguardStateController = keyguardStateController;
-        mSysuiColorExtractor = colorExtractor;
-        mStatusBarService = statusBarService;
-        mNotificationShadeWindowController = notificationShadeWindowController;
-        mSysUiState = sysUiState;
-        mActivityStarter = activityStarter;
-
-        mKeyguardStateController.addCallback(mKeyguardStateControllerListener);
-
-        // Listen for changes to show pay on the power menu while locked
-        onPowerMenuLockScreenSettingsChanged();
-        mGlobalSettings.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.POWER_MENU_LOCKED_SHOW_CONTENT),
-                false /* notifyForDescendants */,
-                mSettingsObserver);
-    }
-
-    @Override
-    public void destroy() {
-        super.destroy();
-        mKeyguardStateController.removeCallback(mKeyguardStateControllerListener);
-        mGlobalSettings.unregisterContentObserver(mSettingsObserver);
-    }
-
-    /**
-     * Show the global actions dialog (creating if necessary)
-     *
-     * @param keyguardShowing True if keyguard is showing
-     */
-    public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned,
-            GlobalActionsPanelPlugin walletPlugin) {
-        mWalletPlugin = walletPlugin;
-        super.showOrHideDialog(keyguardShowing, isDeviceProvisioned);
-    }
-
-    /**
-     * Returns the maximum number of power menu items to show based on which GlobalActions
-     * layout is being used.
-     */
-    @VisibleForTesting
-    @Override
-    protected int getMaxShownPowerItems() {
-        return getContext().getResources().getInteger(
-                com.android.systemui.R.integer.power_menu_max_columns);
-    }
-
-    /**
-     * Create the global actions dialog.
-     *
-     * @return A new dialog.
-     */
-    @Override
-    protected ActionsDialogLite createDialog() {
-        initDialogItems();
-
-        ActionsDialog dialog = new ActionsDialog(getContext(), mAdapter, mOverflowAdapter,
-                this::getWalletViewController, mSysuiColorExtractor,
-                mStatusBarService, mNotificationShadeWindowController,
-                mSysUiState, this::onRotate, isKeyguardShowing(), mPowerAdapter, getEventLogger(),
-                getStatusBar(), getKeyguardUpdateMonitor(), mLockPatternUtils);
-
-        if (shouldShowLockMessage(dialog)) {
-            dialog.showLockMessage();
-        }
-        dialog.setCanceledOnTouchOutside(false); // Handled by the custom class.
-        dialog.setOnDismissListener(this);
-        dialog.setOnShowListener(this);
-
-        return dialog;
-    }
-
-    @Nullable
-    private GlobalActionsPanelPlugin.PanelViewController getWalletViewController() {
-        if (mWalletPlugin == null) {
-            return null;
-        }
-        return mWalletPlugin.onPanelShown(this, !mKeyguardStateController.isUnlocked());
-    }
-
-    /**
-     * Implements {@link GlobalActionsPanelPlugin.Callbacks#dismissGlobalActionsMenu()}, which is
-     * called when the quick access wallet requests that an intent be started (with lock screen
-     * shown first if needed).
-     */
-    @Override
-    public void startPendingIntentDismissingKeyguard(PendingIntent pendingIntent) {
-        mActivityStarter.startPendingIntentDismissingKeyguard(pendingIntent);
-    }
-
-    @Override
-    protected int getEmergencyTextColor(Context context) {
-        return context.getResources().getColor(
-                com.android.systemui.R.color.global_actions_emergency_text);
-    }
-
-    @Override
-    protected int getEmergencyIconColor(Context context) {
-        return getContext().getResources().getColor(
-                com.android.systemui.R.color.global_actions_emergency_text);
-    }
-
-    @Override
-    protected int getEmergencyBackgroundColor(Context context) {
-        return getContext().getResources().getColor(
-                com.android.systemui.R.color.global_actions_emergency_background);
-    }
-
-    @Override
-    protected int getGridItemLayoutResource() {
-        return com.android.systemui.R.layout.global_actions_grid_item_v2;
-    }
-
-    @VisibleForTesting
-    static class ActionsDialog extends ActionsDialogLite {
-
-        private final Provider<GlobalActionsPanelPlugin.PanelViewController> mWalletFactory;
-        @Nullable private GlobalActionsPanelPlugin.PanelViewController mWalletViewController;
-        private ResetOrientationData mResetOrientationData;
-        @VisibleForTesting ViewGroup mLockMessageContainer;
-        private TextView mLockMessage;
-
-        ActionsDialog(Context context, MyAdapter adapter, MyOverflowAdapter overflowAdapter,
-                Provider<GlobalActionsPanelPlugin.PanelViewController> walletFactory,
-                SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService,
-                NotificationShadeWindowController notificationShadeWindowController,
-                SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing,
-                MyPowerOptionsAdapter powerAdapter, UiEventLogger uiEventLogger,
-                StatusBar statusBar, KeyguardUpdateMonitor keyguardUpdateMonitor,
-                LockPatternUtils lockPatternUtils) {
-            super(context, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions,
-                    adapter, overflowAdapter, sysuiColorExtractor, statusBarService,
-                    notificationShadeWindowController, sysuiState, onRotateCallback,
-                    keyguardShowing, powerAdapter, uiEventLogger, null,
-                    statusBar, keyguardUpdateMonitor, lockPatternUtils);
-            mWalletFactory = walletFactory;
-
-            // Update window attributes
-            Window window = getWindow();
-            window.getAttributes().systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
-            window.setLayout(MATCH_PARENT, MATCH_PARENT);
-            window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
-            window.addFlags(
-                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
-                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                            | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
-                            | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
-                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
-                            | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
-            setTitle(R.string.global_actions);
-            initializeLayout();
-        }
-
-        private boolean isWalletViewAvailable() {
-            return mWalletViewController != null && mWalletViewController.getPanelContent() != null;
-        }
-
-        private void initializeWalletView() {
-            if (mWalletFactory == null) {
-                return;
-            }
-            mWalletViewController = mWalletFactory.get();
-            if (!isWalletViewAvailable()) {
-                return;
-            }
-
-            boolean isLandscapeWalletViewShown = mContext.getResources().getBoolean(
-                    com.android.systemui.R.bool.global_actions_show_landscape_wallet_view);
-
-            int rotation = RotationUtils.getRotation(mContext);
-            boolean rotationLocked = RotationPolicy.isRotationLocked(mContext);
-            if (rotation != RotationUtils.ROTATION_NONE) {
-                if (rotationLocked) {
-                    if (mResetOrientationData == null) {
-                        mResetOrientationData = new ResetOrientationData();
-                        mResetOrientationData.locked = true;
-                        mResetOrientationData.rotation = rotation;
-                    }
-
-                    // Unlock rotation, so user can choose to rotate to portrait to see the panel.
-                    // This call is posted so that the rotation does not change until post-layout,
-                    // otherwise onConfigurationChanged() may not get invoked.
-                    mGlobalActionsLayout.post(() ->
-                            RotationPolicy.setRotationLockAtAngle(
-                                    mContext, false, RotationUtils.ROTATION_NONE));
-
-                    if (!isLandscapeWalletViewShown) {
-                        return;
-                    }
-                }
-            } else {
-                if (!rotationLocked) {
-                    if (mResetOrientationData == null) {
-                        mResetOrientationData = new ResetOrientationData();
-                        mResetOrientationData.locked = false;
-                    }
-                }
-
-                boolean shouldLockRotation = !isLandscapeWalletViewShown;
-                if (rotationLocked != shouldLockRotation) {
-                    // Locks the screen to portrait if the landscape / seascape orientation does not
-                    // show the wallet view, so the user doesn't accidentally hide the panel.
-                    // This call is posted so that the rotation does not change until post-layout,
-                    // otherwise onConfigurationChanged() may not get invoked.
-                    mGlobalActionsLayout.post(() ->
-                            RotationPolicy.setRotationLockAtAngle(
-                            mContext, shouldLockRotation, RotationUtils.ROTATION_NONE));
-                }
-            }
-
-            // Disable rotation suggestions, if enabled
-            setRotationSuggestionsEnabled(false);
-
-            FrameLayout panelContainer =
-                    findViewById(com.android.systemui.R.id.global_actions_wallet);
-            FrameLayout.LayoutParams panelParams =
-                    new FrameLayout.LayoutParams(
-                            FrameLayout.LayoutParams.MATCH_PARENT,
-                            FrameLayout.LayoutParams.MATCH_PARENT);
-            panelParams.topMargin = mContext.getResources().getDimensionPixelSize(
-                    com.android.systemui.R.dimen.global_actions_wallet_top_margin);
-            View walletView = mWalletViewController.getPanelContent();
-            panelContainer.addView(walletView, panelParams);
-            // Smooth transitions when wallet is resized, which can happen when a card is added
-            ViewGroup root = findViewById(com.android.systemui.R.id.global_actions_grid_root);
-            if (root != null) {
-                walletView.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) -> {
-                    int oldHeight = ob - ot;
-                    int newHeight = b - t;
-                    if (oldHeight > 0 && oldHeight != newHeight) {
-                        TransitionSet transition = new AutoTransition()
-                                .setDuration(250)
-                                .setOrdering(TransitionSet.ORDERING_TOGETHER);
-                        TransitionManager.beginDelayedTransition(root, transition);
-                    }
-                });
-            }
-        }
-
-        @Override
-        protected int getLayoutResource() {
-            return com.android.systemui.R.layout.global_actions_grid_v2;
-        }
-
-        @Override
-        protected void initializeLayout() {
-            super.initializeLayout();
-            mLockMessageContainer = requireViewById(
-                    com.android.systemui.R.id.global_actions_lock_message_container);
-            mLockMessage = requireViewById(com.android.systemui.R.id.global_actions_lock_message);
-            initializeWalletView();
-            getWindow().setBackgroundDrawable(mBackgroundDrawable);
-        }
-
-        @Override
-        protected void showDialog() {
-            mShowing = true;
-            mNotificationShadeWindowController.setRequestTopUi(true, TAG);
-            mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, true)
-                    .commitUpdate(mContext.getDisplayId());
-
-            ViewGroup root = (ViewGroup) mGlobalActionsLayout.getRootView();
-            root.setOnApplyWindowInsetsListener((v, windowInsets) -> {
-                root.setPadding(windowInsets.getStableInsetLeft(),
-                        windowInsets.getStableInsetTop(),
-                        windowInsets.getStableInsetRight(),
-                        windowInsets.getStableInsetBottom());
-                return WindowInsets.CONSUMED;
-            });
-
-            mBackgroundDrawable.setAlpha(0);
-            float xOffset = mGlobalActionsLayout.getAnimationOffsetX();
-            ObjectAnimator alphaAnimator =
-                    ObjectAnimator.ofFloat(mContainer, "alpha", 0f, 1f);
-            alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-            alphaAnimator.setDuration(183);
-            alphaAnimator.addUpdateListener((animation) -> {
-                float animatedValue = animation.getAnimatedFraction();
-                int alpha = (int) (animatedValue * mScrimAlpha * 255);
-                mBackgroundDrawable.setAlpha(alpha);
-            });
-
-            ObjectAnimator xAnimator =
-                    ObjectAnimator.ofFloat(mContainer, "translationX", xOffset, 0f);
-            xAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-            xAnimator.setDuration(350);
-
-            AnimatorSet animatorSet = new AnimatorSet();
-            animatorSet.playTogether(alphaAnimator, xAnimator);
-            animatorSet.start();
-        }
-
-        @Override
-        protected void dismissInternal() {
-            super.dismissInternal();
-        }
-
-        @Override
-        protected void completeDismiss() {
-            dismissWallet();
-            resetOrientation();
-            super.completeDismiss();
-        }
-
-        private void dismissWallet() {
-            if (mWalletViewController != null) {
-                mWalletViewController.onDismissed();
-                // The wallet controller should not be re-used after being dismissed.
-                mWalletViewController = null;
-            }
-        }
-
-        private void resetOrientation() {
-            if (mResetOrientationData != null) {
-                RotationPolicy.setRotationLockAtAngle(mContext, mResetOrientationData.locked,
-                        mResetOrientationData.rotation);
-            }
-            setRotationSuggestionsEnabled(true);
-        }
-
-        @Override
-        public void refreshDialog() {
-            // ensure dropdown menus are dismissed before re-initializing the dialog
-            dismissWallet();
-            super.refreshDialog();
-        }
-
-        void hideLockMessage() {
-            if (mLockMessageContainer.getVisibility() == View.VISIBLE) {
-                mLockMessageContainer.animate().alpha(0).setDuration(150).setListener(
-                        new AnimatorListenerAdapter() {
-                            @Override
-                            public void onAnimationEnd(Animator animation) {
-                                mLockMessageContainer.setVisibility(View.GONE);
-                            }
-                        }).start();
-            }
-        }
-
-        void showLockMessage() {
-            Drawable lockIcon = mContext.getDrawable(com.android.internal.R.drawable.ic_lock);
-            lockIcon.setTint(mContext.getColor(com.android.systemui.R.color.control_primary_text));
-            mLockMessage.setCompoundDrawablesWithIntrinsicBounds(null, lockIcon, null, null);
-            mLockMessageContainer.setVisibility(View.VISIBLE);
-        }
-
-        private static class ResetOrientationData {
-            public boolean locked;
-            public int rotation;
-        }
-    }
-
-    /**
-     * Determines whether or not debug mode has been activated for the Global Actions Panel.
-     */
-    private static boolean isPanelDebugModeEnabled(Context context) {
-        return Settings.Secure.getInt(context.getContentResolver(),
-                Settings.Secure.GLOBAL_ACTIONS_PANEL_DEBUG_ENABLED, 0) == 1;
-    }
-
-    /**
-     * Determines whether or not the Global Actions menu should be forced to use the newer
-     * grid-style layout.
-     */
-    private static boolean isForceGridEnabled(Context context) {
-        return isPanelDebugModeEnabled(context);
-    }
-
-    private boolean shouldShowLockMessage(ActionsDialog dialog) {
-        return isWalletAvailableAfterUnlock(dialog);
-    }
-
-    // Temporary while we move items out of the power menu
-    private boolean isWalletAvailableAfterUnlock(ActionsDialog dialog) {
-        boolean isLockedAfterBoot = mLockPatternUtils.getStrongAuthForUser(getCurrentUser().id)
-                == STRONG_AUTH_REQUIRED_AFTER_BOOT;
-        return !mKeyguardStateController.isUnlocked()
-                && (!mShowLockScreenCards || isLockedAfterBoot)
-                && dialog.isWalletViewAvailable();
-    }
-
-    private void onPowerMenuLockScreenSettingsChanged() {
-        mShowLockScreenCards = mSecureSettings.getInt(
-                Settings.Secure.POWER_MENU_LOCKED_SHOW_CONTENT, 0) != 0;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 9ada54b..b06b024 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -133,6 +133,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
@@ -197,7 +198,6 @@
     private final MetricsLogger mMetricsLogger;
     private final UiEventLogger mUiEventLogger;
     private final SysUiState mSysUiState;
-    private final GlobalActionsInfoProvider mInfoProvider;
 
     // Used for RingerModeTracker
     private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
@@ -237,7 +237,7 @@
     private int mDialogPressDelay = DIALOG_PRESS_DELAY; // ms
     protected Handler mMainHandler;
     private int mSmallestScreenWidthDp;
-    private final StatusBar mStatusBar;
+    private final Optional<StatusBar> mStatusBarOptional;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
 
     @VisibleForTesting
@@ -341,12 +341,11 @@
             IWindowManager iWindowManager,
             @Background Executor backgroundExecutor,
             UiEventLogger uiEventLogger,
-            GlobalActionsInfoProvider infoProvider,
             RingerModeTracker ringerModeTracker,
             SysUiState sysUiState,
-            @Main Handler handler,
+             @Main Handler handler,
             PackageManager packageManager,
-            StatusBar statusBar,
+            Optional<StatusBar> statusBarOptional,
             KeyguardUpdateMonitor keyguardUpdateMonitor) {
         mContext = context;
         mWindowManagerFuncs = windowManagerFuncs;
@@ -367,7 +366,6 @@
         mTelecomManager = telecomManager;
         mMetricsLogger = metricsLogger;
         mUiEventLogger = uiEventLogger;
-        mInfoProvider = infoProvider;
         mSysuiColorExtractor = colorExtractor;
         mStatusBarService = statusBarService;
         mNotificationShadeWindowController = notificationShadeWindowController;
@@ -377,7 +375,7 @@
         mSysUiState = sysUiState;
         mMainHandler = handler;
         mSmallestScreenWidthDp = resources.getConfiguration().smallestScreenWidthDp;
-        mStatusBar = statusBar;
+        mStatusBarOptional = statusBarOptional;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
 
         // receive broadcasts
@@ -428,8 +426,8 @@
         return mUiEventLogger;
     }
 
-    protected StatusBar getStatusBar() {
-        return mStatusBar;
+    protected Optional<StatusBar> getStatusBar() {
+        return mStatusBarOptional;
     }
 
     protected KeyguardUpdateMonitor getKeyguardUpdateMonitor() {
@@ -667,7 +665,7 @@
                 mAdapter, mOverflowAdapter, mSysuiColorExtractor,
                 mStatusBarService, mNotificationShadeWindowController,
                 mSysUiState, this::onRotate, mKeyguardShowing, mPowerAdapter, mUiEventLogger,
-                mInfoProvider, mStatusBar, mKeyguardUpdateMonitor, mLockPatternUtils);
+                mStatusBarOptional, mKeyguardUpdateMonitor, mLockPatternUtils);
 
         dialog.setOnDismissListener(this);
         dialog.setOnShowListener(this);
@@ -864,7 +862,7 @@
             mUiEventLogger.log(GlobalActionsEvent.GA_EMERGENCY_DIALER_PRESS);
             if (mTelecomManager != null) {
                 // Close shade so user sees the activity
-                mStatusBar.collapseShade();
+                mStatusBarOptional.ifPresent(StatusBar::collapseShade);
                 Intent intent = mTelecomManager.createLaunchEmergencyDialerIntent(
                         null /* number */);
                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
@@ -996,7 +994,7 @@
                             mIActivityManager.requestInteractiveBugReport();
                         }
                         // Close shade so user sees the activity
-                        mStatusBar.collapseShade();
+                        mStatusBarOptional.ifPresent(StatusBar::collapseShade);
                     } catch (RemoteException e) {
                     }
                 }
@@ -1016,7 +1014,7 @@
                 mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
                 mIActivityManager.requestFullBugReport();
                 // Close shade so user sees the activity
-                mStatusBar.collapseShade();
+                mStatusBarOptional.ifPresent(StatusBar::collapseShade);
             } catch (RemoteException e) {
             }
             return false;
@@ -2133,9 +2131,8 @@
         private Dialog mPowerOptionsDialog;
         protected final Runnable mOnRotateCallback;
         private UiEventLogger mUiEventLogger;
-        private GlobalActionsInfoProvider mInfoProvider;
         private GestureDetector mGestureDetector;
-        private StatusBar mStatusBar;
+        private Optional<StatusBar> mStatusBarOptional;
         private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
         private LockPatternUtils mLockPatternUtils;
 
@@ -2162,7 +2159,8 @@
                     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
                             float distanceY) {
                         if (distanceY < 0 && distanceY > distanceX
-                                && e1.getY() <= mStatusBar.getStatusBarHeight()) {
+                                && e1.getY() <= mStatusBarOptional.map(
+                                        StatusBar::getStatusBarHeight).orElse(0)) {
                             // Downwards scroll from top
                             openShadeAndDismiss();
                             return true;
@@ -2174,7 +2172,8 @@
                     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                             float velocityY) {
                         if (velocityY > 0 && Math.abs(velocityY) > Math.abs(velocityX)
-                                && e1.getY() <= mStatusBar.getStatusBarHeight()) {
+                                && e1.getY() <= mStatusBarOptional.map(
+                                        StatusBar::getStatusBarHeight).orElse(0)) {
                             // Downwards fling from top
                             openShadeAndDismiss();
                             return true;
@@ -2189,7 +2188,7 @@
                 NotificationShadeWindowController notificationShadeWindowController,
                 SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing,
                 MyPowerOptionsAdapter powerAdapter, UiEventLogger uiEventLogger,
-                @Nullable GlobalActionsInfoProvider infoProvider, StatusBar statusBar,
+                Optional<StatusBar> statusBarOptional,
                 KeyguardUpdateMonitor keyguardUpdateMonitor, LockPatternUtils lockPatternUtils) {
             super(context, themeRes);
             mContext = context;
@@ -2203,8 +2202,7 @@
             mOnRotateCallback = onRotateCallback;
             mKeyguardShowing = keyguardShowing;
             mUiEventLogger = uiEventLogger;
-            mInfoProvider = infoProvider;
-            mStatusBar = statusBar;
+            mStatusBarOptional = statusBarOptional;
             mKeyguardUpdateMonitor = keyguardUpdateMonitor;
             mLockPatternUtils = lockPatternUtils;
 
@@ -2237,12 +2235,14 @@
 
         private void openShadeAndDismiss() {
             mUiEventLogger.log(GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
-            if (mStatusBar.isKeyguardShowing()) {
+            if (mStatusBarOptional.map(StatusBar::isKeyguardShowing).orElse(false)) {
                 // match existing lockscreen behavior to open QS when swiping from status bar
-                mStatusBar.animateExpandSettingsPanel(null);
+                mStatusBarOptional.ifPresent(
+                        statusBar -> statusBar.animateExpandSettingsPanel(null));
             } else {
                 // otherwise, swiping down should expand notification shade
-                mStatusBar.animateExpandNotificationsPanel();
+                mStatusBarOptional.ifPresent(
+                        statusBar -> statusBar.animateExpandNotificationsPanel());
             }
             dismiss();
         }
@@ -2324,10 +2324,6 @@
                 mScrimAlpha = 1.0f;
             }
 
-            if (mInfoProvider != null && mInfoProvider.shouldShowMessage()) {
-                mInfoProvider.addPanel(mContext, mContainer, mAdapter.getCount(), () -> dismiss());
-            }
-
             // If user entered from the lock screen and smart lock was enabled, disable it
             int user = KeyguardUpdateMonitor.getCurrentUser();
             boolean userHasTrust = mKeyguardUpdateMonitor.getUserHasTrust(user);
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsInfoProvider.kt b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsInfoProvider.kt
deleted file mode 100644
index 25837e3..0000000
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsInfoProvider.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.globalactions
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.content.res.Configuration
-import android.net.Uri
-import android.service.quickaccesswallet.QuickAccessWalletClient
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import android.widget.TextView
-import com.android.systemui.R
-import com.android.systemui.controls.controller.ControlsController
-import com.android.systemui.plugins.ActivityStarter
-import javax.inject.Inject
-
-private const val TAG = "GlobalActionsInfo"
-
-/** Maximum number of times to show change info message  */
-private const val MAX_VIEW_COUNT = 3
-
-/** Maximum number of buttons allowed in landscape before this panel does not fit */
-private const val MAX_BUTTONS_LANDSCAPE = 4
-
-private const val PREFERENCE = "global_actions_info_prefs"
-private const val KEY_VIEW_COUNT = "view_count"
-
-class GlobalActionsInfoProvider @Inject constructor(
-    private val context: Context,
-    private val walletClient: QuickAccessWalletClient,
-    private val controlsController: ControlsController,
-    private val activityStarter: ActivityStarter
-) {
-
-    private var pendingIntent: PendingIntent
-
-    init {
-        val url = context.resources.getString(R.string.global_actions_change_url)
-        val intent = Intent(Intent.ACTION_VIEW).apply {
-            setData(Uri.parse(url))
-            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        }
-        pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
-    }
-
-    fun addPanel(context: Context, parent: ViewGroup, nActions: Int, dismissParent: Runnable) {
-        // This panel does not fit on landscape with two rows of buttons showing,
-        // so skip adding the panel (and incrementing view counT) in that case
-        val isLandscape =
-                context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
-        if (isLandscape && nActions > MAX_BUTTONS_LANDSCAPE) {
-            return
-        }
-
-        val view = LayoutInflater.from(context).inflate(R.layout.global_actions_change_panel,
-                parent, false)
-
-        val walletTitle = walletClient.serviceLabel ?: context.getString(R.string.wallet_title)
-        val message = view.findViewById<TextView>(R.id.global_actions_change_message)
-        message?.setText(context.getString(R.string.global_actions_change_description, walletTitle))
-
-        view.setOnClickListener { _ ->
-            dismissParent.run()
-            activityStarter.postStartActivityDismissingKeyguard(pendingIntent)
-        }
-        parent.addView(view, 0) // Add to top
-        incrementViewCount()
-    }
-
-    fun shouldShowMessage(): Boolean {
-        // This is only relevant for some devices
-        val isEligible = context.resources.getBoolean(
-                R.bool.global_actions_show_change_info)
-        if (!isEligible) {
-            return false
-        }
-
-        val sharedPrefs = context.getSharedPreferences(PREFERENCE, Context.MODE_PRIVATE)
-
-        // Only show to users who previously had these items set up
-        val viewCount = if (sharedPrefs.contains(KEY_VIEW_COUNT) || hadContent()) {
-            sharedPrefs.getInt(KEY_VIEW_COUNT, 0)
-        } else {
-            -1
-        }
-
-        // Limit number of times this is displayed
-        return viewCount > -1 && viewCount < MAX_VIEW_COUNT
-    }
-
-    private fun hadContent(): Boolean {
-        // Check whether user would have seen content in the power menu that has now moved
-        val hadControls = controlsController.getFavorites().size > 0
-        val hadCards = walletClient.isWalletFeatureAvailable
-        Log.d(TAG, "Previously had controls $hadControls, cards $hadCards")
-        return hadControls || hadCards
-    }
-
-    private fun incrementViewCount() {
-        val sharedPrefs = context.getSharedPreferences(PREFERENCE, Context.MODE_PRIVATE)
-        val count = sharedPrefs.getInt(KEY_VIEW_COUNT, 0)
-        sharedPrefs.edit().putInt(KEY_VIEW_COUNT, count + 1).apply()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java
index a51ec54..729730c 100644
--- a/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java
+++ b/packages/SystemUI/src/com/android/systemui/glwallpaper/ImageWallpaperRenderer.java
@@ -66,6 +66,13 @@
         mOnBitmapUpdated = c;
     }
 
+    /**
+     * @hide
+     */
+    public void use(Consumer<Bitmap> c) {
+        mTexture.use(c);
+    }
+
     @Override
     public boolean isWcgContent() {
         return mTexture.isWcgContent();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 62b92cb..3c58079 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -18,14 +18,34 @@
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
+import static android.view.WindowManager.TRANSIT_OLD_NONE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.WindowManager.TransitionFlags;
+import static android.view.WindowManager.TransitionOldType;
+import static android.view.WindowManager.TransitionType;
+import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD;
 
+import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.Service;
+import android.app.WindowConfiguration;
 import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Debug;
@@ -42,8 +62,14 @@
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.view.WindowManagerPolicyConstants;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionFilter;
+import android.window.TransitionInfo;
 
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.policy.IKeyguardDrawnCallback;
@@ -51,8 +77,11 @@
 import com.android.internal.policy.IKeyguardService;
 import com.android.internal.policy.IKeyguardStateCallback;
 import com.android.systemui.SystemUIApplication;
+import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.ArrayList;
+
 import javax.inject.Inject;
 
 public class KeyguardService extends Service {
@@ -79,40 +108,198 @@
      * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY
      */
     public static boolean sEnableRemoteKeyguardGoingAwayAnimation =
-            !Transitions.ENABLE_SHELL_TRANSITIONS && sEnableRemoteKeyguardAnimation >= 1;
+            sEnableRemoteKeyguardAnimation >= 1;
 
     /**
      * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY
      */
     public static boolean sEnableRemoteKeyguardOccludeAnimation =
-            !Transitions.ENABLE_SHELL_TRANSITIONS && sEnableRemoteKeyguardAnimation >= 2;
+            sEnableRemoteKeyguardAnimation >= 2;
 
     private final KeyguardViewMediator mKeyguardViewMediator;
     private final KeyguardLifecyclesDispatcher mKeyguardLifecyclesDispatcher;
 
+    private static int newModeToLegacyMode(int newMode) {
+        switch (newMode) {
+            case WindowManager.TRANSIT_OPEN:
+            case WindowManager.TRANSIT_TO_FRONT:
+                return MODE_OPENING;
+            case WindowManager.TRANSIT_CLOSE:
+            case WindowManager.TRANSIT_TO_BACK:
+                return MODE_CLOSING;
+            default:
+                return 2; // MODE_CHANGING
+        }
+    }
+
+    private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers) {
+        final ArrayList<RemoteAnimationTarget> out = new ArrayList<>();
+        for (int i = 0; i < info.getChanges().size(); i++) {
+            boolean changeIsWallpaper =
+                    (info.getChanges().get(i).getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0;
+            if (wallpapers != changeIsWallpaper) continue;
+
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+            final int taskId = taskInfo != null ? change.getTaskInfo().taskId : -1;
+            boolean isNotInRecents;
+            WindowConfiguration windowConfiguration = null;
+            if (taskInfo != null) {
+                if (taskInfo.getConfiguration() != null) {
+                    windowConfiguration =
+                            change.getTaskInfo().getConfiguration().windowConfiguration;
+                }
+                isNotInRecents = !change.getTaskInfo().isRunning;
+            } else {
+                isNotInRecents = true;
+            }
+            Rect localBounds = new Rect(change.getEndAbsBounds());
+            localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
+
+            out.add(new RemoteAnimationTarget(
+                    taskId,
+                    newModeToLegacyMode(change.getMode()),
+                    change.getLeash(),
+                    (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0
+                            || (change.getFlags() & TransitionInfo.FLAG_SHOW_WALLPAPER) != 0,
+                    null /* clipRect */,
+                    new Rect(0, 0, 0, 0) /* contentInsets */,
+                    info.getChanges().size() - i,
+                    new Point(), localBounds, new Rect(change.getEndAbsBounds()),
+                    windowConfiguration, isNotInRecents, null /* startLeash */,
+                    change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */));
+        }
+        return out.toArray(new RemoteAnimationTarget[out.size()]);
+    }
+
+    private static @TransitionOldType int getTransitionOldType(@TransitionType int type,
+            @TransitionFlags int flags, RemoteAnimationTarget[] apps) {
+        if (type == TRANSIT_KEYGUARD_GOING_AWAY
+                || (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) {
+            return apps.length == 0 ? TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER
+                    : TRANSIT_OLD_KEYGUARD_GOING_AWAY;
+        } else if (type == TRANSIT_KEYGUARD_OCCLUDE) {
+            return TRANSIT_OLD_KEYGUARD_OCCLUDE;
+        } else if (type == TRANSIT_KEYGUARD_UNOCCLUDE) {
+            return TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
+        } else {
+            Slog.d(TAG, "Unexpected transit type: " + type);
+            return TRANSIT_OLD_NONE;
+        }
+    }
+
+    private static IRemoteTransition wrap(IRemoteAnimationRunner runner) {
+        return new IRemoteTransition.Stub() {
+            @Override
+            public void startAnimation(IBinder transition, TransitionInfo info,
+                    SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
+                    throws RemoteException {
+                Slog.d(TAG, "Starts IRemoteAnimationRunner: info=" + info);
+                final RemoteAnimationTarget[] apps = wrap(info, false /* wallpapers */);
+                final RemoteAnimationTarget[] wallpapers = wrap(info, true /* wallpapers */);
+                final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0];
+
+                // TODO: Remove this, and update alpha value in the IAnimationRunner.
+                for (TransitionInfo.Change change : info.getChanges()) {
+                    t.setAlpha(change.getLeash(), 1.0f);
+                }
+                t.apply();
+                runner.onAnimationStart(getTransitionOldType(info.getType(), info.getFlags(), apps),
+                        apps, wallpapers, nonApps,
+                        new IRemoteAnimationFinishedCallback.Stub() {
+                            @Override
+                            public void onAnimationFinished() throws RemoteException {
+                                Slog.d(TAG, "Finish IRemoteAnimationRunner.");
+                                finishCallback.onTransitionFinished(null /* wct */, null /* t */);
+                            }
+                        }
+                );
+            }
+
+            public void mergeAnimation(IBinder transition, TransitionInfo info,
+                    SurfaceControl.Transaction t, IBinder mergeTarget,
+                    IRemoteTransitionFinishedCallback finishCallback) {
+
+            }
+        };
+    }
+
     @Inject
     public KeyguardService(KeyguardViewMediator keyguardViewMediator,
-                           KeyguardLifecyclesDispatcher keyguardLifecyclesDispatcher) {
+                           KeyguardLifecyclesDispatcher keyguardLifecyclesDispatcher,
+                           ShellTransitions shellTransitions) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
 
-        RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
-        if (sEnableRemoteKeyguardGoingAwayAnimation) {
-            final RemoteAnimationAdapter exitAnimationAdapter =
-                    new RemoteAnimationAdapter(mExitAnimationRunner, 0, 0);
-            definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, exitAnimationAdapter);
-            definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
-                    exitAnimationAdapter);
+        if (shellTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS) {
+            if (sEnableRemoteKeyguardGoingAwayAnimation) {
+                Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_GOING_AWAY");
+                TransitionFilter f = new TransitionFilter();
+                f.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+                shellTransitions.registerRemote(f,
+                        new RemoteTransition(wrap(mExitAnimationRunner)));
+            }
+            if (sEnableRemoteKeyguardOccludeAnimation) {
+                Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_(UN)OCCLUDE");
+                // Register for occluding
+                TransitionFilter f = new TransitionFilter();
+                f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED;
+                f.mRequirements = new TransitionFilter.Requirement[]{
+                        new TransitionFilter.Requirement(), new TransitionFilter.Requirement()};
+                // First require at-least one app showing that occludes.
+                f.mRequirements[0].mMustBeIndependent = false;
+                f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD;
+                f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+                // Then require that we aren't closing any occludes (because this would mean a
+                // regular task->task or activity->activity animation not involving keyguard).
+                f.mRequirements[1].mNot = true;
+                f.mRequirements[1].mMustBeIndependent = false;
+                f.mRequirements[1].mFlags = FLAG_OCCLUDES_KEYGUARD;
+                f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+                shellTransitions.registerRemote(f, new RemoteTransition(mOccludeAnimation));
+
+                // Now register for un-occlude.
+                f = new TransitionFilter();
+                f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED;
+                f.mRequirements = new TransitionFilter.Requirement[]{
+                        new TransitionFilter.Requirement(), new TransitionFilter.Requirement()};
+                // First require at-least one app going-away (doesn't need occlude flag
+                // as that is implicit by it having been visible and we don't want to exclude
+                // cases where we are un-occluding because the app removed its showWhenLocked
+                // capability at runtime).
+                f.mRequirements[1].mMustBeIndependent = false;
+                f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+                f.mRequirements[1].mMustBeTask = true;
+                // Then require that we aren't opening any occludes (otherwise we'd remain
+                // occluded).
+                f.mRequirements[0].mNot = true;
+                f.mRequirements[0].mMustBeIndependent = false;
+                f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD;
+                f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+                shellTransitions.registerRemote(f, new RemoteTransition(mUnoccludeAnimation));
+            }
+        } else {
+            RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
+            if (sEnableRemoteKeyguardGoingAwayAnimation) {
+                final RemoteAnimationAdapter exitAnimationAdapter =
+                        new RemoteAnimationAdapter(mExitAnimationRunner, 0, 0);
+                definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
+                        exitAnimationAdapter);
+                definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
+                        exitAnimationAdapter);
+            }
+            if (sEnableRemoteKeyguardOccludeAnimation) {
+                final RemoteAnimationAdapter occludeAnimationAdapter =
+                        new RemoteAnimationAdapter(mOccludeAnimationRunner, 0, 0);
+                definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_OCCLUDE,
+                        occludeAnimationAdapter);
+                definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_UNOCCLUDE,
+                        occludeAnimationAdapter);
+            }
+            ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay(
+                    DEFAULT_DISPLAY, definition);
         }
-        if (sEnableRemoteKeyguardOccludeAnimation) {
-            final RemoteAnimationAdapter occludeAnimationAdapter =
-                    new RemoteAnimationAdapter(mOccludeAnimationRunner, 0, 0);
-            definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_OCCLUDE, occludeAnimationAdapter);
-            definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_UNOCCLUDE, occludeAnimationAdapter);
-        }
-        ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay(
-                DEFAULT_DISPLAY, definition);
     }
 
     @Override
@@ -145,10 +332,10 @@
                 RemoteAnimationTarget[] wallpapers,
                 RemoteAnimationTarget[] nonApps,
                 IRemoteAnimationFinishedCallback finishedCallback) {
-            Trace.beginSection("KeyguardService.mBinder#startKeyguardExitAnimation");
+            Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation");
             checkPermission();
             mKeyguardViewMediator.startKeyguardExitAnimation(transit, apps, wallpapers,
-                    null /* nonApps */, finishedCallback);
+                    nonApps, finishedCallback);
             Trace.endSection();
         }
 
@@ -166,14 +353,14 @@
                        RemoteAnimationTarget[] wallpapers,
                         RemoteAnimationTarget[] nonApps,
                         IRemoteAnimationFinishedCallback finishedCallback) {
+            Slog.d(TAG, "mOccludeAnimationRunner.onAnimationStart: transit=" + transit);
             try {
                 if (transit == TRANSIT_OLD_KEYGUARD_OCCLUDE) {
                     mBinder.setOccluded(true /* isOccluded */, true /* animate */);
                 } else if (transit == TRANSIT_OLD_KEYGUARD_UNOCCLUDE) {
-                    mBinder.setOccluded(false /* isOccluded */, true /* animate */);
+                    mBinder.setOccluded(false /* isOccluded */, false /* animate */);
                 }
-                // TODO(bc-unlock): Implement occlude/unocclude animation applied on apps,
-                //  wallpapers and nonApps.
+                // TODO(bc-unlock): Implement (un)occlude animation.
                 finishedCallback.onAnimationFinished();
             } catch (RemoteException e) {
                 Slog.e(TAG, "RemoteException");
@@ -185,6 +372,40 @@
         }
     };
 
+    final IRemoteTransition mOccludeAnimation = new IRemoteTransition.Stub() {
+        @Override
+        public void startAnimation(IBinder transition, TransitionInfo info,
+                SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
+                    throws RemoteException {
+            t.apply();
+            mBinder.setOccluded(true /* isOccluded */, true /* animate */);
+            finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+        }
+
+        @Override
+        public void mergeAnimation(IBinder transition, TransitionInfo info,
+                SurfaceControl.Transaction t, IBinder mergeTarget,
+                IRemoteTransitionFinishedCallback finishCallback) {
+        }
+    };
+
+    final IRemoteTransition mUnoccludeAnimation = new IRemoteTransition.Stub() {
+        @Override
+        public void startAnimation(IBinder transition, TransitionInfo info,
+                SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
+                throws RemoteException {
+            t.apply();
+            mBinder.setOccluded(false /* isOccluded */, true /* animate */);
+            finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+        }
+
+        @Override
+        public void mergeAnimation(IBinder transition, TransitionInfo info,
+                SurfaceControl.Transaction t, IBinder mergeTarget,
+                IRemoteTransitionFinishedCallback finishCallback) {
+        }
+    };
+
     private final IKeyguardService.Stub mBinder = new IKeyguardService.Stub() {
 
         @Override // Binder interface
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 941f2c6..2cc564b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import dagger.Lazy
 import javax.inject.Inject
@@ -62,7 +62,7 @@
  * The dismiss amount is the inverse of the notification panel expansion, which decreases as the
  * lock screen is swiped away.
  */
-const val DISMISS_AMOUNT_SHOW_SURFACE_THRESHOLD = 0.1f
+const val DISMISS_AMOUNT_SHOW_SURFACE_THRESHOLD = 0.25f
 
 /**
  * Dismiss amount at which to complete the keyguard exit animation and hide the keyguard.
@@ -70,7 +70,7 @@
  * The dismiss amount is the inverse of the notification panel expansion, which decreases as the
  * lock screen is swiped away.
  */
-const val DISMISS_AMOUNT_EXIT_KEYGUARD_THRESHOLD = 0.3f
+const val DISMISS_AMOUNT_EXIT_KEYGUARD_THRESHOLD = 0.4f
 
 /**
  * Initiates, controls, and ends the keyguard unlock animation.
@@ -222,6 +222,11 @@
             keyguardViewController.hide(startTime, 350)
             surfaceBehindEntryAnimator.start()
         }
+
+        // Finish the keyguard remote animation if the dismiss amount has crossed the threshold.
+        // Check it here in case there is no more change to the dismiss amount after the last change
+        // that starts the keyguard animation. @see #updateKeyguardViewMediatorIfThresholdsReached()
+        finishKeyguardExitRemoteAnimationIfReachThreshold()
     }
 
     fun notifyCancelKeyguardExitAnimation() {
@@ -353,16 +358,6 @@
         }
 
         val dismissAmount = keyguardStateController.dismissAmount
-
-        // Hide the keyguard if we're fully dismissed, or if we're swiping to dismiss and have
-        // crossed the threshold to finish the dismissal.
-        val reachedHideKeyguardThreshold = (dismissAmount >= 1f ||
-                (keyguardStateController.isDismissingFromSwipe &&
-                        // Don't hide if we're flinging during a swipe, since we need to finish
-                        // animating it out. This will be called again after the fling ends.
-                        !keyguardStateController.isFlingingToDismissKeyguardDuringSwipeGesture &&
-                        dismissAmount >= DISMISS_AMOUNT_EXIT_KEYGUARD_THRESHOLD))
-
         if (dismissAmount >= DISMISS_AMOUNT_SHOW_SURFACE_THRESHOLD &&
                 !keyguardViewMediator.get().requestedShowSurfaceBehindKeyguard()) {
             // We passed the threshold, and we're not yet showing the surface behind the
@@ -375,9 +370,35 @@
             // out.
             keyguardViewMediator.get().hideSurfaceBehindKeyguard()
             fadeOutSurfaceBehind()
-        } else if (keyguardViewMediator.get()
-                        .isAnimatingBetweenKeyguardAndSurfaceBehindOrWillBe &&
-                reachedHideKeyguardThreshold) {
+        } else {
+            finishKeyguardExitRemoteAnimationIfReachThreshold()
+        }
+    }
+
+    /**
+     * Hides the keyguard if we're fully dismissed, or if we're swiping to dismiss and have crossed
+     * the threshold to finish the dismissal.
+     */
+    private fun finishKeyguardExitRemoteAnimationIfReachThreshold() {
+        // no-op if keyguard is not showing or animation is not enabled.
+        if (!KeyguardService.sEnableRemoteKeyguardGoingAwayAnimation ||
+                !keyguardViewController.isShowing) {
+            return
+        }
+
+        // no-op if animation is not requested yet.
+        if (!keyguardViewMediator.get().requestedShowSurfaceBehindKeyguard() ||
+                !keyguardViewMediator.get().isAnimatingBetweenKeyguardAndSurfaceBehindOrWillBe) {
+            return
+        }
+
+        val dismissAmount = keyguardStateController.dismissAmount
+        if (dismissAmount >= 1f ||
+                (keyguardStateController.isDismissingFromSwipe &&
+                        // Don't hide if we're flinging during a swipe, since we need to finish
+                        // animating it out. This will be called again after the fling ends.
+                        !keyguardStateController.isFlingingToDismissKeyguardDuringSwipeGesture &&
+                        dismissAmount >= DISMISS_AMOUNT_EXIT_KEYGUARD_THRESHOLD)) {
             keyguardViewMediator.get().onKeyguardExitRemoteAnimationFinished(false /* cancelled */)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 1a8af3b..19ee50a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -77,7 +77,6 @@
 import android.view.RemoteAnimationTarget;
 import android.view.SyncRtSurfaceTransactionApplier;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.WindowManagerPolicyConstants;
 import android.view.animation.Animation;
@@ -120,12 +119,15 @@
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
+import com.android.systemui.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.util.DeviceConfigProxy;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import dagger.Lazy;
 
@@ -671,13 +673,6 @@
                 }
             }
         }
-
-        @Override
-        public void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) {
-            synchronized (KeyguardViewMediator.this) {
-                notifyHasLockscreenWallpaperChanged(hasLockscreenWallpaper);
-            }
-        }
     };
 
     ViewMediatorCallback mViewMediatorCallback = new ViewMediatorCallback() {
@@ -816,6 +811,10 @@
     private DeviceConfigProxy mDeviceConfig;
     private DozeParameters mDozeParameters;
 
+    private final UnfoldTransitionConfig mUnfoldTransitionConfig;
+    private final Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealAnimation;
+    private final AtomicInteger mPendingDrawnTasks = new AtomicInteger();
+
     private final KeyguardStateController mKeyguardStateController;
     private final Lazy<KeyguardUnlockAnimationController> mKeyguardUnlockAnimationControllerLazy;
     private boolean mWallpaperSupportsAmbientMode;
@@ -838,6 +837,8 @@
             NavigationModeController navigationModeController,
             KeyguardDisplayManager keyguardDisplayManager,
             DozeParameters dozeParameters,
+            UnfoldTransitionConfig unfoldTransitionConfig,
+            Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
             SysuiStatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
             Lazy<KeyguardUnlockAnimationController> keyguardUnlockAnimationControllerLazy,
@@ -871,6 +872,8 @@
                     mInGestureNavigationMode = QuickStepContract.isGesturalMode(mode);
                 }));
         mDozeParameters = dozeParameters;
+        mUnfoldTransitionConfig = unfoldTransitionConfig;
+        mUnfoldLightRevealAnimation = unfoldLightRevealOverlayAnimation;
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
 
@@ -2349,8 +2352,8 @@
     }
 
     private Configuration.Builder createInteractionJankMonitorConf(String tag) {
-        return new Configuration.Builder(CUJ_LOCKSCREEN_UNLOCK_ANIMATION)
-                .setView(mKeyguardViewControllerLazy.get().getViewRootImpl().getView())
+        return Configuration.Builder.withView(CUJ_LOCKSCREEN_UNLOCK_ANIMATION,
+                mKeyguardViewControllerLazy.get().getViewRootImpl().getView())
                 .setTag(tag);
     }
 
@@ -2394,7 +2397,7 @@
         final boolean wasShowing = mShowing;
         onKeyguardExitFinished();
 
-        if (mKeyguardStateController.isDismissingFromSwipe() || !wasShowing) {
+        if (mKeyguardStateController.isDismissingFromSwipe() || wasShowing) {
             mKeyguardUnlockAnimationControllerLazy.get().hideKeyguardViewAfterRemoteAnimation();
         }
 
@@ -2553,6 +2556,24 @@
         Trace.beginSection("KeyguardViewMediator#handleNotifyScreenTurningOn");
         synchronized (KeyguardViewMediator.this) {
             if (DEBUG) Log.d(TAG, "handleNotifyScreenTurningOn");
+
+            if (mUnfoldTransitionConfig.isEnabled()) {
+                mPendingDrawnTasks.set(2); // unfold overlay and keyguard drawn
+
+                mUnfoldLightRevealAnimation.get()
+                        .onScreenTurningOn(() -> {
+                            if (mPendingDrawnTasks.decrementAndGet() == 0) {
+                                try {
+                                    callback.onDrawn();
+                                } catch (RemoteException e) {
+                                    Slog.w(TAG, "Exception calling onDrawn():", e);
+                                }
+                            }
+                        });
+            } else {
+                mPendingDrawnTasks.set(1); // only keyguard drawn
+            }
+
             mKeyguardViewControllerLazy.get().onScreenTurningOn();
             if (callback != null) {
                 if (mWakeAndUnlocking) {
@@ -2567,9 +2588,6 @@
 
     private void handleNotifyScreenTurnedOn() {
         Trace.beginSection("KeyguardViewMediator#handleNotifyScreenTurnedOn");
-        if (LatencyTracker.isEnabled(mContext)) {
-            LatencyTracker.getInstance(mContext).onActionEnd(LatencyTracker.ACTION_TURN_ON_SCREEN);
-        }
         synchronized (this) {
             if (DEBUG) Log.d(TAG, "handleNotifyScreenTurnedOn");
             mKeyguardViewControllerLazy.get().onScreenTurnedOn();
@@ -2586,10 +2604,12 @@
 
     private void notifyDrawn(final IKeyguardDrawnCallback callback) {
         Trace.beginSection("KeyguardViewMediator#notifyDrawn");
-        try {
-            callback.onDrawn();
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Exception calling onDrawn():", e);
+        if (mPendingDrawnTasks.decrementAndGet() == 0) {
+            try {
+                callback.onDrawn();
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Exception calling onDrawn():", e);
+            }
         }
         Trace.endSection();
     }
@@ -2626,7 +2646,6 @@
      * Registers the StatusBar to which the Keyguard View is mounted.
      *
      * @param statusBar
-     * @param container
      * @param panelView
      * @param biometricUnlockController
      * @param notificationContainer
@@ -2634,10 +2653,10 @@
      * @return the View Controller for the Keyguard View this class is mediating.
      */
     public KeyguardViewController registerStatusBar(StatusBar statusBar,
-            ViewGroup container, NotificationPanelViewController panelView,
+            NotificationPanelViewController panelView,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer, KeyguardBypassController bypassController) {
-        mKeyguardViewControllerLazy.get().registerStatusBar(statusBar, container, panelView,
+        mKeyguardViewControllerLazy.get().registerStatusBar(statusBar, panelView,
                 biometricUnlockController, notificationContainer, bypassController);
         return mKeyguardViewControllerLazy.get();
     }
@@ -2743,6 +2762,7 @@
         pw.print("  mHideAnimationRun: "); pw.println(mHideAnimationRun);
         pw.print("  mPendingReset: "); pw.println(mPendingReset);
         pw.print("  mPendingLock: "); pw.println(mPendingLock);
+        pw.print("  mPendingDrawnTasks: "); pw.println(mPendingDrawnTasks.get());
         pw.print("  mWakeAndUnlocking: "); pw.println(mWakeAndUnlocking);
         pw.print("  mDrawnCallback: "); pw.println(mDrawnCallback);
     }
@@ -2872,21 +2892,6 @@
         }
     }
 
-    private void notifyHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) {
-        int size = mKeyguardStateCallbacks.size();
-        for (int i = size - 1; i >= 0; i--) {
-            try {
-                mKeyguardStateCallbacks.get(i).onHasLockscreenWallpaperChanged(
-                        hasLockscreenWallpaper);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to call onHasLockscreenWallpaperChanged", e);
-                if (e instanceof DeadObjectException) {
-                    mKeyguardStateCallbacks.remove(i);
-                }
-            }
-        }
-    }
-
     public void addStateMonitorCallback(IKeyguardStateCallback callback) {
         synchronized (this) {
             mKeyguardStateCallbacks.add(callback);
@@ -2896,7 +2901,6 @@
                 callback.onInputRestrictedStateChanged(mInputRestricted);
                 callback.onTrustedChanged(mUpdateMonitor.getUserHasTrust(
                         KeyguardUpdateMonitor.getCurrentUser()));
-                callback.onHasLockscreenWallpaperChanged(mUpdateMonitor.hasLockscreenWallpaper());
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to call to IKeyguardStateCallback", e);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
new file mode 100644
index 0000000..044a57c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.keyguard
+
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class LifecycleScreenStatusProvider @Inject constructor(screenLifecycle: ScreenLifecycle) :
+    ScreenStatusProvider, ScreenLifecycle.Observer {
+
+    init {
+        screenLifecycle.addObserver(this)
+    }
+
+    private val listeners: MutableList<ScreenListener> = mutableListOf()
+
+    override fun removeCallback(listener: ScreenListener) {
+        listeners.remove(listener)
+    }
+
+    override fun addCallback(listener: ScreenListener) {
+        listeners.add(listener)
+    }
+
+    override fun onScreenTurnedOn() {
+        listeners.forEach(ScreenListener::onScreenTurnedOn)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java
index 084e84a..d17c39a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java
@@ -19,17 +19,18 @@
 import android.os.Trace;
 
 import com.android.systemui.Dumpable;
-import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
 import javax.inject.Inject;
+import javax.inject.Singleton;
 
 /**
  * Tracks the screen lifecycle.
  */
-@SysUISingleton
+@Singleton
 public class ScreenLifecycle extends Lifecycle<ScreenLifecycle.Observer> implements Dumpable {
 
     public static final int SCREEN_OFF = 0;
@@ -40,7 +41,8 @@
     private int mScreenState = SCREEN_OFF;
 
     @Inject
-    public ScreenLifecycle() {
+    public ScreenLifecycle(DumpManager dumpManager) {
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     public int getScreenState() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
index 6f878d1..2e1c9fa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
@@ -32,6 +32,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -83,10 +84,13 @@
     @Inject
     public WakefulnessLifecycle(
             Context context,
-            @Nullable IWallpaperManager wallpaperManagerService) {
+            @Nullable IWallpaperManager wallpaperManagerService,
+            DumpManager dumpManager) {
         mContext = context;
         mDisplayMetrics = context.getResources().getDisplayMetrics();
         mWallpaperManagerService = wallpaperManagerService;
+
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     public @Wakefulness int getWakefulness() {
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 8a383b9..11d4aac 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -55,6 +55,8 @@
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
+import com.android.systemui.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 import com.android.systemui.util.settings.GlobalSettings;
@@ -99,6 +101,8 @@
             NavigationModeController navigationModeController,
             KeyguardDisplayManager keyguardDisplayManager,
             DozeParameters dozeParameters,
+            UnfoldTransitionConfig unfoldTransitionConfig,
+            Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
             SysuiStatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
             Lazy<KeyguardUnlockAnimationController> keyguardUnlockAnimationController,
@@ -121,6 +125,8 @@
                 navigationModeController,
                 keyguardDisplayManager,
                 dozeParameters,
+                unfoldTransitionConfig,
+                unfoldLightRevealOverlayAnimation,
                 statusBarStateController,
                 keyguardStateController,
                 keyguardUnlockAnimationController,
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
new file mode 100644
index 0000000..c8afd72
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for
+ * {@link com.android.systemui.statusbar.phone.CollapsedStatusBarFragment}-related messages.
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface CollapsedSbFragmentLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 19193f9..72601e9 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -100,6 +100,28 @@
         return factory.create("PrivacyLog", 100);
     }
 
+    /**
+     * Provides a logging buffer for
+     * {@link com.android.systemui.statusbar.phone.CollapsedStatusBarFragment}.
+     */
+    @Provides
+    @SysUISingleton
+    @CollapsedSbFragmentLog
+    public static LogBuffer provideCollapsedSbFragmentLogBuffer(LogBufferFactory factory) {
+        return factory.create("CollapsedSbFragmentLog", 20);
+    }
+
+    /**
+     * Provides a logging buffer for logs related to swiping away the status bar while in immersive
+     * mode. See {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
+     */
+    @Provides
+    @SysUISingleton
+    @SwipeStatusBarAwayLog
+    public static LogBuffer provideSwipeAwayGestureLogBuffer(LogBufferFactory factory) {
+        return factory.create("SwipeStatusBarAwayLog", 30);
+    }
+
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
new file mode 100644
index 0000000..dd68375
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for
+ * {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface SwipeStatusBarAwayLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index 2bf102f7..5ff624d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.dagger.MediaModule.KEYGUARD
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.FeatureFlags
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -45,7 +44,6 @@
     private val bypassController: KeyguardBypassController,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val notifLockscreenUserManager: NotificationLockscreenUserManager,
-    private val featureFlags: FeatureFlags,
     private val context: Context,
     configurationController: ConfigurationController
 ) {
@@ -73,7 +71,7 @@
     }
 
     private fun updateResources() {
-        useSplitShade = Utils.shouldUseSplitNotificationShade(featureFlags, context.resources)
+        useSplitShade = Utils.shouldUseSplitNotificationShade(context.resources)
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index c743fe1..e87558e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -148,7 +148,7 @@
             inflateSettingsButton()
         }
 
-        override fun onOverlayChanged() {
+        override fun onThemeChanged() {
             recreatePlayers()
             inflateSettingsButton()
         }
@@ -833,11 +833,12 @@
     )
 
     private val comparator =
-            compareByDescending<MediaSortKey> { it.data.isPlaying }
+            compareByDescending<MediaSortKey> { it.data.isPlaying == true && it.data.isLocalSession }
+                    .thenByDescending { it.data.isPlaying }
                     .thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec }
-                    .thenByDescending { it.data.isLocalSession }
                     .thenByDescending { !it.data.resumption }
                     .thenByDescending { it.updateTime }
+                    .thenByDescending { !it.data.isLocalSession }
 
     private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
     private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index e7445f9..e73eb66 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -55,6 +55,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.util.animation.TransitionLayout;
@@ -119,6 +120,7 @@
     private int mSmartspaceMediaItemsCount;
     private MediaCarouselController mMediaCarouselController;
     private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final FalsingManager mFalsingManager;
 
     /**
      * Initialize a new control panel
@@ -131,7 +133,8 @@
             ActivityStarter activityStarter, MediaViewController mediaViewController,
             SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
             KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
-            mediaOutputDialogFactory, MediaCarouselController mediaCarouselController) {
+            mediaOutputDialogFactory, MediaCarouselController mediaCarouselController,
+            FalsingManager falsingManager) {
         mContext = context;
         mBackgroundExecutor = backgroundExecutor;
         mActivityStarter = activityStarter;
@@ -141,6 +144,7 @@
         mKeyguardDismissUtil = keyguardDismissUtil;
         mMediaOutputDialogFactory = mediaOutputDialogFactory;
         mMediaCarouselController = mediaCarouselController;
+        mFalsingManager = falsingManager;
         loadDimens();
 
         mSeekBarViewModel.setLogSmartspaceClick(() -> {
@@ -235,10 +239,14 @@
             }
         });
         mPlayerViewHolder.getCancel().setOnClickListener(v -> {
-            closeGuts();
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                closeGuts();
+            }
         });
         mPlayerViewHolder.getSettings().setOnClickListener(v -> {
-            mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            }
         });
     }
 
@@ -259,10 +267,14 @@
             }
         });
         mRecommendationViewHolder.getCancel().setOnClickListener(v -> {
-            closeGuts();
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                closeGuts();
+            }
         });
         mRecommendationViewHolder.getSettings().setOnClickListener(v -> {
-            mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            }
         });
     }
 
@@ -299,6 +311,7 @@
         PendingIntent clickIntent = data.getClickIntent();
         if (clickIntent != null) {
             mPlayerViewHolder.getPlayer().setOnClickListener(v -> {
+                if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
                 if (mMediaViewController.isGutsVisible()) return;
 
                 logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
@@ -364,9 +377,13 @@
         seamlessView.setVisibility(View.VISIBLE);
         setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
         setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
-        seamlessView.setOnClickListener(v -> {
-            mMediaOutputDialogFactory.create(data.getPackageName(), true);
-        });
+        seamlessView.setOnClickListener(
+                v -> {
+                    if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                        mMediaOutputDialogFactory.create(data.getPackageName(), true,
+                                mPlayerViewHolder.getSeamlessButton());
+                    }
+                });
 
         ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
         TextView deviceName = mPlayerViewHolder.getSeamlessText();
@@ -417,9 +434,11 @@
             } else {
                 button.setEnabled(true);
                 button.setOnClickListener(v -> {
-                    logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
-                            /* isRecommendationCard */ false);
-                    action.run();
+                    if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                        logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
+                                /* isRecommendationCard */ false);
+                        action.run();
+                    }
                 });
             }
             boolean visibleInCompat = actionsWhenCollapsed.contains(i);
@@ -451,6 +470,8 @@
         mPlayerViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
         mPlayerViewHolder.getDismiss().setEnabled(isDismissible);
         mPlayerViewHolder.getDismiss().setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+
             logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                     /* isRecommendationCard */ false);
 
@@ -633,6 +654,8 @@
         mSmartspaceMediaItemsCount = uiComponentIndex;
         // Set up long press to show guts setting panel.
         mRecommendationViewHolder.getDismiss().setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+
             logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                     /* isRecommendationCard */ true);
             closeGuts();
@@ -788,6 +811,8 @@
         }
 
         view.setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+
             logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
                     /* isRecommendationCard */ true,
                     interactedSubcardRank,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index eacdab6..3631d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -768,7 +768,7 @@
             val resumeAction = getResumeMediaAction(removed.resumeAction!!)
             val updated = removed.copy(token = null, actions = listOf(resumeAction),
                     actionsToShowInCompact = listOf(0), active = false, resumption = true,
-                    isClearable = true)
+                    isPlaying = false, isClearable = true)
             val pkg = removed.packageName
             val migrate = mediaEntries.put(pkg, updated) == null
             // Notify listeners of "new" controls when migrating or removed and update when not
@@ -889,7 +889,7 @@
             dismissIntent = target
                 .baseAction
                 .extras
-                .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent
+                .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
         }
         packageName(target)?.let {
             return SmartspaceMediaData(target.smartspaceTargetId, isActive, true, it,
diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
index 35603b6..f32dad6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
@@ -43,6 +43,7 @@
     val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
     val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
     val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
+    val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
 
     // Seek bar
     val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
index f17ad6f..33ef19a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
@@ -50,6 +50,7 @@
             holder.seekBar.setProgress(0)
             holder.elapsedTimeView.setText("")
             holder.totalTimeView.setText("")
+            holder.seekBar.contentDescription = ""
             return
         }
 
@@ -61,16 +62,22 @@
             setVerticalPadding(seekBarEnabledVerticalPadding)
         }
 
-        data.duration?.let {
-            holder.seekBar.setMax(it)
-            holder.totalTimeView.setText(DateUtils.formatElapsedTime(
-                    it / DateUtils.SECOND_IN_MILLIS))
-        }
+        holder.seekBar.setMax(data.duration)
+        val totalTimeString = DateUtils.formatElapsedTime(
+            data.duration / DateUtils.SECOND_IN_MILLIS)
+        holder.totalTimeView.setText(totalTimeString)
 
         data.elapsedTime?.let {
             holder.seekBar.setProgress(it)
-            holder.elapsedTimeView.setText(DateUtils.formatElapsedTime(
-                    it / DateUtils.SECOND_IN_MILLIS))
+            val elapsedTimeString = DateUtils.formatElapsedTime(
+                it / DateUtils.SECOND_IN_MILLIS)
+            holder.elapsedTimeView.setText(elapsedTimeString)
+
+            holder.seekBar.contentDescription = holder.seekBar.context.getString(
+                R.string.controls_media_seekbar_description,
+                elapsedTimeString,
+                totalTimeString
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 391dff63..125b87b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -45,11 +45,14 @@
     private static final String TAG = "MediaOutputAdapter";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
+    private final MediaOutputDialog mMediaOutputDialog;
     private ViewGroup mConnectedItem;
     private boolean mIncludeDynamicGroup;
 
-    public MediaOutputAdapter(MediaOutputController controller) {
+    public MediaOutputAdapter(MediaOutputController controller,
+            MediaOutputDialog mediaOutputDialog) {
         super(controller);
+        mMediaOutputDialog = mediaOutputDialog;
     }
 
     @Override
@@ -132,14 +135,11 @@
             if (currentlyConnected && mController.isActiveRemoteDevice(device)
                     && mController.getSelectableMediaDevice().size() > 0) {
                 // Init active device layout
-                mDivider.setVisibility(View.VISIBLE);
-                mDivider.setTransitionAlpha(1);
                 mAddIcon.setVisibility(View.VISIBLE);
                 mAddIcon.setTransitionAlpha(1);
-                mAddIcon.setOnClickListener(v -> onEndItemClick());
+                mAddIcon.setOnClickListener(this::onEndItemClick);
             } else {
                 // Init non-active device layout
-                mDivider.setVisibility(View.GONE);
                 mAddIcon.setVisibility(View.GONE);
             }
             if (mCurrentActivePosition == position) {
@@ -178,7 +178,6 @@
             super.onBind(customizedItem, topMargin, bottomMargin);
             if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
                 mCheckBox.setVisibility(View.GONE);
-                mDivider.setVisibility(View.GONE);
                 mAddIcon.setVisibility(View.GONE);
                 mBottomDivider.setVisibility(View.GONE);
                 setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
@@ -193,13 +192,10 @@
                 mBottomDivider.setVisibility(View.GONE);
                 mCheckBox.setVisibility(View.GONE);
                 if (mController.getSelectableMediaDevice().size() > 0) {
-                    mDivider.setVisibility(View.VISIBLE);
-                    mDivider.setTransitionAlpha(1);
                     mAddIcon.setVisibility(View.VISIBLE);
                     mAddIcon.setTransitionAlpha(1);
-                    mAddIcon.setOnClickListener(v -> onEndItemClick());
+                    mAddIcon.setOnClickListener(this::onEndItemClick);
                 } else {
-                    mDivider.setVisibility(View.GONE);
                     mAddIcon.setVisibility(View.GONE);
                 }
                 mTitleIcon.setImageDrawable(getSpeakerDrawable());
@@ -232,8 +228,8 @@
             }
         }
 
-        private void onEndItemClick() {
-            mController.launchMediaOutputGroupDialog();
+        private void onEndItemClick(View view) {
+            mController.launchMediaOutputGroupDialog(mMediaOutputDialog.getDialogView());
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 0890841..1ffc2c4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -121,7 +121,6 @@
         final ProgressBar mProgressBar;
         final SeekBar mSeekBar;
         final RelativeLayout mTwoLineLayout;
-        final View mDivider;
         final View mBottomDivider;
         final CheckBox mCheckBox;
         private String mDeviceId;
@@ -136,7 +135,6 @@
             mTitleIcon = view.requireViewById(R.id.title_icon);
             mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
             mSeekBar = view.requireViewById(R.id.volume_seekbar);
-            mDivider = view.requireViewById(R.id.end_divider);
             mBottomDivider = view.requireViewById(R.id.bottom_divider);
             mAddIcon = view.requireViewById(R.id.add_icon);
             mCheckBox = view.requireViewById(R.id.check_box);
@@ -151,21 +149,12 @@
                         return;
                     }
                     mTitleIcon.setImageIcon(icon);
-                    setMargin(topMargin, bottomMargin);
                 });
             });
         }
 
         void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
-            setMargin(topMargin, bottomMargin);
-        }
-
-        private void setMargin(boolean topMargin, boolean bottomMargin) {
-            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mContainerLayout
-                    .getLayoutParams();
-            params.topMargin = topMargin ? mMargin : 0;
-            params.bottomMargin = bottomMargin ? mMargin : 0;
-            mContainerLayout.setLayoutParams(params);
+            // TODO (b/201718621): clean up method after adjustment
         }
 
         void setSingleLineLayout(CharSequence title, boolean bFocused) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index cdcdf9a..6895ef1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -82,7 +82,7 @@
     };
 
     public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
-        super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
+        super(context);
         mContext = context;
         mMediaOutputController = mediaOutputController;
         mLayoutManager = new LinearLayoutManager(mContext);
@@ -97,15 +97,15 @@
         mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
         final Window window = getWindow();
         final WindowManager.LayoutParams lp = window.getAttributes();
-        lp.gravity = Gravity.BOTTOM;
+        lp.gravity = Gravity.CENTER;
         // Config insets to make sure the layout is above the navigation bar
         lp.setFitInsetsTypes(statusBars() | navigationBars());
         lp.setFitInsetsSides(WindowInsets.Side.all());
         lp.setFitInsetsIgnoringVisibility(true);
         window.setAttributes(lp);
         window.setContentView(mDialogView);
-        window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-        window.setWindowAnimations(R.style.Animation_MediaOutputDialog);
+        window.setLayout(mContext.getResources().getDimensionPixelSize(R.dimen.large_dialog_width),
+                ViewGroup.LayoutParams.WRAP_CONTENT);
 
         mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
         mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
@@ -175,7 +175,7 @@
         }
         if (!mAdapter.isDragging() && !mAdapter.isAnimating()) {
             int currentActivePosition = mAdapter.getCurrentActivePosition();
-            if (currentActivePosition >= 0) {
+            if (currentActivePosition >= 0 && currentActivePosition < mAdapter.getItemCount()) {
                 mAdapter.notifyItemChanged(currentActivePosition);
             } else {
                 mAdapter.notifyDataSetChanged();
@@ -229,4 +229,8 @@
 
     void onHeaderIconClick() {
     }
+
+    View getDialogView() {
+        return mDialogView;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index b2def7a..42dd886 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -32,6 +32,7 @@
 import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.Log;
+import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
@@ -48,6 +49,7 @@
 import com.android.settingslib.media.MediaOutputConstants;
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.R;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -71,8 +73,10 @@
     private final String mPackageName;
     private final Context mContext;
     private final MediaSessionManager mMediaSessionManager;
+    private final LocalBluetoothManager mLocalBluetoothManager;
     private final ShadeController mShadeController;
     private final ActivityStarter mActivityStarter;
+    private final DialogLaunchAnimator mDialogLaunchAnimator;
     private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
     private final boolean mAboveStatusbar;
     private final NotificationEntryManager mNotificationEntryManager;
@@ -92,10 +96,12 @@
     public MediaOutputController(@NonNull Context context, String packageName,
             boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
             lbm, ShadeController shadeController, ActivityStarter starter,
-            NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger) {
+            NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger,
+            DialogLaunchAnimator dialogLaunchAnimator) {
         mContext = context;
         mPackageName = packageName;
         mMediaSessionManager = mediaSessionManager;
+        mLocalBluetoothManager = lbm;
         mShadeController = shadeController;
         mActivityStarter = starter;
         mAboveStatusbar = aboveStatusbar;
@@ -104,6 +110,7 @@
         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
         mUiEventLogger = uiEventLogger;
+        mDialogLaunchAnimator = dialogLaunchAnimator;
     }
 
     void start(@NonNull Callback cb) {
@@ -436,6 +443,10 @@
     }
 
     void launchBluetoothPairing() {
+        // Dismissing a dialog into its touch surface and starting an activity at the same time
+        // looks bad, so let's make sure the dialog just fades out quickly.
+        mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
+
         mCallback.dismissDialog();
         final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
             mContext.sendBroadcast(new Intent()
@@ -447,14 +458,14 @@
         mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
     }
 
-    void launchMediaOutputDialog() {
-        mCallback.dismissDialog();
-        new MediaOutputDialog(mContext, mAboveStatusbar, this, mUiEventLogger);
-    }
-
-    void launchMediaOutputGroupDialog() {
-        mCallback.dismissDialog();
-        new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
+    void launchMediaOutputGroupDialog(View mediaOutputDialog) {
+        // We show the output group dialog from the output dialog.
+        MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
+                mAboveStatusbar, mMediaSessionManager, mLocalBluetoothManager, mShadeController,
+                mActivityStarter, mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
+        MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar,
+                controller);
+        mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
     }
 
     boolean isActiveRemoteDevice(@NonNull MediaDevice device) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 53029bd0..eca8ac9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -40,11 +40,10 @@
             mediaOutputController, UiEventLogger uiEventLogger) {
         super(context, mediaOutputController);
         mUiEventLogger = uiEventLogger;
-        mAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mAdapter = new MediaOutputAdapter(mMediaOutputController, this);
         if (!aboveStatusbar) {
             getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
         }
-        show();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 0f340a5..b91901d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -18,8 +18,10 @@
 
 import android.content.Context
 import android.media.session.MediaSessionManager
+import android.view.View
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.notification.NotificationEntryManager
 import com.android.systemui.statusbar.phone.ShadeController
@@ -35,19 +37,29 @@
     private val shadeController: ShadeController,
     private val starter: ActivityStarter,
     private val notificationEntryManager: NotificationEntryManager,
-    private val uiEventLogger: UiEventLogger
+    private val uiEventLogger: UiEventLogger,
+    private val dialogLaunchAnimator: DialogLaunchAnimator
 ) {
     companion object {
         var mediaOutputDialog: MediaOutputDialog? = null
     }
 
     /** Creates a [MediaOutputDialog] for the given package. */
-    fun create(packageName: String, aboveStatusBar: Boolean) {
+    fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
+        // Dismiss the previous dialog, if any.
         mediaOutputDialog?.dismiss()
-        mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar,
-                mediaSessionManager, lbm, shadeController, starter, notificationEntryManager,
-                uiEventLogger).run {
-            MediaOutputDialog(context, aboveStatusBar, this, uiEventLogger)
+
+        val controller = MediaOutputController(context, packageName, aboveStatusBar,
+            mediaSessionManager, lbm, shadeController, starter, notificationEntryManager,
+            uiEventLogger, dialogLaunchAnimator)
+        val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger)
+        mediaOutputDialog = dialog
+
+        // Show the dialog.
+        if (view != null) {
+            dialogLaunchAnimator.showFromView(dialog, view)
+        } else {
+            dialog.show()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
index 968c350..11d76db 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
@@ -96,7 +96,6 @@
         @Override
         void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
             super.onBind(device, topMargin, bottomMargin, position);
-            mDivider.setVisibility(View.GONE);
             mAddIcon.setVisibility(View.GONE);
             mBottomDivider.setVisibility(View.GONE);
             mCheckBox.setVisibility(View.VISIBLE);
@@ -135,7 +134,6 @@
                 mTitleIcon.setImageDrawable(getSpeakerDrawable());
                 mBottomDivider.setVisibility(View.VISIBLE);
                 mCheckBox.setVisibility(View.GONE);
-                mDivider.setVisibility(View.GONE);
                 mAddIcon.setVisibility(View.GONE);
                 initSessionSeekbar();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
index 4079304..1300400 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
@@ -38,7 +38,6 @@
         if (!aboveStatusbar) {
             getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
         }
-        show();
     }
 
     @Override
@@ -83,6 +82,8 @@
 
     @Override
     void onHeaderIconClick() {
-        mMediaOutputController.launchMediaOutputDialog();
+        // Given that we launched the media output group dialog from the media output dialog,
+        // dismissing this dialog will show the media output dialog again.
+        dismiss();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 8e6eb02..7809b5f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -23,10 +23,14 @@
 import static android.app.StatusBarManager.WindowType;
 import static android.app.StatusBarManager.WindowVisibleState;
 import static android.app.StatusBarManager.windowStateToString;
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
+import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
+import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.containsType;
+import static android.view.ViewRootImpl.INSETS_LAYOUT_GENERALIZATION;
 import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS;
@@ -40,10 +44,12 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS;
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_FORCE_OPAQUE;
 import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
+import static com.android.systemui.shared.recents.utilities.Utilities.isTablet;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
@@ -54,9 +60,7 @@
 import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_WINDOW_STATE;
 import static com.android.systemui.statusbar.phone.StatusBar.dumpBarTransitions;
 
-import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.IdRes;
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.IActivityTaskManager;
@@ -68,6 +72,7 @@
 import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
+import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -88,8 +93,8 @@
 import android.view.Display;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
-import android.view.IWindowManager;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -101,7 +106,6 @@
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.VisibleForTesting;
@@ -113,7 +117,6 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.view.AppearanceRegion;
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.SystemActions;
@@ -125,11 +128,11 @@
 import com.android.systemui.navigationbar.buttons.KeyButtonView;
 import com.android.systemui.navigationbar.buttons.RotationContextButton;
 import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle;
-import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.AutoHideUiElement;
@@ -144,26 +147,24 @@
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
 import com.android.wm.shell.pip.Pip;
 
 import java.io.PrintWriter;
-import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.function.Consumer;
 
+import javax.inject.Inject;
+
 import dagger.Lazy;
 
 /**
  * Contains logic for a navigation bar view.
  */
 public class NavigationBar implements View.OnAttachStateChangeListener,
-        Callbacks, NavigationModeController.ModeChangedListener,
-        AccessibilityButtonModeObserver.ModeChangedListener {
+        Callbacks, NavigationModeController.ModeChangedListener {
 
     public static final String TAG = "NavigationBar";
     private static final boolean DEBUG = false;
@@ -180,13 +181,12 @@
     private final Context mContext;
     private final WindowManager mWindowManager;
     private final AccessibilityManager mAccessibilityManager;
-    private final AccessibilityManagerWrapper mAccessibilityManagerWrapper;
     private final DeviceProvisionedController mDeviceProvisionedController;
     private final StatusBarStateController mStatusBarStateController;
     private final MetricsLogger mMetricsLogger;
     private final Lazy<AssistManager> mAssistManagerLazy;
     private final SysUiState mSysUiFlagsContainer;
-    private final Lazy<StatusBar> mStatusBarLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private final ShadeController mShadeController;
     private final NotificationRemoteInputManager mNotificationRemoteInputManager;
     private final OverviewProxyService mOverviewProxyService;
@@ -201,11 +201,13 @@
     private final Handler mHandler;
     private final NavigationBarOverlayController mNavbarOverlayController;
     private final UiEventLogger mUiEventLogger;
+    private final NavigationBarA11yHelper mNavigationBarA11yHelper;
     private final UserTracker mUserTracker;
     private final NotificationShadeDepthController mNotificationShadeDepthController;
 
     private Bundle mSavedState;
     private NavigationBarView mNavigationBarView;
+    private NavigationBarFrame mFrame;
 
     private @WindowVisibleState int mNavigationBarWindowState = WINDOW_STATE_SHOWING;
 
@@ -237,7 +239,13 @@
     private boolean mTransientShown;
     private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
     private LightBarController mLightBarController;
+    private final LightBarController mMainLightBarController;
+    private final LightBarController.Factory mLightBarControllerFactory;
     private AutoHideController mAutoHideController;
+    private final AutoHideController mMainAutoHideController;
+    private final AutoHideController.Factory mAutoHideControllerFactory;
+    private final Optional<TelecomManager> mTelecomManagerOptional;
+    private final InputMethodManager mInputMethodManager;
 
     @VisibleForTesting
     public int mDisplayId;
@@ -261,6 +269,7 @@
     private ViewTreeObserver.OnGlobalLayoutListener mOrientationHandleGlobalLayoutListener;
     private boolean mShowOrientedHandleForImmersiveMode;
 
+
     @com.android.internal.annotations.VisibleForTesting
     public enum NavBarActionEvent implements UiEventLogger.UiEventEnum {
 
@@ -287,7 +296,7 @@
 
         @Override
         public boolean shouldHideOnTouch() {
-            return !mNotificationRemoteInputManager.getController().isRemoteInputActive();
+            return !mNotificationRemoteInputManager.isRemoteInputActive();
         }
 
         @Override
@@ -381,6 +390,13 @@
         }
 
         @Override
+        public void onTaskbarStatusUpdated(boolean visible, boolean stashed) {
+            mNavigationBarView
+                    .getFloatingRotationButton()
+                    .onTaskbarStateChanged(visible, stashed);
+        }
+
+        @Override
         public void onToggleRecentApps() {
             // The same case as onOverviewShown but only for 3-button navigation.
             mNavigationBarView.getRotationButtonController().setSkipOverrideUserLockPrefsOnce();
@@ -465,11 +481,10 @@
                 }
             };
 
-    public NavigationBar(Context context,
+    private NavigationBar(Context context,
             WindowManager windowManager,
             Lazy<AssistManager> assistManagerLazy,
             AccessibilityManager accessibilityManager,
-            AccessibilityManagerWrapper accessibilityManagerWrapper,
             DeviceProvisionedController deviceProvisionedController,
             MetricsLogger metricsLogger,
             OverviewProxyService overviewProxyService,
@@ -481,7 +496,8 @@
             CommandQueue commandQueue,
             Optional<Pip> pipOptional,
             Optional<LegacySplitScreen> splitScreenOptional,
-            Optional<Recents> recentsOptional, Lazy<StatusBar> statusBarLazy,
+            Optional<Recents> recentsOptional,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             ShadeController shadeController,
             NotificationRemoteInputManager notificationRemoteInputManager,
             NotificationShadeDepthController notificationShadeDepthController,
@@ -489,17 +505,23 @@
             @Main Handler mainHandler,
             NavigationBarOverlayController navbarOverlayController,
             UiEventLogger uiEventLogger,
-            UserTracker userTracker) {
+            NavigationBarA11yHelper navigationBarA11yHelper,
+            UserTracker userTracker,
+            LightBarController mainLightBarController,
+            LightBarController.Factory lightBarControllerFactory,
+            AutoHideController mainAutoHideController,
+            AutoHideController.Factory autoHideControllerFactory,
+            Optional<TelecomManager> telecomManagerOptional,
+            InputMethodManager inputMethodManager) {
         mContext = context;
         mWindowManager = windowManager;
         mAccessibilityManager = accessibilityManager;
-        mAccessibilityManagerWrapper = accessibilityManagerWrapper;
         mDeviceProvisionedController = deviceProvisionedController;
         mStatusBarStateController = statusBarStateController;
         mMetricsLogger = metricsLogger;
         mAssistManagerLazy = assistManagerLazy;
         mSysUiFlagsContainer = sysUiFlagsContainer;
-        mStatusBarLazy = statusBarLazy;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
         mShadeController = shadeController;
         mNotificationRemoteInputManager = notificationRemoteInputManager;
         mOverviewProxyService = overviewProxyService;
@@ -514,11 +536,17 @@
         mHandler = mainHandler;
         mNavbarOverlayController = navbarOverlayController;
         mUiEventLogger = uiEventLogger;
+        mNavigationBarA11yHelper = navigationBarA11yHelper;
         mUserTracker = userTracker;
         mNotificationShadeDepthController = notificationShadeDepthController;
+        mMainLightBarController = mainLightBarController;
+        mLightBarControllerFactory = lightBarControllerFactory;
+        mMainAutoHideController = mainAutoHideController;
+        mAutoHideControllerFactory = autoHideControllerFactory;
+        mTelecomManagerOptional = telecomManagerOptional;
+        mInputMethodManager = inputMethodManager;
 
         mNavBarMode = mNavigationModeController.addListener(this);
-        mAccessibilityButtonModeObserver.addListener(this);
     }
 
     public NavigationBarView getView() {
@@ -526,34 +554,17 @@
     }
 
     public View createView(Bundle savedState) {
-        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
-                WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT,
-                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
-                WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
-                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
-                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
-                        | WindowManager.LayoutParams.FLAG_SLIPPERY,
-                PixelFormat.TRANSLUCENT);
-        lp.token = new Binder();
-        lp.accessibilityTitle = mContext.getString(R.string.nav_bar);
-        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
-        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-        lp.windowAnimations = 0;
-        lp.setTitle("NavigationBar" + mContext.getDisplayId());
-        lp.setFitInsetsTypes(0 /* types */);
-        lp.setTrustedOverlay();
-
-        NavigationBarFrame frame = (NavigationBarFrame) LayoutInflater.from(mContext).inflate(
+        mFrame = (NavigationBarFrame) LayoutInflater.from(mContext).inflate(
                 R.layout.navigation_bar_window, null);
-        View barView = LayoutInflater.from(frame.getContext()).inflate(
-                R.layout.navigation_bar, frame);
+        View barView = LayoutInflater.from(mFrame.getContext()).inflate(
+                R.layout.navigation_bar, mFrame);
         barView.addOnAttachStateChangeListener(this);
         mNavigationBarView = barView.findViewById(R.id.navigation_bar_view);
 
         if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + barView);
-        mContext.getSystemService(WindowManager.class).addView(frame, lp);
+        mWindowManager.addView(mFrame,
+                getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
+                        .getRotation()));
         mDisplayId = mContext.getDisplayId();
         mIsOnDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
 
@@ -601,7 +612,7 @@
         mDeviceProvisionedController.addCallback(mUserSetupListener);
         mNotificationShadeDepthController.addListener(mDepthListener);
 
-        setAccessibilityFloatingMenuModeIfNeeded();
+        updateAccessibilityButtonModeIfNeeded();
 
         return barView;
     }
@@ -609,12 +620,10 @@
     public void destroyView() {
         setAutoHideController(/* autoHideController */ null);
         mCommandQueue.removeCallback(this);
-        mContext.getSystemService(WindowManager.class).removeViewImmediate(
-                mNavigationBarView.getRootView());
+        mWindowManager.removeViewImmediate(mNavigationBarView.getRootView());
         mNavigationModeController.removeListener(this);
-        mAccessibilityButtonModeObserver.removeListener(this);
 
-        mAccessibilityManagerWrapper.removeCallback(mAccessibilityListener);
+        mNavigationBarA11yHelper.removeA11yEventListener(mAccessibilityListener);
         mContentResolver.unregisterContentObserver(mAssistContentObserver);
         mDeviceProvisionedController.removeCallback(mUserSetupListener);
         mNotificationShadeDepthController.removeListener(mDepthListener);
@@ -625,7 +634,8 @@
     @Override
     public void onViewAttachedToWindow(View v) {
         final Display display = v.getDisplay();
-        mNavigationBarView.setComponents(mStatusBarLazy.get().getPanelController());
+        mNavigationBarView.setComponents(mRecentsOptional);
+        mNavigationBarView.setComponents(mStatusBarOptionalLazy.get().get().getPanelController());
         mNavigationBarView.setDisabledFlags(mDisabledFlags1);
         mNavigationBarView.setOnVerticalChangedListener(this::onVerticalChanged);
         mNavigationBarView.setOnTouchListener(this::onNavigationTouch);
@@ -636,7 +646,7 @@
         mNavigationBarView.setWindowVisible(isNavBarWindowVisible());
         mNavigationBarView.setBehavior(mBehavior);
 
-        mAccessibilityManagerWrapper.addCallback(mAccessibilityListener);
+        mNavigationBarA11yHelper.registerA11yEventListener(mAccessibilityListener);
 
         mSplitScreenOptional.ifPresent(mNavigationBarView::registerDockedListener);
         mPipOptional.ifPresent(mNavigationBarView::registerPipExclusionBoundsChangeListener);
@@ -676,21 +686,16 @@
         // before notifications creation. We cannot directly use getLightBarController()
         // from NavigationBarFragment directly.
         LightBarController lightBarController = mIsOnDefaultDisplay
-                ? Dependency.get(LightBarController.class)
-                : new LightBarController(mContext,
-                        Dependency.get(DarkIconDispatcher.class),
-                        Dependency.get(BatteryController.class),
-                        Dependency.get(NavigationModeController.class));
+                ? mMainLightBarController : mLightBarControllerFactory.create(mContext);
         setLightBarController(lightBarController);
 
         // TODO(b/118592525): to support multi-display, we start to add something which is
         //                    per-display, while others may be global. I think it's time to
         //                    add a new class maybe named DisplayDependency to solve
         //                    per-display Dependency problem.
+        // Alternative: this is a good case for a Dagger subcomponent. Same with LightBarController.
         AutoHideController autoHideController = mIsOnDefaultDisplay
-                ? Dependency.get(AutoHideController.class)
-                : new AutoHideController(mContext, mHandler,
-                        Dependency.get(IWindowManager.class));
+                ? mMainAutoHideController : mAutoHideControllerFactory.create(mContext);
         setAutoHideController(autoHideController);
         restoreAppearanceAndTransientState();
     }
@@ -714,6 +719,8 @@
         mHandler.removeCallbacks(mAutoDim);
         mHandler.removeCallbacks(mOnVariableDurationHomeLongClick);
         mHandler.removeCallbacks(mEnableLayoutTransitions);
+        mNavigationBarA11yHelper.removeA11yEventListener(mAccessibilityListener);
+        mFrame = null;
         mNavigationBarView = null;
         mOrientationHandle = null;
     }
@@ -732,6 +739,7 @@
      * Called when a non-reloading configuration change happens and we need to update.
      */
     public void onConfigurationChanged(Configuration newConfig) {
+        final int rotation = newConfig.windowConfiguration.getRotation();
         final Locale locale = mContext.getResources().getConfiguration().locale;
         final int ld = TextUtils.getLayoutDirectionFromLocale(locale);
         if (!locale.equals(mLocale) || ld != mLayoutDirection) {
@@ -745,9 +753,8 @@
             refreshLayout(ld);
         }
 
-        repositionNavigationBar();
+        repositionNavigationBar(rotation);
         if (canShowSecondaryHandle()) {
-            int rotation = newConfig.windowConfiguration.getRotation();
             if (rotation != mCurrentRotation) {
                 mCurrentRotation = rotation;
                 orientSecondaryHomeHandle();
@@ -899,30 +906,15 @@
             return;
         }
         boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0;
-        int hints = mNavigationIconHints;
-        switch (backDisposition) {
-            case InputMethodService.BACK_DISPOSITION_DEFAULT:
-            case InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS:
-            case InputMethodService.BACK_DISPOSITION_WILL_DISMISS:
-                if (imeShown) {
-                    hints |= NAVIGATION_HINT_BACK_ALT;
-                } else {
-                    hints &= ~NAVIGATION_HINT_BACK_ALT;
-                }
-                break;
-            case InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING:
-                hints &= ~NAVIGATION_HINT_BACK_ALT;
-                break;
-        }
-        if (showImeSwitcher) {
-            hints |= NAVIGATION_HINT_IME_SHOWN;
-        } else {
-            hints &= ~NAVIGATION_HINT_IME_SHOWN;
-        }
+        int hints = Utilities.calculateBackDispositionHints(mNavigationIconHints, backDisposition,
+                imeShown, showImeSwitcher);
         if (hints == mNavigationIconHints) return;
 
         mNavigationIconHints = hints;
-        mNavigationBarView.setNavigationIconHints(hints);
+        if (!isTablet(mContext)) {
+            // All IME functions handled by launcher via Sysui flags for large screen
+            mNavigationBarView.setNavigationIconHints(hints);
+        }
         checkBarModes();
         updateSystemUiStateFlags(-1);
     }
@@ -999,7 +991,7 @@
     @Override
     public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, boolean isFullscreen) {
+            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName) {
         if (displayId != mDisplayId) {
             return;
         }
@@ -1131,13 +1123,12 @@
                 || (mDisabledFlags1 & StatusBarManager.DISABLE_SEARCH) != 0;
     }
 
-    private void repositionNavigationBar() {
-        if (!mNavigationBarView.isAttachedToWindow()) return;
+    private void repositionNavigationBar(int rotation) {
+        if (mNavigationBarView == null || !mNavigationBarView.isAttachedToWindow()) return;
 
         prepareNavigationBarView();
 
-        mWindowManager.updateViewLayout((View) mNavigationBarView.getParent(),
-                ((View) mNavigationBarView.getParent()).getLayoutParams());
+        mWindowManager.updateViewLayout(mFrame, getBarLayoutParams(rotation));
     }
 
     private void updateScreenPinningGestures() {
@@ -1179,7 +1170,7 @@
         ButtonDispatcher accessibilityButton = mNavigationBarView.getAccessibilityButton();
         accessibilityButton.setOnClickListener(this::onAccessibilityClick);
         accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
-        updateAccessibilityServicesState(mAccessibilityManager);
+        updateAccessibilityServicesState();
 
         ButtonDispatcher imeSwitcherButton = mNavigationBarView.getImeSwitchButton();
         imeSwitcherButton.setOnClickListener(this::onImeSwitcherClick);
@@ -1195,13 +1186,13 @@
         // If an incoming call is ringing, HOME is totally disabled.
         // (The user is already on the InCallUI at this point,
         // and their ONLY options are to answer or reject the call.)
+        final Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 mHomeBlockedThisTouch = false;
-                TelecomManager telecomManager =
-                        mContext.getSystemService(TelecomManager.class);
-                if (telecomManager != null && telecomManager.isRinging()) {
-                    if (mStatusBarLazy.get().isKeyguardShowing()) {
+                if (mTelecomManagerOptional.isPresent()
+                        && mTelecomManagerOptional.get().isRinging()) {
+                    if (statusBarOptional.map(StatusBar::isKeyguardShowing).orElse(false)) {
                         Log.i(TAG, "Ignoring HOME; there's a ringing incoming call. " +
                                 "No heads up");
                         mHomeBlockedThisTouch = true;
@@ -1217,14 +1208,15 @@
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
                 mHandler.removeCallbacks(mOnVariableDurationHomeLongClick);
-                mStatusBarLazy.get().awakenDreams();
+                statusBarOptional.ifPresent(StatusBar::awakenDreams);
                 break;
         }
         return false;
     }
 
     private void onVerticalChanged(boolean isVertical) {
-        mStatusBarLazy.get().setQsScrimEnabled(!isVertical);
+        mStatusBarOptionalLazy.get().ifPresent(
+                statusBar -> statusBar.setQsScrimEnabled(!isVertical));
     }
 
     private boolean onNavigationTouch(View v, MotionEvent event) {
@@ -1250,7 +1242,7 @@
                 AssistManager.INVOCATION_TYPE_KEY,
                 AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
         mAssistManagerLazy.get().startAssist(args);
-        mStatusBarLazy.get().awakenDreams();
+        mStatusBarOptionalLazy.get().ifPresent(StatusBar::awakenDreams);
         mNavigationBarView.abortCurrentGesture();
         return true;
     }
@@ -1276,12 +1268,12 @@
             LatencyTracker.getInstance(mContext).onActionStart(
                     LatencyTracker.ACTION_TOGGLE_RECENTS);
         }
-        mStatusBarLazy.get().awakenDreams();
+        mStatusBarOptionalLazy.get().ifPresent(StatusBar::awakenDreams);
         mCommandQueue.toggleRecentApps();
     }
 
     private void onImeSwitcherClick(View v) {
-        mContext.getSystemService(InputMethodManager.class).showInputMethodPickerFromSystem(
+        mInputMethodManager.showInputMethodPickerFromSystem(
                 true /* showAuxiliarySubtypes */, mDisplayId);
     };
 
@@ -1381,8 +1373,11 @@
             return false;
         }
 
-        return mStatusBarLazy.get().toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
-                MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
+        return mStatusBarOptionalLazy.get().map(
+                statusBar -> statusBar.toggleSplitScreenMode(
+                        MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
+                        MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS))
+            .orElse(false);
     }
 
     private void onAccessibilityClick(View v) {
@@ -1400,28 +1395,48 @@
         return true;
     }
 
-    void updateAccessibilityServicesState(AccessibilityManager accessibilityManager) {
-        boolean[] feedbackEnabled = new boolean[1];
-        int a11yFlags = getA11yButtonState(feedbackEnabled);
+    void updateAccessibilityServicesState() {
+        int a11yFlags = mNavigationBarA11yHelper.getA11yButtonState();
 
-        boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
-        boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
-        mNavigationBarView.setAccessibilityButtonState(clickable, longClickable);
-
+        if (mNavigationBarView != null) {
+            boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
+            boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
+            mNavigationBarView.setAccessibilityButtonState(clickable, longClickable);
+        }
         updateSystemUiStateFlags(a11yFlags);
     }
 
-    private void setAccessibilityFloatingMenuModeIfNeeded() {
-        if (QuickStepContract.isGesturalMode(mNavBarMode)) {
+    private void updateAccessibilityButtonModeIfNeeded() {
+        final int mode = Settings.Secure.getIntForUser(mContentResolver,
+                Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
+                ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
+
+        // ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU is compatible under gestural or non-gestural
+        // mode, so we don't need to update it.
+        if (mode == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) {
+            return;
+        }
+
+        // ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR is incompatible under gestural mode. Need to
+        // force update to ACCESSIBILITY_BUTTON_MODE_GESTURE.
+        if (QuickStepContract.isGesturalMode(mNavBarMode)
+                && mode == ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR) {
+            Settings.Secure.putIntForUser(mContentResolver,
+                    Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_GESTURE,
+                    UserHandle.USER_CURRENT);
+            // ACCESSIBILITY_BUTTON_MODE_GESTURE is incompatible under non gestural mode. Need to
+            // force update to ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR.
+        } else if (!QuickStepContract.isGesturalMode(mNavBarMode)
+                && mode == ACCESSIBILITY_BUTTON_MODE_GESTURE) {
             Settings.Secure.putIntForUser(mContentResolver,
                     Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
-                    ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU, UserHandle.USER_CURRENT);
+                    ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
         }
     }
 
     public void updateSystemUiStateFlags(int a11yFlags) {
         if (a11yFlags < 0) {
-            a11yFlags = getA11yButtonState(null);
+            a11yFlags = mNavigationBarA11yHelper.getA11yButtonState();
         }
         boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
         boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
@@ -1431,6 +1446,8 @@
                 .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isNavBarWindowVisible())
                 .setFlag(SYSUI_STATE_IME_SHOWING,
                         (mNavigationIconHints & NAVIGATION_HINT_BACK_ALT) != 0)
+                .setFlag(SYSUI_STATE_IME_SWITCHER_SHOWING,
+                        (mNavigationIconHints & NAVIGATION_HINT_IME_SHOWN) != 0)
                 .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
                         allowSystemGestureIgnoringBarVisibility())
                 .commitUpdate(mDisplayId);
@@ -1446,45 +1463,6 @@
         }
     }
 
-    /**
-     * Returns the system UI flags corresponding the the current accessibility button state
-     *
-     * @param outFeedbackEnabled if non-null, sets it to true if accessibility feedback is enabled.
-     */
-    public int getA11yButtonState(@Nullable boolean[] outFeedbackEnabled) {
-        boolean feedbackEnabled = false;
-        // AccessibilityManagerService resolves services for the current user since the local
-        // AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS permission
-        final List<AccessibilityServiceInfo> services =
-                mAccessibilityManager.getEnabledAccessibilityServiceList(
-                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
-        final List<String> a11yButtonTargets =
-                mAccessibilityManager.getAccessibilityShortcutTargets(
-                        AccessibilityManager.ACCESSIBILITY_BUTTON);
-        final int requestingServices = a11yButtonTargets.size();
-        for (int i = services.size() - 1; i >= 0; --i) {
-            AccessibilityServiceInfo info = services.get(i);
-            if (info.feedbackType != 0 && info.feedbackType !=
-                    AccessibilityServiceInfo.FEEDBACK_GENERIC) {
-                feedbackEnabled = true;
-            }
-        }
-
-        if (outFeedbackEnabled != null) {
-            outFeedbackEnabled[0] = feedbackEnabled;
-        }
-
-        // If accessibility button is floating menu mode, click and long click state should be
-        // disabled.
-        if (mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()
-                == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) {
-            return 0;
-        }
-
-        return (requestingServices >= 1 ? SYSUI_STATE_A11Y_BUTTON_CLICKABLE : 0)
-                | (requestingServices >= 2 ? SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE : 0);
-    }
-
     private void updateAssistantEntrypoints() {
         mAssistantAvailable = mAssistManagerLazy.get()
                 .getAssistInfoForUser(UserHandle.USER_CURRENT) != null;
@@ -1546,7 +1524,7 @@
     private void checkBarModes() {
         // We only have status bar on default display now.
         if (mIsOnDefaultDisplay) {
-            mStatusBarLazy.get().checkBarModes();
+            mStatusBarOptionalLazy.get().ifPresent(StatusBar::checkBarModes);
         } else {
             checkNavBarModes();
         }
@@ -1564,7 +1542,8 @@
      * Checks current navigation bar mode and make transitions.
      */
     public void checkNavBarModes() {
-        final boolean anim = mStatusBarLazy.get().isDeviceInteractive()
+        final boolean anim =
+                mStatusBarOptionalLazy.get().map(StatusBar::isDeviceInteractive).orElse(false)
                 && mNavigationBarWindowState != WINDOW_STATE_HIDDEN;
         mNavigationBarView.getBarTransitions().transitionTo(mNavigationBarMode, anim);
     }
@@ -1579,18 +1558,13 @@
             }
         }
         updateScreenPinningGestures();
-        setAccessibilityFloatingMenuModeIfNeeded();
+        updateAccessibilityButtonModeIfNeeded();
 
         if (!canShowSecondaryHandle()) {
             resetSecondaryHandle();
         }
     }
 
-    @Override
-    public void onAccessibilityButtonModeChanged(int mode) {
-        updateAccessibilityServicesState(mAccessibilityManager);
-    }
-
     public void disableAnimationsDuringHide(long delay) {
         mNavigationBarView.setLayoutTransitionsEnabled(false);
         mHandler.postDelayed(mEnableLayoutTransitions,
@@ -1615,16 +1589,97 @@
         mNavigationBarView.getBarTransitions().finishAnimations();
     }
 
-    private final AccessibilityServicesStateChangeListener mAccessibilityListener =
+    private final NavigationBarA11yHelper.NavA11yEventListener mAccessibilityListener =
             this::updateAccessibilityServicesState;
 
+    private WindowManager.LayoutParams getBarLayoutParams(int rotation) {
+        WindowManager.LayoutParams lp = getBarLayoutParamsForRotation(rotation);
+        lp.paramsForRotation = new WindowManager.LayoutParams[4];
+        for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) {
+            lp.paramsForRotation[rot] = getBarLayoutParamsForRotation(rot);
+        }
+        return lp;
+    }
+
+    private WindowManager.LayoutParams getBarLayoutParamsForRotation(int rotation) {
+        int width = WindowManager.LayoutParams.MATCH_PARENT;
+        int height = WindowManager.LayoutParams.MATCH_PARENT;
+        int insetsHeight = -1;
+        int gravity = Gravity.BOTTOM;
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            boolean navBarCanMove = true;
+            if (mWindowManager != null && mWindowManager.getCurrentWindowMetrics() != null) {
+                Rect displaySize = mWindowManager.getCurrentWindowMetrics().getBounds();
+                navBarCanMove = displaySize.width() != displaySize.height()
+                        && mContext.getResources().getBoolean(
+                        com.android.internal.R.bool.config_navBarCanMove);
+            }
+            if (!navBarCanMove) {
+                height = mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.navigation_bar_frame_height);
+                insetsHeight = mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.navigation_bar_height);
+            } else {
+                switch (rotation) {
+                    case ROTATION_UNDEFINED:
+                    case Surface.ROTATION_0:
+                    case Surface.ROTATION_180:
+                        height = mContext.getResources().getDimensionPixelSize(
+                                com.android.internal.R.dimen.navigation_bar_frame_height);
+                        insetsHeight = mContext.getResources().getDimensionPixelSize(
+                                com.android.internal.R.dimen.navigation_bar_height);
+                        break;
+                    case Surface.ROTATION_90:
+                        gravity = Gravity.RIGHT;
+                        width = mContext.getResources().getDimensionPixelSize(
+                                com.android.internal.R.dimen.navigation_bar_width);
+                        break;
+                    case Surface.ROTATION_270:
+                        gravity = Gravity.LEFT;
+                        width = mContext.getResources().getDimensionPixelSize(
+                                com.android.internal.R.dimen.navigation_bar_width);
+                        break;
+                }
+            }
+        }
+        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                width,
+                height,
+                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
+                WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
+                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+                        | WindowManager.LayoutParams.FLAG_SLIPPERY,
+                PixelFormat.TRANSLUCENT);
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            lp.gravity = gravity;
+            if (insetsHeight != -1) {
+                lp.providedInternalInsets = Insets.of(0, height - insetsHeight, 0, 0);
+            } else {
+                lp.providedInternalInsets = Insets.NONE;
+            }
+        }
+        lp.token = new Binder();
+        lp.accessibilityTitle = mContext.getString(R.string.nav_bar);
+        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
+        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+        lp.windowAnimations = 0;
+        lp.setTitle("NavigationBar" + mContext.getDisplayId());
+        lp.setFitInsetsTypes(0 /* types */);
+        lp.setTrustedOverlay();
+        return lp;
+    }
+
     private boolean canShowSecondaryHandle() {
         return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null;
     }
 
     private final Consumer<Integer> mRotationWatcher = rotation -> {
-        if (mNavigationBarView.needsReorient(rotation)) {
-            repositionNavigationBar();
+        if (mNavigationBarView != null
+                && mNavigationBarView.needsReorient(rotation)) {
+            repositionNavigationBar(rotation);
         }
     };
 
@@ -1644,7 +1699,7 @@
             }
             if (Intent.ACTION_USER_SWITCHED.equals(action)) {
                 // The accessibility settings may be different for the new user
-                updateAccessibilityServicesState(mAccessibilityManager);
+                updateAccessibilityServicesState();
             }
         }
     };
@@ -1653,4 +1708,121 @@
     int getNavigationIconHints() {
         return mNavigationIconHints;
     }
-}
+
+    /**
+     * Injectable factory for construction a {@link NavigationBar}.
+     */
+    public static class Factory {
+        private final Lazy<AssistManager> mAssistManagerLazy;
+        private final AccessibilityManager mAccessibilityManager;
+        private final DeviceProvisionedController mDeviceProvisionedController;
+        private final MetricsLogger mMetricsLogger;
+        private final OverviewProxyService mOverviewProxyService;
+        private final NavigationModeController mNavigationModeController;
+        private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
+        private final StatusBarStateController mStatusBarStateController;
+        private final SysUiState mSysUiFlagsContainer;
+        private final BroadcastDispatcher mBroadcastDispatcher;
+        private final CommandQueue mCommandQueue;
+        private final Optional<Pip> mPipOptional;
+        private final Optional<LegacySplitScreen> mSplitScreenOptional;
+        private final Optional<Recents> mRecentsOptional;
+        private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
+        private final ShadeController mShadeController;
+        private final NotificationRemoteInputManager mNotificationRemoteInputManager;
+        private final NotificationShadeDepthController mNotificationShadeDepthController;
+        private final SystemActions mSystemActions;
+        private final Handler mMainHandler;
+        private final NavigationBarOverlayController mNavbarOverlayController;
+        private final UiEventLogger mUiEventLogger;
+        private final NavigationBarA11yHelper mNavigationBarA11yHelper;
+        private final UserTracker mUserTracker;
+        private final LightBarController mMainLightBarController;
+        private final LightBarController.Factory mLightBarControllerFactory;
+        private final AutoHideController mMainAutoHideController;
+        private final AutoHideController.Factory mAutoHideControllerFactory;
+        private final Optional<TelecomManager> mTelecomManagerOptional;
+        private final InputMethodManager mInputMethodManager;
+
+        @Inject
+        public Factory(
+                Lazy<AssistManager> assistManagerLazy,
+                AccessibilityManager accessibilityManager,
+                DeviceProvisionedController deviceProvisionedController,
+                MetricsLogger metricsLogger,
+                OverviewProxyService overviewProxyService,
+                NavigationModeController navigationModeController,
+                AccessibilityButtonModeObserver accessibilityButtonModeObserver,
+                StatusBarStateController statusBarStateController,
+                SysUiState sysUiFlagsContainer,
+                BroadcastDispatcher broadcastDispatcher,
+                CommandQueue commandQueue,
+                Optional<Pip> pipOptional,
+                Optional<LegacySplitScreen> splitScreenOptional,
+                Optional<Recents> recentsOptional,
+                Lazy<Optional<StatusBar>> statusBarOptionalLazy,
+                ShadeController shadeController,
+                NotificationRemoteInputManager notificationRemoteInputManager,
+                NotificationShadeDepthController notificationShadeDepthController,
+                SystemActions systemActions,
+                @Main Handler mainHandler,
+                NavigationBarOverlayController navbarOverlayController,
+                UiEventLogger uiEventLogger,
+                NavigationBarA11yHelper navigationBarA11yHelper,
+                UserTracker userTracker,
+                LightBarController mainLightBarController,
+                LightBarController.Factory lightBarControllerFactory,
+                AutoHideController mainAutoHideController,
+                AutoHideController.Factory autoHideControllerFactory,
+                Optional<TelecomManager> telecomManagerOptional,
+                InputMethodManager inputMethodManager) {
+            mAssistManagerLazy = assistManagerLazy;
+            mAccessibilityManager = accessibilityManager;
+            mDeviceProvisionedController = deviceProvisionedController;
+            mMetricsLogger = metricsLogger;
+            mOverviewProxyService = overviewProxyService;
+            mNavigationModeController = navigationModeController;
+            mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
+            mStatusBarStateController = statusBarStateController;
+            mSysUiFlagsContainer = sysUiFlagsContainer;
+            mBroadcastDispatcher = broadcastDispatcher;
+            mCommandQueue = commandQueue;
+            mPipOptional = pipOptional;
+            mSplitScreenOptional = splitScreenOptional;
+            mRecentsOptional = recentsOptional;
+            mStatusBarOptionalLazy = statusBarOptionalLazy;
+            mShadeController = shadeController;
+            mNotificationRemoteInputManager = notificationRemoteInputManager;
+            mNotificationShadeDepthController = notificationShadeDepthController;
+            mSystemActions = systemActions;
+            mMainHandler = mainHandler;
+            mNavbarOverlayController = navbarOverlayController;
+            mUiEventLogger = uiEventLogger;
+            mNavigationBarA11yHelper = navigationBarA11yHelper;
+            mUserTracker = userTracker;
+            mMainLightBarController = mainLightBarController;
+            mLightBarControllerFactory = lightBarControllerFactory;
+            mMainAutoHideController = mainAutoHideController;
+            mAutoHideControllerFactory = autoHideControllerFactory;
+            mTelecomManagerOptional = telecomManagerOptional;
+            mInputMethodManager = inputMethodManager;
+        }
+
+        /** Construct a {@link NavigationBar} */
+        public NavigationBar create(Context context) {
+            final WindowManager wm = context.getSystemService(WindowManager.class);
+            return new NavigationBar(context, wm, mAssistManagerLazy,
+                    mAccessibilityManager, mDeviceProvisionedController, mMetricsLogger,
+                    mOverviewProxyService, mNavigationModeController,
+                    mAccessibilityButtonModeObserver, mStatusBarStateController,
+                    mSysUiFlagsContainer, mBroadcastDispatcher, mCommandQueue, mPipOptional,
+                    mSplitScreenOptional, mRecentsOptional, mStatusBarOptionalLazy,
+                    mShadeController, mNotificationRemoteInputManager,
+                    mNotificationShadeDepthController, mSystemActions, mMainHandler,
+                    mNavbarOverlayController, mUiEventLogger, mNavigationBarA11yHelper,
+                    mUserTracker, mMainLightBarController, mLightBarControllerFactory,
+                    mMainAutoHideController, mAutoHideControllerFactory, mTelecomManagerOptional,
+                    mInputMethodManager);
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarA11yHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarA11yHelper.java
new file mode 100644
index 0000000..13e6d8b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarA11yHelper.java
@@ -0,0 +1,90 @@
+package com.android.systemui.navigationbar;
+
+import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
+
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
+
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * Extracts shared elements of a11y necessary between navbar and taskbar delegate
+ */
+@SysUISingleton
+public final class NavigationBarA11yHelper implements
+        AccessibilityButtonModeObserver.ModeChangedListener {
+    private final AccessibilityManager mAccessibilityManager;
+    private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
+    private final List<NavA11yEventListener> mA11yEventListeners = new ArrayList<>();
+
+    @Inject
+    public NavigationBarA11yHelper(AccessibilityManager accessibilityManager,
+            AccessibilityManagerWrapper accessibilityManagerWrapper,
+            AccessibilityButtonModeObserver accessibilityButtonModeObserver) {
+        mAccessibilityManager = accessibilityManager;
+        accessibilityManagerWrapper.addCallback(
+                accessibilityManager1 -> NavigationBarA11yHelper.this.dispatchEventUpdate());
+        mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
+
+        mAccessibilityButtonModeObserver.addListener(this);
+    }
+
+    public void registerA11yEventListener(NavA11yEventListener listener) {
+        mA11yEventListeners.add(listener);
+    }
+
+    public void removeA11yEventListener(NavA11yEventListener listener) {
+        mA11yEventListeners.remove(listener);
+    }
+
+    private void dispatchEventUpdate() {
+        for (NavA11yEventListener listener : mA11yEventListeners) {
+            listener.updateAccessibilityServicesState();
+        }
+    }
+
+    @Override
+    public void onAccessibilityButtonModeChanged(int mode) {
+        dispatchEventUpdate();
+    }
+
+    /**
+     * See {@link QuickStepContract#SYSUI_STATE_A11Y_BUTTON_CLICKABLE} and
+     * {@link QuickStepContract#SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE}
+     *
+     * @return the a11y button clickable and long_clickable states, or 0 if there is no
+     *         a11y button in the navbar
+     */
+    public int getA11yButtonState() {
+        // AccessibilityManagerService resolves services for the current user since the local
+        // AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS permission
+        final List<String> a11yButtonTargets =
+                mAccessibilityManager.getAccessibilityShortcutTargets(
+                        AccessibilityManager.ACCESSIBILITY_BUTTON);
+        final int requestingServices = a11yButtonTargets.size();
+
+        // If accessibility button is floating menu mode, click and long click state should be
+        // disabled.
+        if (mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()
+                == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) {
+            return 0;
+        }
+
+        return (requestingServices >= 1 ? SYSUI_STATE_A11Y_BUTTON_CLICKABLE : 0)
+                | (requestingServices >= 2 ? SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE : 0);
+    }
+
+    public interface NavA11yEventListener {
+        void updateAccessibilityServicesState();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 1628c71..97bcb00 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -17,109 +17,64 @@
 package com.android.systemui.navigationbar;
 
 import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
+
+import static com.android.systemui.shared.recents.utilities.Utilities.isTablet;
 
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.hardware.display.DisplayManager;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.RemoteException;
-import android.os.SystemProperties;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.IWindowManager;
 import android.view.View;
-import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
-import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.RegisterStatusBarResult;
 import com.android.settingslib.applications.InterestingConfigChanges;
 import com.android.systemui.Dumpable;
-import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
-import com.android.systemui.accessibility.SystemActions;
-import com.android.systemui.assist.AssistManager;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
-import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
-import com.android.wm.shell.pip.Pip;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.Optional;
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 
 /** A controller to handle navigation bars. */
 @SysUISingleton
-public class NavigationBarController implements Callbacks,
+public class NavigationBarController implements
+        Callbacks,
         ConfigurationController.ConfigurationListener,
-        NavigationModeController.ModeChangedListener, Dumpable {
-
-    private static final float TABLET_MIN_DPS = 600;
+        NavigationModeController.ModeChangedListener,
+        Dumpable {
 
     private static final String TAG = NavigationBarController.class.getSimpleName();
 
     private final Context mContext;
-    private final WindowManager mWindowManager;
-    private final Lazy<AssistManager> mAssistManagerLazy;
-    private final AccessibilityManager mAccessibilityManager;
-    private final AccessibilityManagerWrapper mAccessibilityManagerWrapper;
-    private final DeviceProvisionedController mDeviceProvisionedController;
-    private final MetricsLogger mMetricsLogger;
-    private final OverviewProxyService mOverviewProxyService;
-    private final NavigationModeController mNavigationModeController;
-    private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
-    private final StatusBarStateController mStatusBarStateController;
-    private final SysUiState mSysUiFlagsContainer;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final CommandQueue mCommandQueue;
-    private final Optional<Pip> mPipOptional;
-    private final Optional<LegacySplitScreen> mSplitScreenOptional;
-    private final Optional<Recents> mRecentsOptional;
-    private final Lazy<StatusBar> mStatusBarLazy;
-    private final ShadeController mShadeController;
-    private final NotificationRemoteInputManager mNotificationRemoteInputManager;
-    private final SystemActions mSystemActions;
-    private final UiEventLogger mUiEventLogger;
     private final Handler mHandler;
+    private final NavigationBar.Factory mNavigationBarFactory;
     private final DisplayManager mDisplayManager;
-    private final NavigationBarOverlayController mNavBarOverlayController;
     private final TaskbarDelegate mTaskbarDelegate;
-    private final NotificationShadeDepthController mNotificationShadeDepthController;
     private int mNavMode;
-    private boolean mIsTablet;
-    private final UserTracker mUserTracker;
+    @VisibleForTesting boolean mIsTablet;
 
     /** A displayId - nav bar maps. */
     @VisibleForTesting
@@ -132,72 +87,37 @@
 
     @Inject
     public NavigationBarController(Context context,
-            WindowManager windowManager,
-            Lazy<AssistManager> assistManagerLazy,
-            AccessibilityManager accessibilityManager,
-            AccessibilityManagerWrapper accessibilityManagerWrapper,
-            DeviceProvisionedController deviceProvisionedController,
-            MetricsLogger metricsLogger,
             OverviewProxyService overviewProxyService,
             NavigationModeController navigationModeController,
-            AccessibilityButtonModeObserver accessibilityButtonModeObserver,
-            StatusBarStateController statusBarStateController,
             SysUiState sysUiFlagsContainer,
-            BroadcastDispatcher broadcastDispatcher,
             CommandQueue commandQueue,
-            Optional<Pip> pipOptional,
-            Optional<LegacySplitScreen> splitScreenOptional,
-            Optional<Recents> recentsOptional,
-            Lazy<StatusBar> statusBarLazy,
-            ShadeController shadeController,
-            NotificationRemoteInputManager notificationRemoteInputManager,
-            NotificationShadeDepthController notificationShadeDepthController,
-            SystemActions systemActions,
             @Main Handler mainHandler,
-            UiEventLogger uiEventLogger,
-            NavigationBarOverlayController navBarOverlayController,
             ConfigurationController configurationController,
-            UserTracker userTracker) {
+            NavigationBarA11yHelper navigationBarA11yHelper,
+            TaskbarDelegate taskbarDelegate,
+            NavigationBar.Factory navigationBarFactory,
+            DumpManager dumpManager,
+            AutoHideController autoHideController) {
         mContext = context;
-        mWindowManager = windowManager;
-        mAssistManagerLazy = assistManagerLazy;
-        mAccessibilityManager = accessibilityManager;
-        mAccessibilityManagerWrapper = accessibilityManagerWrapper;
-        mDeviceProvisionedController = deviceProvisionedController;
-        mMetricsLogger = metricsLogger;
-        mOverviewProxyService = overviewProxyService;
-        mNavigationModeController = navigationModeController;
-        mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
-        mStatusBarStateController = statusBarStateController;
-        mSysUiFlagsContainer = sysUiFlagsContainer;
-        mBroadcastDispatcher = broadcastDispatcher;
-        mCommandQueue = commandQueue;
-        mPipOptional = pipOptional;
-        mSplitScreenOptional = splitScreenOptional;
-        mRecentsOptional = recentsOptional;
-        mStatusBarLazy = statusBarLazy;
-        mShadeController = shadeController;
-        mNotificationRemoteInputManager = notificationRemoteInputManager;
-        mNotificationShadeDepthController = notificationShadeDepthController;
-        mSystemActions = systemActions;
-        mUiEventLogger = uiEventLogger;
         mHandler = mainHandler;
+        mNavigationBarFactory = navigationBarFactory;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         commandQueue.addCallback(this);
         configurationController.addCallback(this);
         mConfigChanges.applyNewConfig(mContext.getResources());
-        mNavBarOverlayController = navBarOverlayController;
-        mNavMode = mNavigationModeController.addListener(this);
-        mNavigationModeController.addListener(this);
-        mTaskbarDelegate = new TaskbarDelegate(mOverviewProxyService);
-        mIsTablet = isTablet(mContext.getResources().getConfiguration());
-        mUserTracker = userTracker;
+        mNavMode = navigationModeController.addListener(this);
+        mTaskbarDelegate = taskbarDelegate;
+        mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService,
+                navigationBarA11yHelper, navigationModeController, sysUiFlagsContainer,
+                dumpManager, autoHideController);
+        mIsTablet = isTablet(mContext);
+        dumpManager.registerDumpable(this);
     }
 
     @Override
     public void onConfigChanged(Configuration newConfig) {
         boolean isOldConfigTablet = mIsTablet;
-        mIsTablet = isTablet(newConfig);
+        mIsTablet = isTablet(mContext);
         boolean largeScreenChanged = mIsTablet != isOldConfigTablet;
         // If we folded/unfolded while in 3 button, show navbar in folded state, hide in unfolded
         if (largeScreenChanged && updateNavbarForTaskbar()) {
@@ -237,25 +157,25 @@
         });
     }
 
-    /**
-     * @return {@code true} if navbar was added/removed, false otherwise
-     */
-    public boolean updateNavbarForTaskbar() {
-        if (!isThreeButtonTaskbarFlagEnabled()) {
-            return false;
-        }
-
-        if (mIsTablet && mNavMode == NAV_BAR_MODE_3BUTTON) {
-            // Remove navigation bar when taskbar is showing, currently only for 3 button mode
-            removeNavigationBar(mContext.getDisplayId());
-            mCommandQueue.addCallback(mTaskbarDelegate);
-        } else if (mNavigationBars.get(mContext.getDisplayId()) == null) {
-            // Add navigation bar after taskbar goes away
+    /** @see #initializeTaskbarIfNecessary() */
+    private boolean updateNavbarForTaskbar() {
+        boolean taskbarShown = initializeTaskbarIfNecessary();
+        if (!taskbarShown && mNavigationBars.get(mContext.getDisplayId()) == null) {
             createNavigationBar(mContext.getDisplay(), null, null);
-            mCommandQueue.removeCallback(mTaskbarDelegate);
         }
+        return taskbarShown;
+    }
 
-        return true;
+    /** @return {@code true} if taskbar is enabled, false otherwise */
+    private boolean initializeTaskbarIfNecessary() {
+        if (mIsTablet) {
+            // Remove navigation bar when taskbar is showing
+            removeNavigationBar(mContext.getDisplayId());
+            mTaskbarDelegate.init(mContext.getDisplayId());
+        } else {
+            mTaskbarDelegate.destroy();
+        }
+        return mIsTablet;
     }
 
     @Override
@@ -266,7 +186,7 @@
     @Override
     public void onDisplayReady(int displayId) {
         Display display = mDisplayManager.getDisplay(displayId);
-        mIsTablet = isTablet(mContext.getResources().getConfiguration());
+        mIsTablet = isTablet(mContext);
         createNavigationBar(display, null /* savedState */, null /* result */);
     }
 
@@ -302,7 +222,7 @@
      */
     public void createNavigationBars(final boolean includeDefaultDisplay,
             RegisterStatusBarResult result) {
-        if (updateNavbarForTaskbar()) {
+        if (initializeTaskbarIfNecessary()) {
             return;
         }
 
@@ -326,7 +246,7 @@
             return;
         }
 
-        if (isThreeButtonTaskbarEnabled()) {
+        if (mIsTablet) {
             return;
         }
 
@@ -346,32 +266,8 @@
         final Context context = isOnDefaultDisplay
                 ? mContext
                 : mContext.createDisplayContext(display);
-        NavigationBar navBar = new NavigationBar(context,
-                mWindowManager,
-                mAssistManagerLazy,
-                mAccessibilityManager,
-                mAccessibilityManagerWrapper,
-                mDeviceProvisionedController,
-                mMetricsLogger,
-                mOverviewProxyService,
-                mNavigationModeController,
-                mAccessibilityButtonModeObserver,
-                mStatusBarStateController,
-                mSysUiFlagsContainer,
-                mBroadcastDispatcher,
-                mCommandQueue,
-                mPipOptional,
-                mSplitScreenOptional,
-                mRecentsOptional,
-                mStatusBarLazy,
-                mShadeController,
-                mNotificationRemoteInputManager,
-                mNotificationShadeDepthController,
-                mSystemActions,
-                mHandler,
-                mNavBarOverlayController,
-                mUiEventLogger,
-                mUserTracker);
+        NavigationBar navBar = mNavigationBarFactory.create(context);
+
         mNavigationBars.put(displayId, navBar);
 
         View navigationBarView = navBar.createView(savedState);
@@ -461,24 +357,6 @@
         return mNavigationBars.get(DEFAULT_DISPLAY);
     }
 
-    private boolean isThreeButtonTaskbarEnabled() {
-        return mIsTablet && mNavMode == NAV_BAR_MODE_3BUTTON &&
-                isThreeButtonTaskbarFlagEnabled();
-    }
-
-    private boolean isThreeButtonTaskbarFlagEnabled() {
-        return SystemProperties.getBoolean("persist.debug.taskbar_three_button", false);
-    }
-
-    private boolean isTablet(Configuration newConfig) {
-        float density = Resources.getSystem().getDisplayMetrics().density;
-        int size = Math.min((int) (density * newConfig.screenWidthDp),
-                (int) (density* newConfig.screenHeightDp));
-        DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
-        float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
-        return (size / densityRatio) >= TABLET_MIN_DPS;
-    }
-
     @Override
     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
         for (int i = 0; i < mNavigationBars.size(); i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
index 70c21e4..c02cc8d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
@@ -19,9 +19,7 @@
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SEARCH_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
@@ -69,6 +67,7 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.RotationButton.RotationButtonUpdatesCallback;
 import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
 import com.android.systemui.navigationbar.buttons.ContextualButton;
 import com.android.systemui.navigationbar.buttons.ContextualButtonGroup;
@@ -78,9 +77,9 @@
 import com.android.systemui.navigationbar.buttons.RotationContextButton;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.navigationbar.gestural.FloatingRotationButton;
-import com.android.systemui.navigationbar.gestural.RegionSamplingHelper;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.SysUiStatsLog;
@@ -96,6 +95,8 @@
 import java.io.PrintWriter;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 public class NavigationBarView extends FrameLayout implements
@@ -159,6 +160,7 @@
     private Configuration mTmpLastConfiguration;
 
     private NavigationBarInflaterView mNavigationInflaterView;
+    private Optional<Recents> mRecentsOptional = Optional.empty();
     private NotificationPanelViewController mPanelView;
     private RotationContextButton mRotationContextButton;
     private FloatingRotationButton mFloatingRotationButton;
@@ -247,7 +249,7 @@
                 @Override
                 public boolean performAccessibilityAction(View host, int action, Bundle args) {
                     if (action == R.id.action_toggle_overview) {
-                        Dependency.get(Recents.class).toggleRecentApps();
+                        mRecentsOptional.ifPresent(Recents::toggleRecentApps);
                     } else {
                         return super.performAccessibilityAction(host, action, args);
                     }
@@ -270,14 +272,23 @@
                 false /* inScreen */, false /* useNearestRegion */));
     };
 
-    private final Consumer<Boolean> mRotationButtonListener = (visible) -> {
-        if (visible && mAutoHideController != null) {
-            // If the button will actually become visible and the navbar is about to hide,
-            // tell the statusbar to keep it around for longer
-            mAutoHideController.touchAutoHide();
-        }
-        notifyActiveTouchRegions();
-    };
+    private final RotationButtonUpdatesCallback mRotationButtonListener =
+            new RotationButtonUpdatesCallback() {
+                @Override
+                public void onVisibilityChanged(boolean visible) {
+                    if (visible && mAutoHideController != null) {
+                        // If the button will actually become visible and the navbar is about
+                        // to hide, tell the statusbar to keep it around for longer
+                        mAutoHideController.touchAutoHide();
+                    }
+                    notifyActiveTouchRegions();
+                }
+
+                @Override
+                public void onPositionChanged() {
+                    notifyActiveTouchRegions();
+                }
+            };
 
     private final Consumer<Boolean> mNavbarOverlayVisibilityChangeCallback = (visible) -> {
         if (visible) {
@@ -339,6 +350,7 @@
         mEdgeBackGestureHandler = Dependency.get(EdgeBackGestureHandler.Factory.class)
                 .create(mContext);
         mEdgeBackGestureHandler.setStateChangeCallback(this::updateStates);
+        Executor backgroundExecutor = Dependency.get(Dependency.BACKGROUND_EXECUTOR);
         mRegionSamplingHelper = new RegionSamplingHelper(this,
                 new RegionSamplingHelper.SamplingCallback() {
                     @Override
@@ -361,7 +373,7 @@
                     public boolean isSamplingEnabled() {
                         return isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode);
                     }
-                });
+                }, backgroundExecutor);
 
         mNavBarOverlayController = Dependency.get(NavigationBarOverlayController.class);
         if (mNavBarOverlayController.isNavigationBarOverlayEnabled()) {
@@ -388,6 +400,10 @@
         return mBarTransitions.getLightTransitionsController();
     }
 
+    public void setComponents(Optional<Recents> recentsOptional) {
+        mRecentsOptional = recentsOptional;
+    }
+
     public void setComponents(NotificationPanelViewController panel) {
         mPanelView = panel;
         updatePanelSystemUiStateFlags();
@@ -826,7 +842,6 @@
 
     public void onStatusBarPanelStateChanged() {
         updateSlippery();
-        updatePanelSystemUiStateFlags();
     }
 
     public void updateDisabledSystemUiStateFlags() {
@@ -843,21 +858,12 @@
                 .commitUpdate(displayId);
     }
 
-    public void updatePanelSystemUiStateFlags() {
-        int displayId = mContext.getDisplayId();
+    private void updatePanelSystemUiStateFlags() {
         if (SysUiState.DEBUG) {
             Log.d(TAG, "Updating panel sysui state flags: panelView=" + mPanelView);
         }
         if (mPanelView != null) {
-            if (SysUiState.DEBUG) {
-                Log.d(TAG, "Updating panel sysui state flags: fullyExpanded="
-                        + mPanelView.isFullyExpanded() + " inQs=" + mPanelView.isInSettings());
-            }
-            mSysUiFlagContainer.setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
-                    mPanelView.isFullyExpanded() && !mPanelView.isInSettings())
-                    .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
-                            mPanelView.isInSettings())
-                    .commitUpdate(displayId);
+            mPanelView.updateSystemUiStateFlags();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
index 422ffd5..73a0c54 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
@@ -36,6 +36,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -99,12 +100,15 @@
     public NavigationModeController(Context context,
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
-            @UiBackground Executor uiBgExecutor) {
+            @UiBackground Executor uiBgExecutor,
+            DumpManager dumpManager) {
         mContext = context;
         mCurrentUserContext = context;
         mOverlayManager = IOverlayManager.Stub.asInterface(
                 ServiceManager.getService(Context.OVERLAY_SERVICE));
         mUiBgExecutor = uiBgExecutor;
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
         deviceProvisionedController.addCallback(mDeviceProvisionedCallback);
 
         IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
@@ -114,7 +118,7 @@
 
         configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 if (DEBUG) {
                     Log.d(TAG, "onOverlayChanged");
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButton.java b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButton.java
index e487858..3486c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButton.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButton.java
@@ -20,12 +20,10 @@
 
 import com.android.systemui.navigationbar.buttons.KeyButtonDrawable;
 
-import java.util.function.Consumer;
-
 /** Interface of a rotation button that interacts {@link RotationButtonController}. */
 public interface RotationButton {
     void setRotationButtonController(RotationButtonController rotationButtonController);
-    void setVisibilityChangedCallback(Consumer<Boolean> visibilityChangedCallback);
+    void setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback);
     View getCurrentView();
     boolean show();
     boolean hide();
@@ -39,4 +37,12 @@
     default boolean acceptRotationProposal() {
         return getCurrentView() != null;
     }
+
+    /**
+     * Callback for updates provided by a rotation button
+     */
+    interface RotationButtonUpdatesCallback {
+        void onVisibilityChanged(boolean isVisible);
+        void onPositionChanged();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java
index 8ea9ae3..0f5c03a 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java
@@ -46,7 +46,10 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.navigationbar.RotationButton.RotationButtonUpdatesCallback;
 import com.android.systemui.navigationbar.buttons.KeyButtonDrawable;
+import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.recents.utilities.ViewRippler;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -138,12 +141,12 @@
     }
 
     void setRotationButton(RotationButton rotationButton,
-            Consumer<Boolean> visibilityChangedCallback) {
+                           RotationButtonUpdatesCallback updatesCallback) {
         mRotationButton = rotationButton;
         mRotationButton.setRotationButtonController(this);
         mRotationButton.setOnClickListener(this::onRotateSuggestionClick);
         mRotationButton.setOnHoverListener(this::onRotateSuggestionHover);
-        mRotationButton.setVisibilityChangedCallback(visibilityChangedCallback);
+        mRotationButton.setUpdatesCallback(updatesCallback);
     }
 
     void registerListeners() {
@@ -311,7 +314,7 @@
 
         // Prepare to show the navbar icon by updating the icon style to change anim params
         mLastRotationSuggestion = rotation; // Remember rotation for click
-        final boolean rotationCCW = isRotationAnimationCCW(windowRotation, rotation);
+        final boolean rotationCCW = Utilities.isRotationAnimationCCW(windowRotation, rotation);
         if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
             mIconResId = rotationCCW
                     ? R.drawable.ic_sysbar_rotate_button_ccw_start_90
@@ -431,23 +434,6 @@
         return rotation == NATURAL_ROTATION;
     }
 
-    private boolean isRotationAnimationCCW(int from, int to) {
-        // All 180deg WM rotation animations are CCW, match that
-        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
-        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
-        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
-        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
-        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
-        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
-        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
-        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
-        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
-        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
-        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
-        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
-        return false; // Default
-    }
-
     private void rescheduleRotationTimeout(final boolean reasonHover) {
         // May be called due to a new rotation proposal or a change in hover state
         if (reasonHover) {
@@ -520,38 +506,6 @@
         }
     }
 
-    private class ViewRippler {
-        private static final int RIPPLE_OFFSET_MS = 50;
-        private static final int RIPPLE_INTERVAL_MS = 2000;
-        private View mRoot;
-
-        public void start(View root) {
-            stop(); // Stop any pending ripple animations
-
-            mRoot = root;
-
-            // Schedule pending ripples, offset the 1st to avoid problems with visibility change
-            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
-            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
-            mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS);
-            mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS);
-            mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS);
-        }
-
-        public void stop() {
-            if (mRoot != null) mRoot.removeCallbacks(mRipple);
-        }
-
-        private final Runnable mRipple = new Runnable() {
-            @Override
-            public void run() { // Cause the ripple to fire via false presses
-                if (!mRoot.isAttachedToWindow()) return;
-                mRoot.setPressed(true /* pressed */);
-                mRoot.setPressed(false /* pressed */);
-            }
-        };
-    }
-
     enum RotationButtonEvent implements UiEventLogger.UiEventEnum {
         @UiEvent(doc = "The rotation button was shown")
         ROTATION_SUGGESTION_SHOWN(206),
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 03147d8..d707dbd 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -16,23 +16,290 @@
 
 package com.android.systemui.navigationbar;
 
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
+import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
+import static android.view.InsetsState.containsType;
+import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+
+import android.app.StatusBarManager;
+import android.app.StatusBarManager.WindowVisibleState;
+import android.content.ComponentCallbacks;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.hardware.display.DisplayManager;
+import android.inputmethodservice.InputMethodService;
 import android.os.IBinder;
+import android.view.Display;
+import android.view.InsetsVisibilities;
+import android.view.View;
+import android.view.WindowInsetsController.Behavior;
 
+import androidx.annotation.NonNull;
+
+import com.android.internal.view.AppearanceRegion;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.AutoHideController;
 
-public class TaskbarDelegate implements CommandQueue.Callbacks {
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 
-    private final OverviewProxyService mOverviewProxyService;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 
-    public TaskbarDelegate(OverviewProxyService overviewProxyService) {
+@Singleton
+public class TaskbarDelegate implements CommandQueue.Callbacks,
+        OverviewProxyService.OverviewProxyListener, NavigationModeController.ModeChangedListener,
+        ComponentCallbacks, Dumpable {
+
+    private final EdgeBackGestureHandler mEdgeBackGestureHandler;
+
+    private CommandQueue mCommandQueue;
+    private OverviewProxyService mOverviewProxyService;
+    private NavigationBarA11yHelper mNavigationBarA11yHelper;
+    private NavigationModeController mNavigationModeController;
+    private SysUiState mSysUiState;
+    private AutoHideController mAutoHideController;
+    private int mDisplayId;
+    private int mNavigationIconHints;
+    private final NavigationBarA11yHelper.NavA11yEventListener mNavA11yEventListener =
+            this::updateSysuiFlags;
+    private int mDisabledFlags;
+    private @WindowVisibleState int mTaskBarWindowState = WINDOW_STATE_SHOWING;
+    private @Behavior int mBehavior;
+    private final Context mContext;
+    private final DisplayManager mDisplayManager;
+    private Context mWindowContext;
+    /**
+     * Tracks the system calls for when taskbar should transiently show or hide so we can return
+     * this value in {@link AutoHideUiElement#isVisible()} below.
+     *
+     * This also gets set by {@link #onTaskbarAutohideSuspend(boolean)} to force show the transient
+     * taskbar if launcher has requested to suspend auto-hide behavior.
+     */
+    private boolean mTaskbarTransientShowing;
+    private final AutoHideUiElement mAutoHideUiElement = new AutoHideUiElement() {
+        @Override
+        public void synchronizeState() {
+        }
+
+        @Override
+        public boolean isVisible() {
+            return mTaskbarTransientShowing;
+        }
+
+        @Override
+        public void hide() {
+        }
+    };
+
+    @Inject
+    public TaskbarDelegate(Context context) {
+        mEdgeBackGestureHandler = Dependency.get(EdgeBackGestureHandler.Factory.class)
+                .create(context);
+        mContext = context;
+        mDisplayManager = mContext.getSystemService(DisplayManager.class);
+    }
+
+    public void setDependencies(CommandQueue commandQueue,
+            OverviewProxyService overviewProxyService,
+            NavigationBarA11yHelper navigationBarA11yHelper,
+            NavigationModeController navigationModeController,
+            SysUiState sysUiState, DumpManager dumpManager,
+            AutoHideController autoHideController) {
+        // TODO: adding this in the ctor results in a dagger dependency cycle :(
+        mCommandQueue = commandQueue;
         mOverviewProxyService = overviewProxyService;
+        mNavigationBarA11yHelper = navigationBarA11yHelper;
+        mNavigationModeController = navigationModeController;
+        mSysUiState = sysUiState;
+        dumpManager.registerDumpable(this);
+        mAutoHideController = autoHideController;
+    }
+
+    public void init(int displayId) {
+        mDisplayId = displayId;
+        mCommandQueue.addCallback(this);
+        mOverviewProxyService.addCallback(this);
+        mEdgeBackGestureHandler.onNavigationModeChanged(
+                mNavigationModeController.addListener(this));
+        mNavigationBarA11yHelper.registerA11yEventListener(mNavA11yEventListener);
+        mEdgeBackGestureHandler.onNavBarAttached();
+        // Initialize component callback
+        Display display = mDisplayManager.getDisplay(displayId);
+        mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
+        mWindowContext.registerComponentCallbacks(this);
+        // Set initial state for any listeners
+        updateSysuiFlags();
+        mAutoHideController.setNavigationBar(mAutoHideUiElement);
+    }
+
+    public void destroy() {
+        mCommandQueue.removeCallback(this);
+        mOverviewProxyService.removeCallback(this);
+        mNavigationModeController.removeListener(this);
+        mNavigationBarA11yHelper.removeA11yEventListener(mNavA11yEventListener);
+        mEdgeBackGestureHandler.onNavBarDetached();
+        if (mWindowContext != null) {
+            mWindowContext.unregisterComponentCallbacks(this);
+            mWindowContext = null;
+        }
+        mAutoHideController.setNavigationBar(null);
+    }
+
+    private void updateSysuiFlags() {
+        int a11yFlags = mNavigationBarA11yHelper.getA11yButtonState();
+        boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
+        boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
+
+        mSysUiState.setFlag(SYSUI_STATE_A11Y_BUTTON_CLICKABLE, clickable)
+                .setFlag(SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE, longClickable)
+                .setFlag(SYSUI_STATE_IME_SHOWING,
+                        (mNavigationIconHints & NAVIGATION_HINT_BACK_ALT) != 0)
+                .setFlag(SYSUI_STATE_IME_SWITCHER_SHOWING,
+                        (mNavigationIconHints & NAVIGATION_HINT_IME_SHOWN) != 0)
+                .setFlag(SYSUI_STATE_OVERVIEW_DISABLED,
+                        (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0)
+                .setFlag(SYSUI_STATE_HOME_DISABLED,
+                        (mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0)
+                .setFlag(SYSUI_STATE_BACK_DISABLED,
+                        (mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)
+                .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isWindowVisible())
+                .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
+                        allowSystemGestureIgnoringBarVisibility())
+                .setFlag(SYSUI_STATE_SCREEN_PINNING,
+                        ActivityManagerWrapper.getInstance().isScreenPinningActive())
+                .commitUpdate(mDisplayId);
     }
 
     @Override
     public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
             boolean showImeSwitcher) {
-        mOverviewProxyService.notifyImeWindowStatus(displayId, token, vis, backDisposition,
-                showImeSwitcher);
+        boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0;
+        int hints = Utilities.calculateBackDispositionHints(mNavigationIconHints, backDisposition,
+                imeShown, showImeSwitcher);
+        if (hints != mNavigationIconHints) {
+            mNavigationIconHints = hints;
+            updateSysuiFlags();
+        }
+    }
+
+    @Override
+    public void setWindowState(int displayId, int window, int state) {
+        if (displayId == mDisplayId
+                && window == StatusBarManager.WINDOW_NAVIGATION_BAR
+                && mTaskBarWindowState != state) {
+            mTaskBarWindowState = state;
+            updateSysuiFlags();
+        }
+    }
+
+    @Override
+    public void onRotationProposal(int rotation, boolean isValid) {
+        mOverviewProxyService.onRotationProposal(rotation, isValid);
+    }
+
+    @Override
+    public void disable(int displayId, int state1, int state2, boolean animate) {
+        mDisabledFlags = state1;
+        updateSysuiFlags();
+        mOverviewProxyService.disable(displayId, state1, state2, animate);
+    }
+
+    @Override
+    public void onSystemBarAttributesChanged(int displayId, int appearance,
+            AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme, int behavior,
+            InsetsVisibilities requestedVisibilities, String packageName) {
+        mOverviewProxyService.onSystemBarAttributesChanged(displayId, behavior);
+        if (mBehavior != behavior) {
+            mBehavior = behavior;
+            updateSysuiFlags();
+        }
+    }
+
+    @Override
+    public void showTransient(int displayId, int[] types) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        if (!containsType(types, ITYPE_NAVIGATION_BAR)) {
+            return;
+        }
+        mTaskbarTransientShowing = true;
+    }
+
+    @Override
+    public void abortTransient(int displayId, int[] types) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        if (!containsType(types, ITYPE_NAVIGATION_BAR)) {
+            return;
+        }
+        mTaskbarTransientShowing = false;
+    }
+
+    @Override
+    public void onTaskbarAutohideSuspend(boolean suspend) {
+        mTaskbarTransientShowing = suspend;
+        if (suspend) {
+            mAutoHideController.suspendAutoHide();
+        } else {
+            mAutoHideController.resumeSuspendedAutoHide();
+        }
+    }
+
+    @Override
+    public void onNavigationModeChanged(int mode) {
+        mEdgeBackGestureHandler.onNavigationModeChanged(mode);
+    }
+
+    private boolean isWindowVisible() {
+        return mTaskBarWindowState == WINDOW_STATE_SHOWING;
+    }
+
+    private boolean allowSystemGestureIgnoringBarVisibility() {
+        return mBehavior != BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration configuration) {
+        mEdgeBackGestureHandler.onConfigurationChanged(configuration);
+    }
+
+    @Override
+    public void onLowMemory() {}
+
+    @Override
+    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
+        pw.println("TaskbarDelegate (displayId=" + mDisplayId + "):");
+        pw.println("  mNavigationIconHints=" + mNavigationIconHints);
+        pw.println("  mDisabledFlags=" + mDisabledFlags);
+        pw.println("  mTaskBarWindowState=" + mTaskBarWindowState);
+        pw.println("  mBehavior=" + mBehavior);
+        pw.println("  mTaskbarTransientShowing=" + mTaskbarTransientShowing);
+        mEdgeBackGestureHandler.dump(pw);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/RotationContextButton.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/RotationContextButton.java
index 6a97a33..ebb67af 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/RotationContextButton.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/RotationContextButton.java
@@ -23,10 +23,6 @@
 
 import com.android.systemui.navigationbar.RotationButton;
 import com.android.systemui.navigationbar.RotationButtonController;
-import com.android.systemui.navigationbar.buttons.ContextualButton;
-import com.android.systemui.navigationbar.buttons.KeyButtonDrawable;
-
-import java.util.function.Consumer;
 
 /** Containing logic for the rotation button in nav bar. */
 public class RotationContextButton extends ContextualButton implements RotationButton {
@@ -48,13 +44,10 @@
     }
 
     @Override
-    public void setVisibilityChangedCallback(Consumer<Boolean> visibilityChangedCallback) {
-        setListener(new ContextButtonListener() {
-            @Override
-            public void onVisibilityChanged(ContextualButton button, boolean visible) {
-                if (visibilityChangedCallback != null) {
-                    visibilityChangedCallback.accept(visible);
-                }
+    public void setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback) {
+        setListener((button, visible) -> {
+            if (updatesCallback != null) {
+                updatesCallback.onVisibilityChanged(visible);
             }
         });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index aaa3bf0..c6da342 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -101,8 +101,8 @@
     private static final int MAX_NUM_LOGGED_PREDICTIONS = 10;
     private static final int MAX_NUM_LOGGED_GESTURES = 10;
 
-    // Temporary log until b/176302696 is resolved
-    static final boolean DEBUG_MISSING_GESTURE = false;
+    // Temporary log until b/201642126 is resolved
+    static final boolean DEBUG_MISSING_GESTURE = true;
     static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture";
 
     private static final boolean ENABLE_PER_WINDOW_INPUT_ROTATION =
@@ -384,7 +384,7 @@
     private void onNavigationSettingsChanged() {
         boolean wasBackAllowed = isHandlingGestures();
         updateCurrentUserResources();
-        if (wasBackAllowed != isHandlingGestures()) {
+        if (mStateChangeCallback != null && wasBackAllowed != isHandlingGestures()) {
             mStateChangeCallback.run();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/FloatingRotationButton.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/FloatingRotationButton.java
index 61118c5..4605795 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/FloatingRotationButton.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/FloatingRotationButton.java
@@ -20,48 +20,72 @@
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
-import android.view.Gravity;
 import android.view.LayoutInflater;
-import android.view.Surface;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.WindowManager;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.FrameLayout;
 
 import com.android.systemui.R;
 import com.android.systemui.navigationbar.RotationButton;
 import com.android.systemui.navigationbar.RotationButtonController;
 import com.android.systemui.navigationbar.buttons.KeyButtonDrawable;
 import com.android.systemui.navigationbar.buttons.KeyButtonView;
+import com.android.systemui.navigationbar.gestural.FloatingRotationButtonPositionCalculator.Position;
 
-import java.util.function.Consumer;
-
-/** Containing logic for the rotation button on the physical left bottom corner of the screen. */
+/**
+ * Containing logic for the rotation button on the physical left bottom corner of the screen.
+ */
 public class FloatingRotationButton implements RotationButton {
 
     private static final float BACKGROUND_ALPHA = 0.92f;
+    private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300;
 
-    private final Context mContext;
     private final WindowManager mWindowManager;
+    private final ViewGroup mKeyButtonContainer;
     private final KeyButtonView mKeyButtonView;
-    private final int mDiameter;
-    private final int mMargin;
+
+    private final int mContainerSize;
+
     private KeyButtonDrawable mKeyButtonDrawable;
     private boolean mIsShowing;
     private boolean mCanShow = true;
+    private int mDisplayRotation;
+
+    private boolean mIsTaskbarVisible = false;
+    private boolean mIsTaskbarStashed = false;
+
+    private final FloatingRotationButtonPositionCalculator mPositionCalculator;
 
     private RotationButtonController mRotationButtonController;
-    private Consumer<Boolean> mVisibilityChangedCallback;
+    private RotationButtonUpdatesCallback mUpdatesCallback;
+    private Position mPosition;
 
     public FloatingRotationButton(Context context) {
-        mContext = context;
-        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
-        mKeyButtonView = (KeyButtonView) LayoutInflater.from(mContext).inflate(
+        mWindowManager = context.getSystemService(WindowManager.class);
+        mKeyButtonContainer = (ViewGroup) LayoutInflater.from(context).inflate(
                 R.layout.rotate_suggestion, null);
+        mKeyButtonView = mKeyButtonContainer.findViewById(R.id.rotate_suggestion);
         mKeyButtonView.setVisibility(View.VISIBLE);
 
-        Resources res = mContext.getResources();
-        mDiameter = res.getDimensionPixelSize(R.dimen.floating_rotation_button_diameter);
-        mMargin = Math.max(res.getDimensionPixelSize(R.dimen.floating_rotation_button_min_margin),
+        Resources res = context.getResources();
+
+        int defaultMargin = Math.max(
+                res.getDimensionPixelSize(R.dimen.floating_rotation_button_min_margin),
                 res.getDimensionPixelSize(R.dimen.rounded_corner_content_padding));
+
+        int taskbarMarginLeft =
+                res.getDimensionPixelSize(R.dimen.floating_rotation_button_taskbar_left_margin);
+        int taskbarMarginBottom =
+                res.getDimensionPixelSize(R.dimen.floating_rotation_button_taskbar_bottom_margin);
+
+        mPositionCalculator = new FloatingRotationButtonPositionCalculator(defaultMargin,
+                taskbarMarginLeft, taskbarMarginBottom);
+
+        final int diameter = res.getDimensionPixelSize(R.dimen.floating_rotation_button_diameter);
+        mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft,
+                taskbarMarginBottom));
     }
 
     @Override
@@ -72,8 +96,8 @@
     }
 
     @Override
-    public void setVisibilityChangedCallback(Consumer<Boolean> visibilityChangedCallback) {
-        mVisibilityChangedCallback = visibilityChangedCallback;
+    public void setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback) {
+        mUpdatesCallback = updatesCallback;
     }
 
     @Override
@@ -86,45 +110,39 @@
         if (!mCanShow || mIsShowing) {
             return false;
         }
+
         mIsShowing = true;
         int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(mDiameter, mDiameter,
-                mMargin, mMargin, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, flags,
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                mContainerSize,
+                mContainerSize,
+                0, 0, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, flags,
                 PixelFormat.TRANSLUCENT);
+
         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
         lp.setTitle("FloatingRotationButton");
         lp.setFitInsetsTypes(0 /*types */);
-        switch (mWindowManager.getDefaultDisplay().getRotation()) {
-            case Surface.ROTATION_0:
-                lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
-                break;
-            case Surface.ROTATION_90:
-                lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
-                break;
-            case Surface.ROTATION_180:
-                lp.gravity = Gravity.TOP | Gravity.RIGHT;
-                break;
-            case Surface.ROTATION_270:
-                lp.gravity = Gravity.TOP | Gravity.LEFT;
-                break;
-            default:
-                break;
-        }
-        mWindowManager.addView(mKeyButtonView, lp);
+
+        mDisplayRotation = mWindowManager.getDefaultDisplay().getRotation();
+        mPosition = mPositionCalculator
+                .calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed);
+
+        lp.gravity = mPosition.getGravity();
+        ((FrameLayout.LayoutParams) mKeyButtonView.getLayoutParams()).gravity =
+                mPosition.getGravity();
+
+        updateTranslation(mPosition, /* animate */ false);
+
+        mWindowManager.addView(mKeyButtonContainer, lp);
         if (mKeyButtonDrawable != null && mKeyButtonDrawable.canAnimate()) {
             mKeyButtonDrawable.resetAnimation();
             mKeyButtonDrawable.startAnimation();
         }
-        mKeyButtonView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
-            @Override
-            public void onLayoutChange(View view, int i, int i1, int i2, int i3, int i4, int i5,
-                    int i6, int i7) {
-                if (mIsShowing && mVisibilityChangedCallback != null) {
-                    mVisibilityChangedCallback.accept(true);
-                }
-                mKeyButtonView.removeOnLayoutChangeListener(this);
-            }
-        });
+
+        if (mUpdatesCallback != null) {
+            mUpdatesCallback.onVisibilityChanged(true);
+        }
+
         return true;
     }
 
@@ -133,10 +151,10 @@
         if (!mIsShowing) {
             return false;
         }
-        mWindowManager.removeViewImmediate(mKeyButtonView);
+        mWindowManager.removeViewImmediate(mKeyButtonContainer);
         mIsShowing = false;
-        if (mVisibilityChangedCallback != null) {
-            mVisibilityChangedCallback.accept(false);
+        if (mUpdatesCallback != null) {
+            mUpdatesCallback.onVisibilityChanged(false);
         }
         return true;
     }
@@ -183,4 +201,43 @@
             hide();
         }
     }
+
+    public void onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed) {
+        mIsTaskbarVisible = taskbarVisible;
+        mIsTaskbarStashed = taskbarStashed;
+
+        if (!mIsShowing) return;
+
+        final Position newPosition = mPositionCalculator
+                .calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed);
+
+        if (newPosition.getTranslationX() != mPosition.getTranslationX()
+                || newPosition.getTranslationY() != mPosition.getTranslationY()) {
+            updateTranslation(newPosition, /* animate */ true);
+            mPosition = newPosition;
+        }
+    }
+
+    private void updateTranslation(Position position, boolean animate) {
+        final int translationX = position.getTranslationX();
+        final int translationY = position.getTranslationY();
+
+        if (animate) {
+            mKeyButtonView
+                    .animate()
+                    .translationX(translationX)
+                    .translationY(translationY)
+                    .setDuration(MARGIN_ANIMATION_DURATION_MILLIS)
+                    .setInterpolator(new AccelerateDecelerateInterpolator())
+                    .withEndAction(() -> {
+                        if (mUpdatesCallback != null && mIsShowing) {
+                            mUpdatesCallback.onPositionChanged();
+                        }
+                    })
+                    .start();
+        } else {
+            mKeyButtonView.setTranslationX(translationX);
+            mKeyButtonView.setTranslationY(translationY);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculator.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculator.kt
new file mode 100644
index 0000000..3ce51ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculator.kt
@@ -0,0 +1,65 @@
+package com.android.systemui.navigationbar.gestural
+
+import android.view.Gravity
+import android.view.Surface
+
+/**
+ * Calculates gravity and translation that is necessary to display
+ * the button in the correct position based on the current state
+ */
+internal class FloatingRotationButtonPositionCalculator(
+    private val defaultMargin: Int,
+    private val taskbarMarginLeft: Int,
+    private val taskbarMarginBottom: Int
+) {
+
+    fun calculatePosition(
+        currentRotation: Int,
+        taskbarVisible: Boolean,
+        taskbarStashed: Boolean
+    ): Position {
+
+        val isTaskbarSide = currentRotation == Surface.ROTATION_0
+            || currentRotation == Surface.ROTATION_90
+        val useTaskbarMargin = isTaskbarSide && taskbarVisible && !taskbarStashed
+
+        val gravity = resolveGravity(currentRotation)
+
+        val marginLeft = if (useTaskbarMargin) taskbarMarginLeft else defaultMargin
+        val marginBottom = if (useTaskbarMargin) taskbarMarginBottom else defaultMargin
+
+        val translationX =
+            if (gravity and Gravity.RIGHT == Gravity.RIGHT) {
+                -marginLeft
+            } else {
+                marginLeft
+            }
+        val translationY =
+            if (gravity and Gravity.BOTTOM == Gravity.BOTTOM) {
+                -marginBottom
+            } else {
+                marginBottom
+            }
+
+        return Position(
+            gravity = gravity,
+            translationX = translationX,
+            translationY = translationY
+        )
+    }
+
+    data class Position(
+        val gravity: Int,
+        val translationX: Int,
+        val translationY: Int
+    )
+
+    private fun resolveGravity(rotation: Int): Int =
+        when (rotation) {
+            Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.LEFT
+            Surface.ROTATION_90 -> Gravity.BOTTOM or Gravity.RIGHT
+            Surface.ROTATION_180 -> Gravity.TOP or Gravity.RIGHT
+            Surface.ROTATION_270 -> Gravity.TOP or Gravity.LEFT
+            else -> throw IllegalArgumentException("Invalid rotation $rotation")
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index 7fdb79e..8d1dfc8 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -55,9 +55,11 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.plugins.NavigationEdgeBackPlugin;
+import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 import com.android.systemui.statusbar.VibratorHelper;
 
 import java.io.PrintWriter;
+import java.util.concurrent.Executor;
 
 public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPlugin {
 
@@ -349,6 +351,7 @@
                 .getDimension(R.dimen.navigation_edge_action_drag_threshold);
         setVisibility(GONE);
 
+        Executor backgroundExecutor = Dependency.get(Dependency.BACKGROUND_EXECUTOR);
         boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY;
         mRegionSamplingHelper = new RegionSamplingHelper(this,
                 new RegionSamplingHelper.SamplingCallback() {
@@ -366,7 +369,7 @@
                     public boolean isSamplingEnabled() {
                         return isPrimaryDisplay;
                     }
-                });
+                }, backgroundExecutor);
         mRegionSamplingHelper.setWindowVisible(true);
         mShowProtection = !isPrimaryDisplay;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginDependencyProvider.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginDependencyProvider.java
index ad1e21d..0b565ea 100644
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginDependencyProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginDependencyProvider.java
@@ -17,25 +17,27 @@
 import android.util.ArrayMap;
 
 import com.android.systemui.Dependency;
-import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.PluginDependency.DependencyProvider;
 import com.android.systemui.shared.plugins.PluginManager;
 
 import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Lazy;
 
 /**
  */
-@SysUISingleton
+@Singleton
 public class PluginDependencyProvider extends DependencyProvider {
 
     private final ArrayMap<Class<?>, Object> mDependencies = new ArrayMap<>();
-    private final PluginManager mManager;
+    private final Lazy<PluginManager> mManagerLazy;
 
     /**
      */
     @Inject
-    public PluginDependencyProvider(PluginManager manager) {
-        mManager = manager;
+    public PluginDependencyProvider(Lazy<PluginManager> managerLazy) {
+        mManagerLazy = managerLazy;
         PluginDependency.sProvider = this;
     }
 
@@ -51,7 +53,7 @@
 
     @Override
     <T> T get(Plugin p, Class<T> cls) {
-        if (!mManager.dependsOn(p, cls)) {
+        if (!mManagerLazy.get().dependsOn(p, cls)) {
             throw new IllegalArgumentException(p.getClass() + " does not depend on " + cls);
         }
         synchronized (mDependencies) {
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginEnablerImpl.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginEnablerImpl.java
index 6337415..40f59744 100644
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginEnablerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginEnablerImpl.java
@@ -22,16 +22,22 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.shared.plugins.PluginEnabler;
 
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** */
+@Singleton
 public class PluginEnablerImpl implements PluginEnabler {
     private static final String CRASH_DISABLED_PLUGINS_PREF_FILE = "auto_disabled_plugins_prefs";
 
-    private PackageManager mPm;
+    private final PackageManager mPm;
     private final SharedPreferences mAutoDisabledPrefs;
 
     public PluginEnablerImpl(Context context) {
         this(context, context.getPackageManager());
     }
 
+    @Inject
     @VisibleForTesting public PluginEnablerImpl(Context context, PackageManager pm) {
         mAutoDisabledPrefs = context.getSharedPreferences(
                 CRASH_DISABLED_PLUGINS_PREF_FILE, Context.MODE_PRIVATE);
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginInitializerImpl.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInitializerImpl.java
deleted file mode 100644
index 7f01d6f..0000000
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginInitializerImpl.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.Looper;
-import android.util.Log;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.R;
-import com.android.systemui.shared.plugins.PluginEnabler;
-import com.android.systemui.shared.plugins.PluginInitializer;
-import com.android.systemui.shared.plugins.PluginManagerImpl;
-
-public class PluginInitializerImpl implements PluginInitializer {
-
-    /**
-     * True if WTFs should lead to crashes
-     */
-    private static final boolean WTFS_SHOULD_CRASH = false;
-    private boolean mWtfsSet;
-
-    @Override
-    public Looper getBgLooper() {
-        return Dependency.get(Dependency.BG_LOOPER);
-    }
-
-    @Override
-    public void onPluginManagerInit() {
-        // Plugin dependencies that don't have another good home can go here, but
-        // dependencies that have better places to init can happen elsewhere.
-        Dependency.get(PluginDependencyProvider.class)
-                .allowPluginDependency(ActivityStarter.class);
-    }
-
-    @Override
-    public String[] getWhitelistedPlugins(Context context) {
-        return context.getResources().getStringArray(R.array.config_pluginWhitelist);
-    }
-
-    public PluginEnabler getPluginEnabler(Context context) {
-        return new PluginEnablerImpl(context);
-    }
-
-    @Override
-    public void handleWtfs() {
-        if (WTFS_SHOULD_CRASH && !mWtfsSet) {
-            mWtfsSet = true;
-            Log.setWtfHandler(new Log.TerribleFailureHandler() {
-                @Override
-                public void onTerribleFailure(String tag, Log.TerribleFailure what,
-                        boolean system) {
-                    throw new PluginManagerImpl.CrashWhilePluginActiveException(what);
-                }
-            });
-        }
-    }
-
-    @Override
-    public boolean isDebuggable() {
-        return Build.IS_DEBUGGABLE;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java
new file mode 100644
index 0000000..16b971f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import static com.android.systemui.util.concurrency.GlobalConcurrencyModule.PRE_HANDLER;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.PluginModule;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.shared.plugins.PluginActionManager;
+import com.android.systemui.shared.plugins.PluginEnabler;
+import com.android.systemui.shared.plugins.PluginInstance;
+import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.shared.plugins.PluginManagerImpl;
+import com.android.systemui.shared.plugins.PluginPrefs;
+import com.android.systemui.util.concurrency.GlobalConcurrencyModule;
+import com.android.systemui.util.concurrency.ThreadFactory;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Dagger Module for code related to plugins.
+ *
+ * Covers code both in com.android.systemui.plugins and code in
+ * com.android.systemui.shared.plugins.
+ */
+@Module(includes = {GlobalConcurrencyModule.class})
+public abstract class PluginsModule {
+    public static final String PLUGIN_THREAD = "plugin_thread";
+    public static final String PLUGIN_DEBUG = "plugin_debug";
+    public static final String PLUGIN_PRIVILEGED = "plugin_privileged";
+
+    @Provides
+    @Named(PLUGIN_DEBUG)
+    static boolean providesPluginDebug() {
+        return Build.IS_DEBUGGABLE;
+    }
+
+    @Binds
+    abstract PluginEnabler bindsPluginEnablerImpl(PluginEnablerImpl impl);
+
+    @Provides
+    @Singleton
+    static PluginInstance.Factory providesPluginInstanceFactory(
+            @Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins,
+            @Named(PLUGIN_DEBUG) boolean isDebug) {
+        return new PluginInstance.Factory(
+                PluginModule.class.getClassLoader(),
+                new PluginInstance.InstanceFactory<>(),
+                new PluginInstance.VersionChecker(),
+                privilegedPlugins,
+                isDebug);
+    }
+
+    @Provides
+    @Singleton
+    static PluginActionManager.Factory providePluginInstanceManagerFactory(Context context,
+            PackageManager packageManager, @Main Executor mainExecutor,
+            @Named(PLUGIN_THREAD) Executor pluginExecutor,
+            NotificationManager notificationManager, PluginEnabler pluginEnabler,
+            @Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins,
+            PluginInstance.Factory pluginInstanceFactory) {
+        return new PluginActionManager.Factory(
+                context, packageManager, mainExecutor, pluginExecutor,
+                notificationManager, pluginEnabler, privilegedPlugins, pluginInstanceFactory);
+    }
+
+    @Provides
+    @Singleton
+    @Named(PLUGIN_THREAD)
+    static Executor providesPluginExecutor(ThreadFactory threadFactory) {
+        return threadFactory.buildExecutorOnNewThread("plugin");
+    }
+
+    @Provides
+    @Singleton
+    static PluginManager providesPluginManager(
+            Context context,
+            PluginActionManager.Factory instanceManagerFactory,
+            @Named(PLUGIN_DEBUG) boolean debug,
+            @Named(PRE_HANDLER)
+                    Optional<Thread.UncaughtExceptionHandler> uncaughtExceptionHandlerOptional,
+            PluginEnabler pluginEnabler,
+            PluginPrefs pluginPrefs,
+            @Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins) {
+        return new PluginManagerImpl(context, instanceManagerFactory, debug,
+                uncaughtExceptionHandlerOptional, pluginEnabler, pluginPrefs,
+                privilegedPlugins);
+    }
+
+    @Provides
+    static PluginPrefs providesPluginPrefs(Context context) {
+        return new PluginPrefs(context);
+    }
+
+    @Provides
+    @Named(PLUGIN_PRIVILEGED)
+    static List<String> providesPrivilegedPlugins(Context context) {
+        return Arrays.asList(context.getResources().getStringArray(R.array.config_pluginWhitelist));
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index a888305..6252654 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -54,6 +54,7 @@
 import java.io.PrintWriter;
 import java.time.Duration;
 import java.util.Arrays;
+import java.util.Optional;
 import java.util.concurrent.Future;
 
 import javax.inject.Inject;
@@ -108,15 +109,15 @@
     private IThermalEventListener mUsbThermalEventListener;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final CommandQueue mCommandQueue;
-    private final Lazy<StatusBar> mStatusBarLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
 
     @Inject
     public PowerUI(Context context, BroadcastDispatcher broadcastDispatcher,
-            CommandQueue commandQueue, Lazy<StatusBar> statusBarLazy) {
+            CommandQueue commandQueue, Lazy<Optional<StatusBar>> statusBarOptionalLazy) {
         super(context);
         mBroadcastDispatcher = broadcastDispatcher;
         mCommandQueue = commandQueue;
-        mStatusBarLazy = statusBarLazy;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
     }
 
     public void start() {
@@ -710,7 +711,8 @@
             int status = temp.getStatus();
 
             if (status >= Temperature.THROTTLING_EMERGENCY) {
-                if (!mStatusBarLazy.get().isDeviceInVrMode()) {
+                final Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
+                if (!statusBarOptional.map(StatusBar::isDeviceInVrMode).orElse(false)) {
                     mWarnings.showHighTemperatureWarning();
                     Slog.d(TAG, "SkinThermalEventListener: notifyThrottling was called "
                             + ", current skin status = " + status
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
new file mode 100644
index 0000000..4f87cad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import android.content.Intent
+import android.os.UserManager
+import android.provider.Settings
+import android.view.View
+import android.widget.Toast
+import androidx.annotation.VisibleForTesting
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.nano.MetricsProto
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.globalactions.GlobalActionsDialogLite
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.FooterActionsController.ExpansionState.COLLAPSED
+import com.android.systemui.qs.FooterActionsController.ExpansionState.EXPANDED
+import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED
+import com.android.systemui.statusbar.phone.MultiUserSwitchController
+import com.android.systemui.statusbar.phone.SettingsButton
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.UserInfoController
+import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.ViewController
+import javax.inject.Inject
+import javax.inject.Named
+
+/**
+ * Manages [FooterActionsView] behaviour, both when it's placed in QS or QQS (split shade).
+ * Main difference between QS and QQS behaviour is condition when buttons should be visible,
+ * determined by [buttonsVisibleState]
+ */
+class FooterActionsController @Inject constructor(
+    view: FooterActionsView,
+    private val qsPanelController: QSPanelController,
+    private val activityStarter: ActivityStarter,
+    private val userManager: UserManager,
+    private val userInfoController: UserInfoController,
+    private val multiUserSwitchController: MultiUserSwitchController,
+    private val deviceProvisionedController: DeviceProvisionedController,
+    private val falsingManager: FalsingManager,
+    private val metricsLogger: MetricsLogger,
+    private val tunerService: TunerService,
+    private val globalActionsDialog: GlobalActionsDialogLite,
+    private val uiEventLogger: UiEventLogger,
+    @Named(PM_LITE_ENABLED) private val showPMLiteButton: Boolean,
+    private val buttonsVisibleState: ExpansionState
+) : ViewController<FooterActionsView>(view) {
+
+    enum class ExpansionState { COLLAPSED, EXPANDED }
+
+    private var listening: Boolean = false
+
+    var expanded = false
+        set(value) {
+            if (field != value) {
+                field = value
+                updateView()
+            }
+        }
+
+    private val settingsButton: SettingsButton = view.findViewById(R.id.settings_button)
+    private val settingsButtonContainer: View? = view.findViewById(R.id.settings_button_container)
+    private val editButton: View = view.findViewById(android.R.id.edit)
+    private val powerMenuLite: View = view.findViewById(R.id.pm_lite)
+
+    private val onUserInfoChangedListener = OnUserInfoChangedListener { _, picture, _ ->
+        val isGuestUser: Boolean = userManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser())
+        mView.onUserInfoChanged(picture, isGuestUser)
+    }
+
+    private val onClickListener = View.OnClickListener { v ->
+        // Don't do anything until views are unhidden. Don't do anything if the tap looks
+        // suspicious.
+        if (!buttonsVisible() || falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+            return@OnClickListener
+        }
+        if (v === settingsButton) {
+            if (!deviceProvisionedController.isCurrentUserSetup) {
+                // If user isn't setup just unlock the device and dump them back at SUW.
+                activityStarter.postQSRunnableDismissingKeyguard {}
+                return@OnClickListener
+            }
+            metricsLogger.action(
+                    if (expanded) MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH
+                    else MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH)
+            if (settingsButton.isTunerClick) {
+                activityStarter.postQSRunnableDismissingKeyguard {
+                    if (isTunerEnabled()) {
+                        tunerService.showResetRequest {
+                            // Relaunch settings so that the tuner disappears.
+                            startSettingsActivity()
+                        }
+                    } else {
+                        Toast.makeText(context, R.string.tuner_toast, Toast.LENGTH_LONG).show()
+                        tunerService.isTunerEnabled = true
+                    }
+                    startSettingsActivity()
+                }
+            } else {
+                startSettingsActivity()
+            }
+        } else if (v === powerMenuLite) {
+            uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
+            globalActionsDialog.showOrHideDialog(false, true)
+        }
+    }
+
+    private fun buttonsVisible(): Boolean {
+        return when (buttonsVisibleState) {
+            EXPANDED -> expanded
+            COLLAPSED -> !expanded
+        }
+    }
+
+    override fun onInit() {
+        multiUserSwitchController.init()
+    }
+
+    fun hideFooter() {
+        mView.visibility = View.GONE
+    }
+
+    fun showFooter() {
+        mView.visibility = View.VISIBLE
+        updateView()
+    }
+
+    private fun startSettingsActivity() {
+        val animationController = settingsButtonContainer?.let {
+            ActivityLaunchAnimator.Controller.fromView(
+                    it,
+                    InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON)
+            }
+        activityStarter.startActivity(Intent(Settings.ACTION_SETTINGS),
+                true /* dismissShade */, animationController)
+    }
+
+    @VisibleForTesting
+    public override fun onViewAttached() {
+        if (showPMLiteButton) {
+            powerMenuLite.visibility = View.VISIBLE
+            powerMenuLite.setOnClickListener(onClickListener)
+        } else {
+            powerMenuLite.visibility = View.GONE
+        }
+        settingsButton.setOnClickListener(onClickListener)
+        editButton.setOnClickListener(View.OnClickListener { view: View? ->
+            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                return@OnClickListener
+            }
+            activityStarter.postQSRunnableDismissingKeyguard { qsPanelController.showEdit(view) }
+        })
+
+        updateView()
+    }
+
+    private fun updateView() {
+        mView.updateEverything(buttonsVisible(), isTunerEnabled(),
+                multiUserSwitchController.isMultiUserEnabled)
+    }
+
+    override fun onViewDetached() {
+        setListening(false)
+    }
+
+    fun setListening(listening: Boolean) {
+        if (this.listening == listening) {
+            return
+        }
+        this.listening = listening
+        if (this.listening) {
+            userInfoController.addCallback(onUserInfoChangedListener)
+        } else {
+            userInfoController.removeCallback(onUserInfoChangedListener)
+        }
+    }
+
+    fun disable(state2: Int) {
+        mView.disable(buttonsVisible(), state2, isTunerEnabled(),
+                multiUserSwitchController.isMultiUserEnabled)
+    }
+
+    fun setExpansion(headerExpansionFraction: Float) {
+        mView.setExpansion(headerExpansionFraction)
+    }
+
+    fun updateAnimator(width: Int, numTiles: Int) {
+        mView.updateAnimator(width, numTiles)
+    }
+
+    fun setKeyguardShowing() {
+        mView.setKeyguardShowing()
+    }
+
+    fun refreshVisibility(shouldBeVisible: Boolean) {
+        if (shouldBeVisible) {
+            showFooter()
+        } else {
+            hideFooter()
+        }
+    }
+
+    private fun isTunerEnabled() = tunerService.isTunerEnabled
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsControllerBuilder.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsControllerBuilder.kt
new file mode 100644
index 0000000..f6c89a9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsControllerBuilder.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import android.os.UserManager
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.globalactions.GlobalActionsDialogLite
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.FooterActionsController.ExpansionState
+import com.android.systemui.qs.dagger.QSFlagsModule
+import com.android.systemui.statusbar.phone.MultiUserSwitchController
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.UserInfoController
+import com.android.systemui.tuner.TunerService
+import javax.inject.Inject
+import javax.inject.Named
+
+class FooterActionsControllerBuilder @Inject constructor(
+    private val qsPanelController: QSPanelController,
+    private val activityStarter: ActivityStarter,
+    private val userManager: UserManager,
+    private val userInfoController: UserInfoController,
+    private val multiUserSwitchControllerFactory: MultiUserSwitchController.Factory,
+    private val deviceProvisionedController: DeviceProvisionedController,
+    private val falsingManager: FalsingManager,
+    private val metricsLogger: MetricsLogger,
+    private val tunerService: TunerService,
+    private val globalActionsDialog: GlobalActionsDialogLite,
+    private val uiEventLogger: UiEventLogger,
+    @Named(QSFlagsModule.PM_LITE_ENABLED) private val showPMLiteButton: Boolean
+) {
+    private lateinit var view: FooterActionsView
+    private lateinit var buttonsVisibleState: ExpansionState
+
+    fun withView(view: FooterActionsView): FooterActionsControllerBuilder {
+        this.view = view
+        return this
+    }
+
+    fun withButtonsVisibleWhen(state: ExpansionState): FooterActionsControllerBuilder {
+        buttonsVisibleState = state
+        return this
+    }
+
+    fun build(): FooterActionsController {
+        return FooterActionsController(view, qsPanelController, activityStarter, userManager,
+                userInfoController, multiUserSwitchControllerFactory.create(view),
+                deviceProvisionedController, falsingManager, metricsLogger, tunerService,
+                globalActionsDialog, uiEventLogger, showPMLiteButton, buttonsVisibleState)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt
new file mode 100644
index 0000000..941e54a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.qs
+
+import android.app.StatusBarManager
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.RippleDrawable
+import android.os.UserManager
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import com.android.settingslib.Utils
+import com.android.settingslib.drawable.UserIconDrawable
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.MultiUserSwitch
+import com.android.systemui.statusbar.phone.SettingsButton
+
+/**
+ * Quick Settings bottom buttons placed in footer (aka utility bar) - always visible in expanded QS,
+ * in split shade mode visible also in collapsed state. May contain up to 5 buttons: settings,
+ * edit tiles, power off and conditionally: user switch and tuner
+ */
+class FooterActionsView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
+    private lateinit var settingsContainer: View
+    private lateinit var settingsButton: SettingsButton
+    private lateinit var multiUserSwitch: MultiUserSwitch
+    private lateinit var multiUserAvatar: ImageView
+    private lateinit var tunerIcon: View
+    private lateinit var editTilesButton: View
+
+    private var settingsCogAnimator: TouchAnimator? = null
+
+    private var qsDisabled = false
+    private var expansionAmount = 0f
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        editTilesButton = requireViewById(android.R.id.edit)
+        settingsButton = findViewById(R.id.settings_button)
+        settingsContainer = findViewById(R.id.settings_button_container)
+        multiUserSwitch = findViewById(R.id.multi_user_switch)
+        multiUserAvatar = multiUserSwitch.findViewById(R.id.multi_user_avatar)
+        tunerIcon = requireViewById(R.id.tuner_icon)
+
+        // RenderThread is doing more harm than good when touching the header (to expand quick
+        // settings), so disable it for this view
+        if (settingsButton.background is RippleDrawable) {
+            (settingsButton.background as RippleDrawable).setForceSoftware(true)
+        }
+        updateResources()
+        importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+    }
+
+    fun updateAnimator(width: Int, numTiles: Int) {
+        val size = (mContext.resources.getDimensionPixelSize(R.dimen.qs_quick_tile_size) -
+                mContext.resources.getDimensionPixelSize(R.dimen.qs_tile_padding))
+        val remaining = (width - numTiles * size) / (numTiles - 1)
+        val defSpace = mContext.resources.getDimensionPixelOffset(R.dimen.default_gear_space)
+        val translation = if (isLayoutRtl) (remaining - defSpace) else -(remaining - defSpace)
+        settingsCogAnimator = TouchAnimator.Builder()
+                .addFloat(settingsButton, "translationX", translation.toFloat(), 0f)
+                .addFloat(settingsButton, "rotation", -120f, 0f)
+                .build()
+        setExpansion(expansionAmount)
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        updateResources()
+    }
+
+    override fun onRtlPropertiesChanged(layoutDirection: Int) {
+        super.onRtlPropertiesChanged(layoutDirection)
+        updateResources()
+    }
+
+    private fun updateResources() {
+        val tunerIconTranslation = mContext.resources
+                .getDimensionPixelOffset(R.dimen.qs_footer_tuner_icon_translation).toFloat()
+        tunerIcon.translationX = if (isLayoutRtl) (-tunerIconTranslation) else tunerIconTranslation
+    }
+
+    fun setKeyguardShowing() {
+        setExpansion(expansionAmount)
+    }
+
+    fun setExpansion(headerExpansionFraction: Float) {
+        expansionAmount = headerExpansionFraction
+        if (settingsCogAnimator != null) settingsCogAnimator!!.setPosition(headerExpansionFraction)
+    }
+
+    fun disable(
+        buttonsVisible: Boolean,
+        state2: Int,
+        isTunerEnabled: Boolean,
+        multiUserEnabled: Boolean
+    ) {
+        val disabled = state2 and StatusBarManager.DISABLE2_QUICK_SETTINGS != 0
+        if (disabled == qsDisabled) return
+        qsDisabled = disabled
+        updateEverything(buttonsVisible, isTunerEnabled, multiUserEnabled)
+    }
+
+    fun updateEverything(
+        buttonsVisible: Boolean,
+        isTunerEnabled: Boolean,
+        multiUserEnabled: Boolean
+    ) {
+        post {
+            updateVisibilities(buttonsVisible, isTunerEnabled, multiUserEnabled)
+            updateClickabilities()
+            isClickable = false
+        }
+    }
+
+    private fun updateClickabilities() {
+        multiUserSwitch.isClickable = multiUserSwitch.visibility == VISIBLE
+        editTilesButton.isClickable = editTilesButton.visibility == VISIBLE
+        settingsButton.isClickable = settingsButton.visibility == VISIBLE
+    }
+
+    private fun updateVisibilities(
+        buttonsVisible: Boolean,
+        isTunerEnabled: Boolean,
+        multiUserEnabled: Boolean
+    ) {
+        settingsContainer.visibility = if (qsDisabled) GONE else VISIBLE
+        tunerIcon.visibility = if (isTunerEnabled) VISIBLE else INVISIBLE
+        multiUserSwitch.visibility = if (buttonsVisible && multiUserEnabled) VISIBLE else GONE
+        val isDemo = UserManager.isDeviceInDemoMode(context)
+        settingsButton.visibility = if (isDemo || !buttonsVisible) INVISIBLE else VISIBLE
+    }
+
+    fun onUserInfoChanged(picture: Drawable?, isGuestUser: Boolean) {
+        var pictureToSet = picture
+        if (picture != null && isGuestUser && picture !is UserIconDrawable) {
+            pictureToSet = picture.constantState.newDrawable(resources).mutate()
+            pictureToSet.setColorFilter(
+                    Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorForeground),
+                    PorterDuff.Mode.SRC_IN)
+        }
+        multiUserAvatar.setImageDrawable(pictureToSet)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 1bd3664..1a7a306 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -114,6 +114,7 @@
         super.onConfigurationChanged(newConfig);
         if (mLayoutOrientation != newConfig.orientation) {
             mLayoutOrientation = newConfig.orientation;
+            mDistributeTiles = true;
             setCurrentItem(0, false);
             mPageToRestore = 0;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PseudoGridView.java b/packages/SystemUI/src/com/android/systemui/qs/PseudoGridView.java
index 87c64c7..2f189be 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PseudoGridView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PseudoGridView.java
@@ -38,6 +38,7 @@
     private int mNumColumns = 3;
     private int mVerticalSpacing;
     private int mHorizontalSpacing;
+    private int mFixedChildWidth = -1;
 
     public PseudoGridView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -53,6 +54,8 @@
                 mVerticalSpacing = a.getDimensionPixelSize(attr, 0);
             } else if (attr == R.styleable.PseudoGridView_horizontalSpacing) {
                 mHorizontalSpacing = a.getDimensionPixelSize(attr, 0);
+            } else if (attr == R.styleable.PseudoGridView_fixedChildWidth) {
+                mFixedChildWidth = a.getDimensionPixelSize(attr, -1);
             }
         }
 
@@ -65,8 +68,15 @@
             throw new UnsupportedOperationException("Needs a maximum width");
         }
         int width = MeasureSpec.getSize(widthMeasureSpec);
-
-        int childWidth = (width - (mNumColumns - 1) * mHorizontalSpacing) / mNumColumns;
+        int childWidth;
+        int necessarySpaceForChildWidth =
+                mFixedChildWidth * mNumColumns + mHorizontalSpacing * (mNumColumns - 1);
+        if (mFixedChildWidth != -1 && necessarySpaceForChildWidth <= width) {
+            childWidth = mFixedChildWidth;
+            width = mFixedChildWidth * mNumColumns + mHorizontalSpacing * (mNumColumns - 1);
+        } else {
+            childWidth = (width - (mNumColumns - 1) * mHorizontalSpacing) / mNumColumns;
+        }
         int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
         int childHeightSpec = MeasureSpec.UNSPECIFIED;
         int totalHeight = 0;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index 4fcd46c..90d3448 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -14,6 +14,9 @@
 
 package com.android.systemui.qs;
 
+import static com.android.systemui.qs.dagger.QSFragmentModule.QQS_FOOTER;
+import static com.android.systemui.qs.dagger.QSFragmentModule.QS_FOOTER;
+
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.util.Log;
@@ -32,7 +35,6 @@
 import com.android.systemui.qs.TouchAnimator.Listener;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.tileimpl.HeightOverrideable;
-import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 import com.android.wm.shell.animation.Interpolators;
@@ -43,6 +45,7 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /** */
 @QSScope
@@ -60,20 +63,25 @@
     // Fade out faster than fade in to finish before QQS hides.
     private static final long QQS_FADE_OUT_DURATION = 50L;
 
-
+    /**
+     * List of all views that will be reset when clearing animation state
+     * see {@link #clearAnimationState()} }
+     */
     private final ArrayList<View> mAllViews = new ArrayList<>();
     /**
      * List of {@link View}s representing Quick Settings that are being animated from the quick QS
      * position to the normal QS panel. These views will only show once the animation is complete,
      * to prevent overlapping of semi transparent views
      */
-    private final ArrayList<View> mQuickQsViews = new ArrayList<>();
+    private final ArrayList<View> mAnimatedQsViews = new ArrayList<>();
     private final QuickQSPanel mQuickQsPanel;
     private final QSPanelController mQsPanelController;
     private final QuickQSPanelController mQuickQSPanelController;
     private final QuickStatusBarHeader mQuickStatusBarHeader;
     private final QSSecurityFooter mSecurityFooter;
     private final QS mQs;
+    private final View mQSFooterActions;
+    private final View mQQSFooterActions;
 
     private PagedTileLayout mPagedLayout;
 
@@ -88,6 +96,7 @@
     // This animates fading of SecurityFooter and media divider
     private TouchAnimator mAllPagesDelayedAnimator;
     private TouchAnimator mBrightnessAnimator;
+    private TouchAnimator mQQSFooterActionsAnimator;
     private HeightExpansionAnimator mQQSTileHeightAnimator;
     private HeightExpansionAnimator mOtherTilesExpandAnimator;
 
@@ -110,12 +119,16 @@
             QSPanelController qsPanelController,
             QuickQSPanelController quickQSPanelController, QSTileHost qsTileHost,
             QSSecurityFooter securityFooter, @Main Executor executor, TunerService tunerService,
-            QSExpansionPathInterpolator qsExpansionPathInterpolator) {
+            QSExpansionPathInterpolator qsExpansionPathInterpolator,
+            @Named(QS_FOOTER) FooterActionsView qsFooterActionsView,
+            @Named(QQS_FOOTER) FooterActionsView qqsFooterActionsView) {
         mQs = qs;
         mQuickQsPanel = quickPanel;
         mQsPanelController = qsPanelController;
         mQuickQSPanelController = quickQSPanelController;
         mQuickStatusBarHeader = quickStatusBarHeader;
+        mQQSFooterActions = qqsFooterActionsView;
+        mQSFooterActions = qsFooterActionsView;
         mSecurityFooter = securityFooter;
         mHost = qsTileHost;
         mExecutor = executor;
@@ -156,19 +169,6 @@
         }
     }
 
-    void startAlphaAnimation(boolean show) {
-        if (show == mToShowing) {
-            return;
-        }
-        mToShowing = show;
-        if (show) {
-            CrossFadeHelper.fadeIn(mQs.getView(), QQS_FADE_IN_DURATION, 0 /* delay */);
-        } else {
-            CrossFadeHelper.fadeOut(mQs.getView(), QQS_FADE_OUT_DURATION, 0 /* delay */,
-                    null /* endRunnable */);
-        }
-    }
-
     /**
      * Sets whether or not the keyguard is currently being shown with a collapsed header.
      */
@@ -262,7 +262,7 @@
 
         clearAnimationState();
         mAllViews.clear();
-        mQuickQsViews.clear();
+        mAnimatedQsViews.clear();
         mQQSTileHeightAnimator = null;
         mOtherTilesExpandAnimator = null;
 
@@ -360,7 +360,7 @@
 
                     firstPageBuilder.addFloat(quickTileView.getSecondaryLabel(), "alpha", 0, 1);
 
-                    mQuickQsViews.add(tileView);
+                    mAnimatedQsViews.add(tileView);
                     mAllViews.add(quickTileView);
                     mAllViews.add(quickTileView.getSecondaryLabel());
                 } else if (mFullRows && isIconInAnimatedRow(count)) {
@@ -386,6 +386,7 @@
                     tileView.setClipChildren(true);
                     tileView.setClipToPadding(true);
                     firstPageBuilder.addFloat(tileView.getSecondaryLabel(), "alpha", 0, 1);
+                    mAllViews.add(tileView.getSecondaryLabel());
                 }
 
                 mAllViews.add(tileView);
@@ -394,21 +395,8 @@
         }
 
         if (mAllowFancy) {
-            // Make brightness appear static position and alpha in through second half.
-            View brightness = mQsPanelController.getBrightnessView();
-            if (brightness != null) {
-                firstPageBuilder.addFloat(brightness, "translationY",
-                        brightness.getMeasuredHeight() * 0.5f, 0);
-                mBrightnessAnimator = new TouchAnimator.Builder()
-                        .addFloat(brightness, "alpha", 0, 1)
-                        .addFloat(brightness, "sliderScaleY", 0.3f, 1)
-                        .setInterpolator(Interpolators.ALPHA_IN)
-                        .setStartDelay(0.3f)
-                        .build();
-                mAllViews.add(brightness);
-            } else {
-                mBrightnessAnimator = null;
-            }
+            animateBrightnessSlider(firstPageBuilder);
+
             mFirstPageAnimator = firstPageBuilder
                     .setListener(this)
                     .build();
@@ -417,6 +405,13 @@
                     .addFloat(tileLayout, "alpha", 0, 1);
             mFirstPageDelayedAnimator = builder.build();
 
+            if (mQQSFooterActions.getVisibility() != View.GONE) {
+                // only when qqs footer is present (which means split shade mode) it needs to
+                // be animated
+                updateQQSFooterAnimation();
+            }
+
+
             // Fade in the security footer and the divider as we reach the final position
             builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY);
             builder.addFloat(mSecurityFooter.getView(), "alpha", 0, 1);
@@ -452,6 +447,53 @@
                 .addFloat(tileLayout, "alpha", 0, 1).build();
     }
 
+    private void animateBrightnessSlider(Builder firstPageBuilder) {
+        View qsBrightness = mQsPanelController.getBrightnessView();
+        View qqsBrightness = mQuickQSPanelController.getBrightnessView();
+        if (qqsBrightness != null && qqsBrightness.getVisibility() == View.VISIBLE) {
+            // animating in split shade mode
+            mAnimatedQsViews.add(qsBrightness);
+            mAllViews.add(qqsBrightness);
+            int translationY = getRelativeTranslationY(qsBrightness, qqsBrightness);
+            mBrightnessAnimator = new Builder()
+                    // we need to animate qs brightness even if animation will not be visible,
+                    // as we might start from sliderScaleY set to 0.3 if device was in collapsed QS
+                    // portrait orientation before
+                    .addFloat(qsBrightness, "sliderScaleY", 0.3f, 1)
+                    .addFloat(qqsBrightness, "translationY", 0, translationY)
+                    .build();
+        } else if (qsBrightness != null) {
+            firstPageBuilder.addFloat(qsBrightness, "translationY",
+                    qsBrightness.getMeasuredHeight() * 0.5f, 0);
+            mBrightnessAnimator = new Builder()
+                    .addFloat(qsBrightness, "alpha", 0, 1)
+                    .addFloat(qsBrightness, "sliderScaleY", 0.3f, 1)
+                    .setInterpolator(Interpolators.ALPHA_IN)
+                    .setStartDelay(0.3f)
+                    .build();
+            mAllViews.add(qsBrightness);
+        } else {
+            mBrightnessAnimator = null;
+        }
+    }
+
+    private void updateQQSFooterAnimation() {
+        int translationY = getRelativeTranslationY(mQSFooterActions, mQQSFooterActions);
+        mQQSFooterActionsAnimator = new TouchAnimator.Builder()
+                .addFloat(mQQSFooterActions, "translationY", 0, translationY)
+                .build();
+        mAnimatedQsViews.add(mQSFooterActions);
+    }
+
+    private int getRelativeTranslationY(View view1, View view2) {
+        int[] qsPosition = new int[2];
+        int[] qqsPosition = new int[2];
+        View commonView = mQs.getView();
+        getRelativePositionInt(qsPosition, view1, commonView);
+        getRelativePositionInt(qqsPosition, view2, commonView);
+        return (qsPosition[1] - qqsPosition[1]) - mQuickStatusBarHeader.getOffsetTranslation();
+    }
+
     private boolean isIconInAnimatedRow(int count) {
         if (mPagedLayout == null) {
             return false;
@@ -521,6 +563,9 @@
             if (mBrightnessAnimator != null) {
                 mBrightnessAnimator.setPosition(position);
             }
+            if (mQQSFooterActionsAnimator != null) {
+                mQQSFooterActionsAnimator.setPosition(position);
+            }
         }
     }
 
@@ -532,9 +577,9 @@
     @Override
     public void onAnimationAtEnd() {
         mQuickQsPanel.setVisibility(View.INVISIBLE);
-        final int N = mQuickQsViews.size();
+        final int N = mAnimatedQsViews.size();
         for (int i = 0; i < N; i++) {
-            mQuickQsViews.get(i).setVisibility(View.VISIBLE);
+            mAnimatedQsViews.get(i).setVisibility(View.VISIBLE);
         }
     }
 
@@ -542,9 +587,9 @@
     public void onAnimationStarted() {
         updateQQSVisibility();
         if (mOnFirstPage) {
-            final int N = mQuickQsViews.size();
+            final int N = mAnimatedQsViews.size();
             for (int i = 0; i < N; i++) {
-                mQuickQsViews.get(i).setVisibility(View.INVISIBLE);
+                mAnimatedQsViews.get(i).setVisibility(View.INVISIBLE);
             }
         }
     }
@@ -569,9 +614,9 @@
         if (mOtherTilesExpandAnimator != null) {
             mOtherTilesExpandAnimator.resetViewsHeights();
         }
-        final int N2 = mQuickQsViews.size();
+        final int N2 = mAnimatedQsViews.size();
         for (int i = 0; i < N2; i++) {
-            mQuickQsViews.get(i).setVisibility(View.VISIBLE);
+            mAnimatedQsViews.get(i).setVisibility(View.VISIBLE);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 6f12e46..3fc4f50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -26,8 +26,8 @@
 import android.graphics.PointF;
 import android.util.AttributeSet;
 import android.view.View;
-import android.view.WindowInsets;
 import android.widget.FrameLayout;
+import android.widget.ImageView;
 
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
@@ -52,11 +52,11 @@
     private float mQsExpansion;
     private QSCustomizer mQSCustomizer;
     private NonInterceptingScrollView mQSPanelContainer;
+    private ImageView mDragHandle;
 
     private int mSideMargins;
     private boolean mQsDisabled;
     private int mContentPadding = -1;
-    private int mNavBarInset = 0;
     private boolean mClippingEnabled;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
@@ -70,6 +70,7 @@
         mQSDetail = findViewById(R.id.qs_detail);
         mHeader = findViewById(R.id.header);
         mQSCustomizer = findViewById(R.id.qs_customize);
+        mDragHandle = findViewById(R.id.qs_drag_handle);
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
     }
 
@@ -92,24 +93,13 @@
     }
 
     @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mNavBarInset = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
-        mQSPanelContainer.setPaddingRelative(
-                mQSPanelContainer.getPaddingStart(),
-                mQSPanelContainer.getPaddingTop(),
-                mQSPanelContainer.getPaddingEnd(),
-                mNavBarInset
-        );
-        return super.onApplyWindowInsets(insets);
-    }
-
-    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
         // bottom and footer are inside the screen.
         MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
 
-        int maxQs = getDisplayHeight() - layoutParams.topMargin - layoutParams.bottomMargin
+        int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec);
+        int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
                 - getPaddingBottom();
         int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
                 + layoutParams.rightMargin;
@@ -119,11 +109,11 @@
                 MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
         int width = mQSPanelContainer.getMeasuredWidth() + padding;
         super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
+                MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
         // QSCustomizer will always be the height of the screen, but do this after
         // other measuring to avoid changing the height of the QS.
         mQSCustomizer.measure(widthMeasureSpec,
-                MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
+                MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
     }
 
     @Override
@@ -165,8 +155,8 @@
             QuickStatusBarHeaderController quickStatusBarHeaderController) {
         mQSPanelContainer.setPaddingRelative(
                 getPaddingStart(),
-                mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.quick_qs_offset_height),
+                mContext.getResources()
+                        .getDimensionPixelSize(R.dimen.qs_header_system_icons_area_height),
                 getPaddingEnd(),
                 getPaddingBottom()
         );
@@ -200,13 +190,23 @@
         mQSDetail.setBottom(getTop() + scrollBottom);
         int qsDetailBottomMargin = ((MarginLayoutParams) mQSDetail.getLayoutParams()).bottomMargin;
         mQSDetail.setBottom(getTop() + scrollBottom - qsDetailBottomMargin);
+        // Pin the drag handle to the bottom of the panel.
+        mDragHandle.setTranslationY(scrollBottom - mDragHandle.getHeight());
     }
 
     protected int calculateContainerHeight() {
         int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight();
+        // Need to add the dragHandle height so touches will be intercepted by it.
+        int dragHandleHeight;
+        if (mDragHandle.getVisibility() == VISIBLE) {
+            dragHandleHeight = Math.round((1 - mQsExpansion) * mDragHandle.getHeight());
+        } else {
+            dragHandleHeight = 0;
+        }
         return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight()
                 : Math.round(mQsExpansion * (heightOverride - mHeader.getHeight()))
-                + mHeader.getHeight();
+                + mHeader.getHeight()
+                + dragHandleHeight;
     }
 
     int calculateContainerBottom() {
@@ -221,6 +221,8 @@
     public void setExpansion(float expansion) {
         mQsExpansion = expansion;
         mQSPanelContainer.setScrollingEnabled(expansion > 0f);
+        mDragHandle.setAlpha(1.0f - expansion);
+        mDragHandle.setClickable(expansion == 0f); // Only clickable when fully collapsed
         updateExpansion();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
index 929927e..58a942a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
@@ -46,8 +46,8 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 
 public class QSDetail extends LinearLayout {
 
@@ -86,7 +86,7 @@
     private boolean mSwitchState;
     private QSFooter mFooter;
 
-    private NotificationsQuickSettingsContainer mContainer;
+    private QSContainerController mQsContainerController;
 
     public QSDetail(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
@@ -120,8 +120,8 @@
         mClipper = new QSDetailClipper(this);
     }
 
-    public void setContainer(NotificationsQuickSettingsContainer container) {
-        mContainer = container;
+    public void setContainerController(QSContainerController controller) {
+        mQsContainerController = controller;
     }
 
     /** */
@@ -262,8 +262,8 @@
         }
         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
         animateDetailVisibleDiff(x, y, visibleDiff, listener);
-        if (mContainer != null) {
-            mContainer.setDetailShowing(showingDetail);
+        if (mQsContainerController != null) {
+            mQsContainerController.setDetailShowing(showingDetail);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
index e38bd4b..0e0681b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
@@ -35,11 +35,6 @@
     void setExpanded(boolean expanded);
 
     /**
-     * Returns the full height of the footer.
-     */
-    int getHeight();
-
-    /**
      * Sets the percentage amount that the quick settings has been expanded.
      *
      * @param expansion A value from 1 to 0 that indicates how much the quick settings have been
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java
index 57438d1..4d23958 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java
@@ -21,60 +21,40 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.RippleDrawable;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.provider.Settings;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
-import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.settingslib.Utils;
 import com.android.settingslib.development.DevelopmentSettingsEnabler;
-import com.android.settingslib.drawable.UserIconDrawable;
 import com.android.systemui.R;
-import com.android.systemui.qs.TouchAnimator.Builder;
-import com.android.systemui.statusbar.phone.MultiUserSwitch;
-import com.android.systemui.statusbar.phone.SettingsButton;
 
-/** */
+/**
+ * Footer of expanded Quick Settings, tiles page indicator, (optionally) build number and
+ * {@link FooterActionsView}
+ */
 public class QSFooterView extends FrameLayout {
-    private SettingsButton mSettingsButton;
-    protected View mSettingsContainer;
     private PageIndicator mPageIndicator;
     private TextView mBuildText;
-    private boolean mShouldShowBuildText;
-
-    private boolean mQsDisabled;
-
-    private boolean mExpanded;
-
-    private boolean mListening;
-
-    protected MultiUserSwitch mMultiUserSwitch;
-    private ImageView mMultiUserAvatar;
+    private View mActionsContainer;
 
     protected TouchAnimator mFooterAnimator;
+
+    private boolean mQsDisabled;
+    private boolean mExpanded;
     private float mExpansionAmount;
 
-    protected View mEdit;
-    private TouchAnimator mSettingsCogAnimator;
-
-    private View mActionsContainer;
-    private View mTunerIcon;
-    private int mTunerIconTranslation;
+    private boolean mShouldShowBuildText;
 
     private OnClickListener mExpandClickListener;
 
@@ -94,27 +74,11 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mEdit = requireViewById(android.R.id.edit);
-
         mPageIndicator = findViewById(R.id.footer_page_indicator);
-
-        mSettingsButton = findViewById(R.id.settings_button);
-        mSettingsContainer = findViewById(R.id.settings_button_container);
-
-        mMultiUserSwitch = findViewById(R.id.multi_user_switch);
-        mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar);
-
-        mActionsContainer = requireViewById(R.id.qs_footer_actions_container);
+        mActionsContainer = requireViewById(R.id.qs_footer_actions);
         mBuildText = findViewById(R.id.build);
-        mTunerIcon = requireViewById(R.id.tuner_icon);
 
-        // RenderThread is doing more harm than good when touching the header (to expand quick
-        // settings), so disable it for this view
-        if (mSettingsButton.getBackground() instanceof RippleDrawable) {
-            ((RippleDrawable) mSettingsButton.getBackground()).setForceSoftware(true);
-        }
         updateResources();
-
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
         setBuildText();
     }
@@ -137,18 +101,7 @@
         }
     }
 
-    void updateAnimator(int width, int numTiles) {
-        int size = mContext.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size)
-                - mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_padding);
-        int remaining = (width - numTiles * size) / (numTiles - 1);
-        int defSpace = mContext.getResources().getDimensionPixelOffset(R.dimen.default_gear_space);
-
-        mSettingsCogAnimator = new Builder()
-                .addFloat(mSettingsButton, "translationX",
-                        isLayoutRtl() ? (remaining - defSpace) : -(remaining - defSpace), 0)
-                .addFloat(mSettingsButton, "rotation", -120, 0)
-                .build();
-
+    void updateExpansion() {
         setExpansion(mExpansionAmount);
     }
 
@@ -158,20 +111,11 @@
         updateResources();
     }
 
-    @Override
-    public void onRtlPropertiesChanged(int layoutDirection) {
-        super.onRtlPropertiesChanged(layoutDirection);
-        updateResources();
-    }
-
     private void updateResources() {
         updateFooterAnimator();
         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
         lp.bottomMargin = getResources().getDimensionPixelSize(R.dimen.qs_footers_margin_bottom);
         setLayoutParams(lp);
-        mTunerIconTranslation = mContext.getResources()
-                .getDimensionPixelOffset(R.dimen.qs_footer_tuner_icon_translation);
-        mTunerIcon.setTranslationX(isLayoutRtl() ? -mTunerIconTranslation : mTunerIconTranslation);
     }
 
     private void updateFooterAnimator() {
@@ -197,17 +141,15 @@
         mExpandClickListener = onClickListener;
     }
 
-    void setExpanded(boolean expanded, boolean isTunerEnabled, boolean multiUserEnabled) {
+    void setExpanded(boolean expanded) {
         if (mExpanded == expanded) return;
         mExpanded = expanded;
-        updateEverything(isTunerEnabled, multiUserEnabled);
+        updateEverything();
     }
 
     /** */
     public void setExpansion(float headerExpansionFraction) {
         mExpansionAmount = headerExpansionFraction;
-        if (mSettingsCogAnimator != null) mSettingsCogAnimator.setPosition(headerExpansionFraction);
-
         if (mFooterAnimator != null) {
             mFooterAnimator.setPosition(headerExpansionFraction);
         }
@@ -228,14 +170,6 @@
         super.onDetachedFromWindow();
     }
 
-    /** */
-    public void setListening(boolean listening) {
-        if (listening == mListening) {
-            return;
-        }
-        mListening = listening;
-    }
-
     @Override
     public boolean performAccessibilityAction(int action, Bundle arguments) {
         if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
@@ -253,50 +187,26 @@
         info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
     }
 
-    void disable(int state2, boolean isTunerEnabled, boolean multiUserEnabled) {
+    void disable(int state2) {
         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
         if (disabled == mQsDisabled) return;
         mQsDisabled = disabled;
-        updateEverything(isTunerEnabled, multiUserEnabled);
+        updateEverything();
     }
 
-    void updateEverything(boolean isTunerEnabled, boolean multiUserEnabled) {
+    void updateEverything() {
         post(() -> {
-            updateVisibilities(isTunerEnabled, multiUserEnabled);
+            updateVisibilities();
             updateClickabilities();
             setClickable(false);
         });
     }
 
     private void updateClickabilities() {
-        mMultiUserSwitch.setClickable(mMultiUserSwitch.getVisibility() == View.VISIBLE);
-        mEdit.setClickable(mEdit.getVisibility() == View.VISIBLE);
-        mSettingsButton.setClickable(mSettingsButton.getVisibility() == View.VISIBLE);
         mBuildText.setLongClickable(mBuildText.getVisibility() == View.VISIBLE);
     }
 
-    private void updateVisibilities(boolean isTunerEnabled, boolean multiUserEnabled) {
-        mSettingsContainer.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
-        mTunerIcon.setVisibility(isTunerEnabled ? View.VISIBLE : View.INVISIBLE);
-        final boolean isDemo = UserManager.isDeviceInDemoMode(mContext);
-        mMultiUserSwitch.setVisibility(
-                showUserSwitcher(multiUserEnabled) ? View.VISIBLE : View.GONE);
-        mSettingsButton.setVisibility(isDemo || !mExpanded ? View.INVISIBLE : View.VISIBLE);
-
+    private void updateVisibilities() {
         mBuildText.setVisibility(mExpanded && mShouldShowBuildText ? View.VISIBLE : View.INVISIBLE);
     }
-
-    private boolean showUserSwitcher(boolean multiUserEnabled) {
-        return mExpanded && multiUserEnabled;
-    }
-
-    void onUserInfoChanged(Drawable picture, boolean isGuestUser) {
-        if (picture != null && isGuestUser && !(picture instanceof UserIconDrawable)) {
-            picture = picture.getConstantState().newDrawable(getResources()).mutate();
-            picture.setColorFilter(
-                    Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorForeground),
-                    Mode.SRC_IN);
-        }
-        mMultiUserAvatar.setImageDrawable(picture);
-    }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
index 929aeda..e7c06e3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
@@ -16,35 +16,18 @@
 
 package com.android.systemui.qs;
 
-import static com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED;
+import static com.android.systemui.qs.dagger.QSFragmentModule.QS_FOOTER;
 
 import android.content.ClipData;
 import android.content.ClipboardManager;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.os.UserManager;
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.TextView;
 import android.widget.Toast;
 
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.nano.MetricsProto;
-import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.globalactions.GlobalActionsDialogLite;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.phone.MultiUserSwitchController;
-import com.android.systemui.statusbar.phone.SettingsButton;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.UserInfoController;
-import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.ViewController;
 
 import javax.inject.Inject;
@@ -56,137 +39,45 @@
 @QSScope
 public class QSFooterViewController extends ViewController<QSFooterView> implements QSFooter {
 
-    private final UserManager mUserManager;
-    private final UserInfoController mUserInfoController;
-    private final ActivityStarter mActivityStarter;
-    private final DeviceProvisionedController mDeviceProvisionedController;
     private final UserTracker mUserTracker;
     private final QSPanelController mQsPanelController;
     private final QuickQSPanelController mQuickQSPanelController;
-    private final TunerService mTunerService;
-    private final MetricsLogger mMetricsLogger;
-    private final FalsingManager mFalsingManager;
-    private final MultiUserSwitchController mMultiUserSwitchController;
-    private final SettingsButton mSettingsButton;
-    private final View mSettingsButtonContainer;
+    private final FooterActionsController mFooterActionsController;
     private final TextView mBuildText;
-    private final View mEdit;
     private final PageIndicator mPageIndicator;
-    private final View mPowerMenuLite;
-    private final boolean mShowPMLiteButton;
-    private final GlobalActionsDialogLite mGlobalActionsDialog;
-    private final UiEventLogger mUiEventLogger;
-
-    private final UserInfoController.OnUserInfoChangedListener mOnUserInfoChangedListener =
-            new UserInfoController.OnUserInfoChangedListener() {
-        @Override
-        public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
-            boolean isGuestUser = mUserManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser());
-            mView.onUserInfoChanged(picture, isGuestUser);
-        }
-    };
-
-    private final View.OnClickListener mSettingsOnClickListener = new View.OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            // Don't do anything until views are unhidden. Don't do anything if the tap looks
-            // suspicious.
-            if (!mExpanded || mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                return;
-            }
-
-            if (v == mSettingsButton) {
-                if (!mDeviceProvisionedController.isCurrentUserSetup()) {
-                    // If user isn't setup just unlock the device and dump them back at SUW.
-                    mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
-                    });
-                    return;
-                }
-                mMetricsLogger.action(
-                        mExpanded ? MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH
-                                : MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH);
-                if (mSettingsButton.isTunerClick()) {
-                    mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
-                        if (isTunerEnabled()) {
-                            mTunerService.showResetRequest(
-                                    () -> {
-                                        // Relaunch settings so that the tuner disappears.
-                                        startSettingsActivity();
-                                    });
-                        } else {
-                            Toast.makeText(getContext(), R.string.tuner_toast,
-                                    Toast.LENGTH_LONG).show();
-                            mTunerService.setTunerEnabled(true);
-                        }
-                        startSettingsActivity();
-
-                    });
-                } else {
-                    startSettingsActivity();
-                }
-            } else if (v == mPowerMenuLite) {
-                mUiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS);
-                mGlobalActionsDialog.showOrHideDialog(false, true);
-            }
-        }
-    };
-
-    private boolean mListening;
-    private boolean mExpanded;
 
     @Inject
-    QSFooterViewController(QSFooterView view, UserManager userManager,
-            UserInfoController userInfoController, ActivityStarter activityStarter,
-            DeviceProvisionedController deviceProvisionedController, UserTracker userTracker,
+    QSFooterViewController(QSFooterView view,
+            UserTracker userTracker,
             QSPanelController qsPanelController,
-            MultiUserSwitchController multiUserSwitchController,
             QuickQSPanelController quickQSPanelController,
-            TunerService tunerService, MetricsLogger metricsLogger, FalsingManager falsingManager,
-            @Named(PM_LITE_ENABLED) boolean showPMLiteButton,
-            GlobalActionsDialogLite globalActionsDialog, UiEventLogger uiEventLogger) {
+            @Named(QS_FOOTER) FooterActionsController footerActionsController) {
         super(view);
-        mUserManager = userManager;
-        mUserInfoController = userInfoController;
-        mActivityStarter = activityStarter;
-        mDeviceProvisionedController = deviceProvisionedController;
         mUserTracker = userTracker;
         mQsPanelController = qsPanelController;
         mQuickQSPanelController = quickQSPanelController;
-        mTunerService = tunerService;
-        mMetricsLogger = metricsLogger;
-        mFalsingManager = falsingManager;
-        mMultiUserSwitchController = multiUserSwitchController;
+        mFooterActionsController = footerActionsController;
 
-        mSettingsButton = mView.findViewById(R.id.settings_button);
-        mSettingsButtonContainer = mView.findViewById(R.id.settings_button_container);
         mBuildText = mView.findViewById(R.id.build);
-        mEdit = mView.findViewById(android.R.id.edit);
         mPageIndicator = mView.findViewById(R.id.footer_page_indicator);
-        mPowerMenuLite = mView.findViewById(R.id.pm_lite);
-        mShowPMLiteButton = showPMLiteButton;
-        mGlobalActionsDialog = globalActionsDialog;
-        mUiEventLogger = uiEventLogger;
     }
 
     @Override
     protected void onInit() {
         super.onInit();
-        mMultiUserSwitchController.init();
+        mFooterActionsController.init();
     }
 
     @Override
     protected void onViewAttached() {
-        if (mShowPMLiteButton) {
-            mPowerMenuLite.setVisibility(View.VISIBLE);
-            mPowerMenuLite.setOnClickListener(mSettingsOnClickListener);
-        } else {
-            mPowerMenuLite.setVisibility(View.GONE);
-        }
         mView.addOnLayoutChangeListener(
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
-                        mView.updateAnimator(
-                                right - left, mQuickQSPanelController.getNumQuickTiles()));
-        mSettingsButton.setOnClickListener(mSettingsOnClickListener);
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    mView.updateExpansion();
+                    mFooterActionsController.updateAnimator(right - left,
+                            mQuickQSPanelController.getNumQuickTiles());
+                }
+        );
+
         mBuildText.setOnLongClickListener(view -> {
             CharSequence buildText = mBuildText.getText();
             if (!TextUtils.isEmpty(buildText)) {
@@ -200,17 +91,8 @@
             }
             return false;
         });
-
-        mEdit.setOnClickListener(view -> {
-            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                return;
-            }
-            mActivityStarter.postQSRunnableDismissingKeyguard(() ->
-                    mQsPanelController.showEdit(view));
-        });
-
         mQsPanelController.setFooterPageIndicator(mPageIndicator);
-        mView.updateEverything(isTunerEnabled(), mMultiUserSwitchController.isMultiUserEnabled());
+        mView.updateEverything();
     }
 
     @Override
@@ -225,38 +107,25 @@
 
     @Override
     public void setExpanded(boolean expanded) {
-        mExpanded = expanded;
-        mView.setExpanded(
-                expanded, isTunerEnabled(), mMultiUserSwitchController.isMultiUserEnabled());
-    }
-
-    @Override
-    public int getHeight() {
-        return mView.getHeight();
+        mFooterActionsController.setExpanded(expanded);
+        mView.setExpanded(expanded);
     }
 
     @Override
     public void setExpansion(float expansion) {
         mView.setExpansion(expansion);
+        mFooterActionsController.setExpansion(expansion);
     }
 
     @Override
     public void setListening(boolean listening) {
-        if (mListening == listening) {
-            return;
-        }
-
-        mListening = listening;
-        if (mListening) {
-            mUserInfoController.addCallback(mOnUserInfoChangedListener);
-        } else {
-            mUserInfoController.removeCallback(mOnUserInfoChangedListener);
-        }
+        mFooterActionsController.setListening(listening);
     }
 
     @Override
     public void setKeyguardShowing(boolean keyguardShowing) {
         mView.setKeyguardShowing();
+        mFooterActionsController.setKeyguardShowing();
     }
 
     /** */
@@ -267,19 +136,7 @@
 
     @Override
     public void disable(int state1, int state2, boolean animate) {
-        mView.disable(state2, isTunerEnabled(), mMultiUserSwitchController.isMultiUserEnabled());
-    }
-
-    private void startSettingsActivity() {
-        ActivityLaunchAnimator.Controller animationController =
-                mSettingsButtonContainer != null ? ActivityLaunchAnimator.Controller.fromView(
-                        mSettingsButtonContainer,
-                        InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON) : null;
-        mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS),
-                true /* dismissShade */, animationController);
-    }
-
-    private boolean isTunerEnabled() {
-        return mTunerService.isTunerEnabled();
+        mView.disable(state2);
+        mFooterActionsController.disable(state2);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 1c841ec..6c0cf19 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -32,6 +32,7 @@
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -39,19 +40,20 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
+import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.util.InjectionInflationController;
 import com.android.systemui.util.LifecycleFragment;
@@ -89,7 +91,9 @@
     private int mLayoutDirection;
     private QSFooter mFooter;
     private float mLastQSExpansion = -1;
+    private float mLastPanelFraction;
     private boolean mQsDisabled;
+    private ImageView mQsDragHandler;
 
     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
     private final InjectionInflationController mInjectionInflater;
@@ -115,12 +119,11 @@
     private QuickQSPanelController mQuickQSPanelController;
     private QSCustomizerController mQSCustomizerController;
     private ScrollListener mScrollListener;
-    private FeatureFlags mFeatureFlags;
     /**
      * When true, QS will translate from outside the screen. It will be clipped with parallax
      * otherwise.
      */
-    private boolean mTranslateWhileExpanding;
+    private boolean mInSplitShade;
     private boolean mPulseExpanding;
 
     /**
@@ -135,6 +138,12 @@
 
     private DumpManager mDumpManager;
 
+    /**
+     * Progress of pull down from the center of the lock screen.
+     * @see com.android.systemui.statusbar.LockscreenShadeTransitionController
+     */
+    private float mFullShadeProgress;
+
     @Inject
     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
             InjectionInflationController injectionInflater, QSTileHost qsTileHost,
@@ -142,7 +151,7 @@
             QSDetailDisplayer qsDetailDisplayer, @Named(QS_PANEL) MediaHost qsMediaHost,
             @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
             KeyguardBypassController keyguardBypassController,
-            QSFragmentComponent.Factory qsComponentFactory, FeatureFlags featureFlags,
+            QSFragmentComponent.Factory qsComponentFactory,
             FalsingManager falsingManager, DumpManager dumpManager) {
         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
         mInjectionInflater = injectionInflater;
@@ -153,7 +162,6 @@
         mQsComponentFactory = qsComponentFactory;
         commandQueue.observe(getLifecycle(), this);
         mHost = qsTileHost;
-        mFeatureFlags = featureFlags;
         mFalsingManager = falsingManager;
         mBypassController = keyguardBypassController;
         mStatusBarStateController = statusBarStateController;
@@ -196,6 +204,7 @@
         mHeader = view.findViewById(R.id.header);
         mQSPanelController.setHeaderContainer(view.findViewById(R.id.header_text_container));
         mFooter = qsFragmentComponent.getQSFooter();
+        mQsDragHandler = view.findViewById(R.id.qs_drag_handle);
 
         mQsDetailDisplayer.setQsPanelController(mQSPanelController);
 
@@ -226,7 +235,8 @@
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     boolean sizeChanged = (oldTop - oldBottom) != (top - bottom);
                     if (sizeChanged) {
-                        setQsExpansion(mLastQSExpansion, mLastHeaderTranslation);
+                        setQsExpansion(mLastQSExpansion, mLastPanelFraction,
+                                mLastHeaderTranslation);
                     }
                 });
         mQSPanelController.setUsingHorizontalLayoutChangeListener(
@@ -237,6 +247,11 @@
                     mQSPanelController.getMediaHost().getHostView().setAlpha(1.0f);
                     mQSAnimator.requestAnimatorUpdate();
                 });
+
+        mQsDragHandler.setOnClickListener(v -> {
+            Log.d(TAG, "drag handler clicked");
+            mCommandQueue.animateExpandSettingsPanel(null);
+        });
     }
 
     @Override
@@ -302,6 +317,7 @@
                 mQSAnimator.onRtlChanged();
             }
         }
+        updateQsState();
     }
 
     @Override
@@ -330,11 +346,9 @@
     }
 
     @Override
-    public void setContainer(ViewGroup container) {
-        if (container instanceof NotificationsQuickSettingsContainer) {
-            mQSCustomizerController.setContainer((NotificationsQuickSettingsContainer) container);
-            mQSDetail.setContainer((NotificationsQuickSettingsContainer) container);
-        }
+    public void setContainerController(QSContainerController controller) {
+        mQSCustomizerController.setContainerController(controller);
+        mQSDetail.setContainerController(controller);
     }
 
     @Override
@@ -374,15 +388,19 @@
                 : View.INVISIBLE);
         mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
                 || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController);
-        mFooter.setVisibility(
-                !mQsDisabled && (mQsExpanded || !keyguardShowing || mHeaderAnimating
-                        || mShowCollapsedOnKeyguard)
+        mFooter.setVisibility(!mQsDisabled && (mQsExpanded || !keyguardShowing || mHeaderAnimating
+                || mShowCollapsedOnKeyguard)
                 ? View.VISIBLE
                 : View.INVISIBLE);
         mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
                 || (mQsExpanded && !mStackScrollerOverscrolling));
         mQSPanelController.setVisibility(
                 !mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE);
+        mQsDragHandler.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating
+                || mShowCollapsedOnKeyguard)
+                && Utils.shouldUseSplitNotificationShade(getResources())
+                ? View.VISIBLE
+                : View.GONE);
     }
 
     private boolean isKeyguardState() {
@@ -400,7 +418,7 @@
                 mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed);
             }
             if (!showCollapsed && isKeyguardState()) {
-                setQsExpansion(mLastQSExpansion, 0);
+                setQsExpansion(mLastQSExpansion, mLastPanelFraction, 0);
             }
         }
     }
@@ -409,6 +427,12 @@
         return mQSPanelController;
     }
 
+    public void setBrightnessMirrorController(
+            BrightnessMirrorController brightnessMirrorController) {
+        mQSPanelController.setBrightnessMirror(brightnessMirrorController);
+        mQuickQSPanelController.setBrightnessMirror(brightnessMirrorController);
+    }
+
     @Override
     public boolean isShowingDetail() {
         return mQSCustomizerController.isCustomizing() || mQSDetail.isShowingDetail();
@@ -462,33 +486,30 @@
     }
 
     @Override
-    public void setTranslateWhileExpanding(boolean shouldTranslate) {
-        mTranslateWhileExpanding = shouldTranslate;
-        mQSAnimator.setTranslateWhileExpanding(shouldTranslate);
+    public void setInSplitShade(boolean inSplitShade) {
+        mInSplitShade = inSplitShade;
+        mQSAnimator.setTranslateWhileExpanding(inSplitShade);
     }
 
     @Override
-    public void setTransitionToFullShadeAmount(float pxAmount, boolean animated) {
+    public void setTransitionToFullShadeAmount(float pxAmount, float progress) {
         boolean isTransitioningToFullShade = pxAmount > 0;
         if (isTransitioningToFullShade != mTransitioningToFullShade) {
             mTransitioningToFullShade = isTransitioningToFullShade;
             updateShowCollapsedOnKeyguard();
-            setQsExpansion(mLastQSExpansion, mLastHeaderTranslation);
         }
+        mFullShadeProgress = progress;
+        setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation);
     }
 
     @Override
-    public void setQsExpansion(float expansion, float proposedTranslation) {
-        if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + proposedTranslation);
+    public void setQsExpansion(float expansion, float panelExpansionFraction,
+            float proposedTranslation) {
         float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation;
-        if (mQSAnimator != null) {
-            final boolean showQSOnLockscreen = expansion > 0;
-            final boolean showQSUnlocked = headerTranslation == 0 || !mTranslateWhileExpanding;
-            mQSAnimator.startAlphaAnimation(showQSOnLockscreen || showQSUnlocked
-                    || mTransitioningToFullShade);
-        }
+        float progress = mTransitioningToFullShade ? mFullShadeProgress : panelExpansionFraction;
+        setAlphaAnimationProgress(mInSplitShade ? progress : 1);
         mContainer.setExpansion(expansion);
-        final float translationScaleY = (mTranslateWhileExpanding
+        final float translationScaleY = (mInSplitShade
                 ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1);
         boolean onKeyguardAndExpanded = isKeyguardState() && !mShowCollapsedOnKeyguard;
         if (!mHeaderAnimating && !headerWillBeAnimating()) {
@@ -505,6 +526,7 @@
             return;
         }
         mLastHeaderTranslation = headerTranslation;
+        mLastPanelFraction = panelExpansionFraction;
         mLastQSExpansion = expansion;
         mLastKeyguardAndExpanded = onKeyguardAndExpanded;
         mLastViewHeight = currentHeight;
@@ -546,6 +568,17 @@
         updateMediaPositions();
     }
 
+    private void setAlphaAnimationProgress(float progress) {
+        final View view = getView();
+        if (progress == 0 && view.getVisibility() != View.INVISIBLE) {
+            view.setVisibility(View.INVISIBLE);
+        } else if (progress > 0 && view.getVisibility() != View.VISIBLE) {
+            view.setVisibility((View.VISIBLE));
+        }
+        float alpha = ShadeInterpolation.getContentAlpha(progress);
+        view.setAlpha(alpha);
+    }
+
     private void updateQsBounds() {
         if (mLastQSExpansion == 1.0f) {
             // Fully expanded, let's set the layout bounds as clip bounds. This is necessary because
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index cde80e66..28aa884 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -45,7 +45,6 @@
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
-import com.android.systemui.util.animation.UniqueObjectHostView;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -322,7 +321,6 @@
         super.onConfigurationChanged(newConfig);
         mOnConfigurationChangedListeners.forEach(
                 listener -> listener.onConfigurationChange(newConfig));
-        switchSecurityFooter();
     }
 
     @Override
@@ -372,30 +370,21 @@
             switchToParent(mFooter, parent, index);
             index++;
         }
-
-        // The security footer is switched on orientation changes
     }
 
-    private void switchSecurityFooter() {
-        if (mSecurityFooter != null) {
-            if (mContext.getResources().getConfiguration().orientation
-                    == Configuration.ORIENTATION_LANDSCAPE && mHeaderContainer != null) {
-                // Adding the security view to the header, that enables us to avoid scrolling
-                switchToParent(mSecurityFooter, mHeaderContainer, 0);
-            } else {
-                // Where should this go? If there's media, right before it. Otherwise, at the end.
-                View mediaView = findViewByPredicate(v -> v instanceof UniqueObjectHostView);
-                int index = -1;
-                if (mediaView != null) {
-                    index = indexOfChild(mediaView);
-                }
-                if (mSecurityFooter.getParent() == this && indexOfChild(mSecurityFooter) < index) {
-                    // When we remove the securityFooter to rearrange, the index of media will go
-                    // down by one, so we correct it
-                    index--;
-                }
-                switchToParent(mSecurityFooter, this, index);
-            }
+    /** Switch the security footer between top and bottom of QS depending on orientation. */
+    public void switchSecurityFooter(boolean shouldUseSplitNotificationShade) {
+        if (mSecurityFooter == null) return;
+
+        if (!shouldUseSplitNotificationShade
+                && mContext.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE && mHeaderContainer != null) {
+            // Adding the security view to the header, that enables us to avoid scrolling
+            switchToParent(mSecurityFooter, mHeaderContainer, 0);
+        } else {
+            // Add after the footer
+            int index = indexOfChild(mFooter);
+            switchToParent(mSecurityFooter, this, index + 1);
         }
     }
 
@@ -666,9 +655,14 @@
         return mListening;
     }
 
-    public void setSecurityFooter(View view) {
+    /**
+     * Set the security footer view and switch it into the right place
+     * @param view the view in question
+     * @param shouldUseSplitNotificationShade if QS is in split shade mode
+     */
+    public void setSecurityFooter(View view, boolean shouldUseSplitNotificationShade) {
         mSecurityFooter = view;
-        switchSecurityFooter();
+        switchSecurityFooter(shouldUseSplitNotificationShade);
     }
 
     protected void setPageMargin(int pageMargin) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index ae0f510..70892a7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -40,8 +40,9 @@
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.settings.brightness.BrightnessController;
+import com.android.systemui.settings.brightness.BrightnessMirrorHandler;
 import com.android.systemui.settings.brightness.BrightnessSlider;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.tuner.TunerService;
 
@@ -60,11 +61,11 @@
     private final QSCustomizerController mQsCustomizerController;
     private final QSTileRevealController.Factory mQsTileRevealControllerFactory;
     private final FalsingManager mFalsingManager;
+    private final CommandQueue mCommandQueue;
     private final BrightnessController mBrightnessController;
-    private final BrightnessSlider.Factory mBrightnessSliderFactory;
     private final BrightnessSlider mBrightnessSlider;
+    private final BrightnessMirrorHandler mBrightnessMirrorHandler;
 
-    private BrightnessMirrorController mBrightnessMirrorController;
     private boolean mGridContentVisible = true;
 
     private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
@@ -76,13 +77,10 @@
             if (mView.isListening()) {
                 refreshAllTiles();
             }
-            updateBrightnessMirror();
+            mView.switchSecurityFooter(mShouldUseSplitNotificationShade);
         }
     };
 
-    private final BrightnessMirrorController.BrightnessMirrorListener mBrightnessMirrorListener =
-            mirror -> updateBrightnessMirror();
-
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
@@ -102,21 +100,22 @@
             DumpManager dumpManager, MetricsLogger metricsLogger, UiEventLogger uiEventLogger,
             QSLogger qsLogger, BrightnessController.Factory brightnessControllerFactory,
             BrightnessSlider.Factory brightnessSliderFactory, FalsingManager falsingManager,
-            FeatureFlags featureFlags) {
+            CommandQueue commandQueue) {
         super(view, qstileHost, qsCustomizerController, usingMediaPlayer, mediaHost,
-                metricsLogger, uiEventLogger, qsLogger, dumpManager, featureFlags);
+                metricsLogger, uiEventLogger, qsLogger, dumpManager);
         mQsSecurityFooter = qsSecurityFooter;
         mTunerService = tunerService;
         mQsCustomizerController = qsCustomizerController;
         mQsTileRevealControllerFactory = qsTileRevealControllerFactory;
         mFalsingManager = falsingManager;
+        mCommandQueue = commandQueue;
         mQsSecurityFooter.setHostEnvironment(qstileHost);
-        mBrightnessSliderFactory = brightnessSliderFactory;
 
-        mBrightnessSlider = mBrightnessSliderFactory.create(getContext(), mView);
+        mBrightnessSlider = brightnessSliderFactory.create(getContext(), mView);
         mView.setBrightnessView(mBrightnessSlider.getRootView());
 
         mBrightnessController = brightnessControllerFactory.create(mBrightnessSlider);
+        mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController);
     }
 
     @Override
@@ -141,11 +140,9 @@
             refreshAllTiles();
         }
         mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
-        mView.setSecurityFooter(mQsSecurityFooter.getView());
+        mView.setSecurityFooter(mQsSecurityFooter.getView(), mShouldUseSplitNotificationShade);
         switchTileLayout(true);
-        if (mBrightnessMirrorController != null) {
-            mBrightnessMirrorController.addCallback(mBrightnessMirrorListener);
-        }
+        mBrightnessMirrorHandler.onQsPanelAttached();
 
         ((PagedTileLayout) mView.getOrCreateTileLayout())
                 .setOnTouchListener(mTileLayoutTouchListener);
@@ -161,9 +158,7 @@
     protected void onViewDetached() {
         mTunerService.removeTunable(mView);
         mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
-        if (mBrightnessMirrorController != null) {
-            mBrightnessMirrorController.removeCallback(mBrightnessMirrorListener);
-        }
+        mBrightnessMirrorHandler.onQsPanelDettached();
         super.onViewDetached();
     }
 
@@ -197,23 +192,8 @@
         }
     }
 
-    /** */
     public void setBrightnessMirror(BrightnessMirrorController brightnessMirrorController) {
-        mBrightnessMirrorController = brightnessMirrorController;
-        if (mBrightnessMirrorController != null) {
-            mBrightnessMirrorController.removeCallback(mBrightnessMirrorListener);
-        }
-        mBrightnessMirrorController = brightnessMirrorController;
-        if (mBrightnessMirrorController != null) {
-            mBrightnessMirrorController.addCallback(mBrightnessMirrorListener);
-        }
-        updateBrightnessMirror();
-    }
-
-    private void updateBrightnessMirror() {
-        if (mBrightnessMirrorController != null) {
-            mBrightnessSlider.setMirrorControllerAndMirror(mBrightnessMirrorController);
-        }
+        mBrightnessMirrorHandler.setController(brightnessMirrorController);
     }
 
     /** Get the QSTileHost this panel uses. */
@@ -278,10 +258,6 @@
         return mView.isLayoutRtl();
     }
 
-    public View getBrightnessView() {
-        return mView.getBrightnessView();
-    }
-
     /** */
     public void setPageListener(PagedTileLayout.PageListener listener) {
         mView.setPageListener(listener);
@@ -298,6 +274,14 @@
 
     /** */
     public void showDetailAdapter(DetailAdapter detailAdapter, int x, int y) {
+        // TODO(b/199296365)
+        // Workaround for opening detail from QQS, when there might not be enough space to
+        // display e.g. in case of multiuser detail from split shade. Currently showing detail works
+        // only for QS (mView below) and that's why expanding panel (thus showing QS instead of QQS)
+        // makes it displayed correctly.
+        if (!isExpanded()) {
+            mCommandQueue.animateExpandSettingsPanel(null);
+        }
         mView.showDetailAdapter(true, detailAdapter, new int[]{x, y});
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 08cb4a9..f7d1b1e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -24,6 +24,7 @@
 import android.content.ComponentName;
 import android.content.res.Configuration;
 import android.metrics.LogMaker;
+import android.view.View;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
@@ -36,7 +37,6 @@
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.util.Utils;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.animation.DisappearParameters;
@@ -68,9 +68,8 @@
     private final UiEventLogger mUiEventLogger;
     private final QSLogger mQSLogger;
     private final DumpManager mDumpManager;
-    private final FeatureFlags mFeatureFlags;
     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
-    private boolean mShouldUseSplitNotificationShade;
+    protected boolean mShouldUseSplitNotificationShade;
 
     @Nullable
     private Consumer<Boolean> mMediaVisibilityChangedListener;
@@ -87,7 +86,8 @@
                 @Override
                 public void onConfigurationChange(Configuration newConfig) {
                     mShouldUseSplitNotificationShade =
-                            Utils.shouldUseSplitNotificationShade(mFeatureFlags, getResources());
+                            Utils.shouldUseSplitNotificationShade(getResources());
+                    onConfigurationChanged();
                     if (newConfig.orientation != mLastOrientation) {
                         mLastOrientation = newConfig.orientation;
                         switchTileLayout(false);
@@ -95,6 +95,8 @@
                 }
             };
 
+    protected void onConfigurationChanged() { }
+
     private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> {
         if (mMediaVisibilityChangedListener != null) {
             mMediaVisibilityChangedListener.accept(visible);
@@ -117,8 +119,7 @@
             MetricsLogger metricsLogger,
             UiEventLogger uiEventLogger,
             QSLogger qsLogger,
-            DumpManager dumpManager,
-            FeatureFlags featureFlags
+            DumpManager dumpManager
     ) {
         super(view);
         mHost = host;
@@ -129,9 +130,8 @@
         mUiEventLogger = uiEventLogger;
         mQSLogger = qsLogger;
         mDumpManager = dumpManager;
-        mFeatureFlags = featureFlags;
         mShouldUseSplitNotificationShade =
-                Utils.shouldUseSplitNotificationShade(mFeatureFlags, getResources());
+                Utils.shouldUseSplitNotificationShade(getResources());
     }
 
     @Override
@@ -408,6 +408,10 @@
         mUsingHorizontalLayoutChangedListener = listener;
     }
 
+    public View getBrightnessView() {
+        return mView.getBrightnessView();
+    }
+
     /** */
     public static final class TileRecord extends QSPanel.Record {
         public QSTile tile;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index e60fb49..4e9b0f1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -39,6 +39,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -51,7 +52,6 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSBrightnessController.kt b/packages/SystemUI/src/com/android/systemui/qs/QuickQSBrightnessController.kt
new file mode 100644
index 0000000..14374ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSBrightnessController.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.settings.brightness.BrightnessController
+import com.android.systemui.settings.brightness.BrightnessSlider
+import com.android.systemui.settings.brightness.MirroredBrightnessController
+import com.android.systemui.statusbar.policy.BrightnessMirrorController
+import javax.inject.Inject
+
+/**
+ * Controls brightness slider in QQS, which is visible only in split shade. It's responsible for
+ * showing/hiding it when appropriate and (un)registering listeners
+ */
+class QuickQSBrightnessController @VisibleForTesting constructor(
+    private val brightnessControllerFactory: () -> BrightnessController
+) : MirroredBrightnessController {
+
+    @Inject constructor(
+        brightnessControllerFactory: BrightnessController.Factory,
+        brightnessSliderFactory: BrightnessSlider.Factory,
+        quickQSPanel: QuickQSPanel
+    ) : this(brightnessControllerFactory = {
+            val slider = brightnessSliderFactory.create(quickQSPanel.context, quickQSPanel)
+            slider.init()
+            quickQSPanel.setBrightnessView(slider.rootView)
+            brightnessControllerFactory.create(slider)
+        })
+
+    private var isListening = false
+    private var brightnessController: BrightnessController? = null
+    private var mirrorController: BrightnessMirrorController? = null
+
+    fun init(shouldUseSplitNotificationShade: Boolean) {
+        refreshVisibility(shouldUseSplitNotificationShade)
+    }
+
+    /**
+     * Starts/Stops listening for brightness changing events.
+     * It's fine to call this function even if slider is not visible (which would be the case for
+     * all small screen devices), it will just do nothing in that case
+     */
+    fun setListening(listening: Boolean) {
+        if (listening) {
+            // controller can be null when slider was never shown
+            if (!isListening && brightnessController != null) {
+                brightnessController?.registerCallbacks()
+                isListening = true
+            }
+        } else {
+            brightnessController?.unregisterCallbacks()
+            isListening = false
+        }
+    }
+
+    fun checkRestrictionAndSetEnabled() {
+        brightnessController?.checkRestrictionAndSetEnabled()
+    }
+
+    fun refreshVisibility(shouldUseSplitNotificationShade: Boolean) {
+        if (shouldUseSplitNotificationShade) {
+            showBrightnessSlider()
+        } else {
+            hideBrightnessSlider()
+        }
+    }
+
+    override fun setMirror(controller: BrightnessMirrorController) {
+        mirrorController = controller
+        mirrorController?.let { brightnessController?.setMirror(it) }
+    }
+
+    private fun hideBrightnessSlider() {
+        brightnessController?.hideSlider()
+    }
+
+    private fun showBrightnessSlider() {
+        if (brightnessController == null) {
+            brightnessController = brightnessControllerFactory()
+            mirrorController?.also { brightnessController?.setMirror(it) }
+            brightnessController?.registerCallbacks()
+            isListening = true
+        }
+        brightnessController?.showSlider()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index c5bfe97..613e7f8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -33,23 +33,16 @@
  */
 public class QuickQSPanel extends QSPanel {
 
-    public static final String NUM_QUICK_TILES = "sysui_qqs_count";
     private static final String TAG = "QuickQSPanel";
-    // A default value so that we never return 0.
-    public static final int DEFAULT_MAX_TILES = 6;
+    // A fallback value for max tiles number when setting via Tuner (parseNumTiles)
+    public static final int TUNER_MAX_TILES_FALLBACK = 6;
 
     private boolean mDisabledByPolicy;
     private int mMaxTiles;
 
     public QuickQSPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mMaxTiles = Math.min(DEFAULT_MAX_TILES,
-                getResources().getInteger(R.integer.quick_qs_panel_max_columns));
-    }
-
-    @Override
-    public void setBrightnessView(View view) {
-        // Don't add brightness view
+        mMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_tiles);
     }
 
     @Override
@@ -106,7 +99,7 @@
     }
 
     public void setMaxTiles(int maxTiles) {
-        mMaxTiles = Math.min(maxTiles, DEFAULT_MAX_TILES);
+        mMaxTiles = maxTiles;
     }
 
     @Override
@@ -122,17 +115,18 @@
     }
 
     /**
-     * Parses the String setting into the number of tiles. Defaults to {@code mDefaultMaxTiles}
+     * Parses the String setting into the number of tiles. Defaults to
+     * {@link #TUNER_MAX_TILES_FALLBACK}
      *
      * @param numTilesValue value of the setting to parse
-     * @return parsed value of numTilesValue OR {@code mDefaultMaxTiles} on error
+     * @return parsed value of numTilesValue OR {@link #TUNER_MAX_TILES_FALLBACK} on error
      */
     public static int parseNumTiles(String numTilesValue) {
         try {
             return Integer.parseInt(numTilesValue);
         } catch (NumberFormatException e) {
             // Couldn't read an int from the new setting value. Use default.
-            return DEFAULT_MAX_TILES;
+            return TUNER_MAX_TILES_FALLBACK;
         }
     }
 
@@ -193,7 +187,7 @@
         public boolean updateResources() {
             mCellHeightResId = R.dimen.qs_quick_tile_size;
             boolean b = super.updateResources();
-            mMaxAllowedRows = 2;
+            mMaxAllowedRows = getResources().getInteger(R.integer.quick_qs_panel_max_rows);
             return b;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index fee56b9..92690c7d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -17,6 +17,7 @@
 package com.android.systemui.qs;
 
 import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL;
+import static com.android.systemui.qs.dagger.QSFragmentModule.QQS_FOOTER;
 import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
 
 import com.android.internal.logging.MetricsLogger;
@@ -29,7 +30,8 @@
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.settings.brightness.BrightnessMirrorHandler;
+import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -43,22 +45,32 @@
 
     private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
             newConfig -> {
-                int newMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns);
+                int newMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_tiles);
                 if (newMaxTiles != mView.getNumQuickTiles()) {
                     setMaxTiles(newMaxTiles);
                 }
             };
 
+    // brightness is visible only in split shade
+    private final QuickQSBrightnessController mBrightnessController;
+    private final BrightnessMirrorHandler mBrightnessMirrorHandler;
+    private final FooterActionsController mFooterActionsController;
+
     @Inject
     QuickQSPanelController(QuickQSPanel view, QSTileHost qsTileHost,
             QSCustomizerController qsCustomizerController,
             @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
             @Named(QUICK_QS_PANEL) MediaHost mediaHost,
             MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
-            DumpManager dumpManager, FeatureFlags featureFlags
+            DumpManager dumpManager,
+            QuickQSBrightnessController quickQSBrightnessController,
+            @Named(QQS_FOOTER) FooterActionsController footerActionsController
     ) {
         super(view, qsTileHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
-                uiEventLogger, qsLogger, dumpManager, featureFlags);
+                uiEventLogger, qsLogger, dumpManager);
+        mBrightnessController = quickQSBrightnessController;
+        mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController);
+        mFooterActionsController = footerActionsController;
     }
 
     @Override
@@ -67,18 +79,30 @@
         mMediaHost.setExpansion(0.0f);
         mMediaHost.setShowsOnlyActiveMedia(true);
         mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
+        mBrightnessController.init(mShouldUseSplitNotificationShade);
+        mFooterActionsController.init();
+        mFooterActionsController.refreshVisibility(mShouldUseSplitNotificationShade);
     }
 
     @Override
     protected void onViewAttached() {
         super.onViewAttached();
         mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
+        mBrightnessMirrorHandler.onQsPanelAttached();
     }
 
     @Override
     protected void onViewDetached() {
         super.onViewDetached();
         mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
+        mBrightnessMirrorHandler.onQsPanelDettached();
+    }
+
+    @Override
+    void setListening(boolean listening) {
+        super.setListening(listening);
+        mBrightnessController.setListening(listening);
+        mFooterActionsController.setListening(listening);
     }
 
     public boolean isListening() {
@@ -91,6 +115,18 @@
     }
 
     @Override
+    public void refreshAllTiles() {
+        mBrightnessController.checkRestrictionAndSetEnabled();
+        super.refreshAllTiles();
+    }
+
+    @Override
+    protected void onConfigurationChanged() {
+        mBrightnessController.refreshVisibility(mShouldUseSplitNotificationShade);
+        mFooterActionsController.refreshVisibility(mShouldUseSplitNotificationShade);
+    }
+
+    @Override
     public void setTiles() {
         List<QSTile> tiles = new ArrayList<>();
         for (QSTile tile : mHost.getTiles()) {
@@ -110,4 +146,8 @@
     public int getNumQuickTiles() {
         return mView.getNumQuickTiles();
     }
+
+    public void setBrightnessMirror(BrightnessMirrorController brightnessMirrorController) {
+        mBrightnessMirrorHandler.setController(brightnessMirrorController);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 84b961e..a81d3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -35,8 +35,8 @@
 import androidx.annotation.NonNull;
 
 import com.android.settingslib.Utils;
-import com.android.systemui.BatteryMeterView;
 import com.android.systemui.R;
+import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.qs.QSDetail.Callback;
 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
 import com.android.systemui.statusbar.phone.StatusBarWindowView;
@@ -66,7 +66,7 @@
     // DateView next to clock. Visible on QQS
     private VariableDateView mClockDateView;
     private View mSecurityHeaderView;
-    private View mClockIconsView;
+    private View mStatusIconsView;
     private View mContainer;
 
     private View mQSCarriers;
@@ -120,7 +120,7 @@
 
         mHeaderQsPanel = findViewById(R.id.quick_qs_panel);
         mDatePrivacyView = findViewById(R.id.quick_status_bar_date_privacy);
-        mClockIconsView = findViewById(R.id.quick_qs_status_icons);
+        mStatusIconsView = findViewById(R.id.quick_qs_status_icons);
         mQSCarriers = findViewById(R.id.carrier_group);
         mContainer = findViewById(R.id.qs_container);
         mIconContainer = findViewById(R.id.statusIcons);
@@ -145,8 +145,6 @@
         setSecurityHeaderContainerVisibility(
                 config.orientation == Configuration.ORIENTATION_LANDSCAPE);
 
-        // Don't need to worry about tuner settings for this icon
-        mBatteryRemainingIcon.setIgnoreTunerUpdates(true);
         // QS will always show the estimate, and BatteryMeterView handles the case where
         // it's unavailable or charging
         mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
@@ -231,6 +229,11 @@
 
     void updateResources() {
         Resources resources = mContext.getResources();
+        // status bar is already displayed out of QS in split shade
+        boolean shouldUseSplitShade =
+                resources.getBoolean(R.bool.config_use_split_notification_shade);
+        mStatusIconsView.setVisibility(shouldUseSplitShade ? View.GONE : View.VISIBLE);
+        mDatePrivacyView.setVisibility(shouldUseSplitShade ? View.GONE : View.VISIBLE);
 
         mConfigShowBatteryEstimate = resources.getBoolean(R.bool.config_showBatteryEstimateQSBH);
 
@@ -244,13 +247,13 @@
                 Math.max(qsOffsetHeight, mDatePrivacyView.getMinimumHeight());
         mDatePrivacyView.setLayoutParams(mDatePrivacyView.getLayoutParams());
 
-        mClockIconsView.getLayoutParams().height =
-                Math.max(qsOffsetHeight, mClockIconsView.getMinimumHeight());
-        mClockIconsView.setLayoutParams(mClockIconsView.getLayoutParams());
+        mStatusIconsView.getLayoutParams().height =
+                Math.max(qsOffsetHeight, mStatusIconsView.getMinimumHeight());
+        mStatusIconsView.setLayoutParams(mStatusIconsView.getLayoutParams());
 
         ViewGroup.LayoutParams lp = getLayoutParams();
         if (mQsDisabled) {
-            lp.height = mClockIconsView.getLayoutParams().height;
+            lp.height = mStatusIconsView.getLayoutParams().height;
         } else {
             lp.height = WRAP_CONTENT;
         }
@@ -410,7 +413,7 @@
         if (disabled == mQsDisabled) return;
         mQsDisabled = disabled;
         mHeaderQsPanel.setDisabledByPolicy(disabled);
-        mClockIconsView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+        mStatusIconsView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
         updateResources();
     }
 
@@ -424,7 +427,7 @@
                 StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner(
                         cutout, cornerCutoutPadding, -1);
         mDatePrivacyView.setPadding(padding.first, 0, padding.second, 0);
-        mClockIconsView.setPadding(padding.first, 0, padding.second, 0);
+        mStatusIconsView.setPadding(padding.first, 0, padding.second, 0);
         LinearLayout.LayoutParams datePrivacySeparatorLayoutParams =
                 (LinearLayout.LayoutParams) mDatePrivacySeparator.getLayoutParams();
         LinearLayout.LayoutParams mClockIconsSeparatorLayoutParams =
@@ -489,7 +492,7 @@
 
     private void updateHeadersPadding() {
         setContentMargins(mDatePrivacyView, 0, 0);
-        setContentMargins(mClockIconsView, 0, 0);
+        setContentMargins(mStatusIconsView, 0, 0);
         int paddingLeft = 0;
         int paddingRight = 0;
 
@@ -515,7 +518,7 @@
                 mWaterfallTopInset,
                 paddingRight,
                 0);
-        mClockIconsView.setPadding(paddingLeft,
+        mStatusIconsView.setPadding(paddingLeft,
                 mWaterfallTopInset,
                 paddingRight,
                 0);
@@ -542,7 +545,7 @@
      * @param scrollY the scroll of the QSPanel container
      */
     public void setExpandedScrollAmount(int scrollY) {
-        mClockIconsView.setScrollY(scrollY);
+        mStatusIconsView.setScrollY(scrollY);
         mDatePrivacyView.setScrollY(scrollY);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
index 18d6e64..38428c5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
@@ -25,9 +25,11 @@
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
+import com.android.systemui.battery.BatteryMeterViewController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.privacy.OngoingPrivacyChip;
 import com.android.systemui.privacy.PrivacyChipEvent;
@@ -37,7 +39,6 @@
 import com.android.systemui.privacy.logging.PrivacyLogger;
 import com.android.systemui.qs.carrier.QSCarrierGroupController;
 import com.android.systemui.qs.dagger.QSScope;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusIconContainer;
 import com.android.systemui.statusbar.policy.Clock;
@@ -59,7 +60,7 @@
     private final ActivityStarter mActivityStarter;
     private final UiEventLogger mUiEventLogger;
     private final QSCarrierGroupController mQSCarrierGroupController;
-    private final QuickQSPanelController mHeaderQsPanelController;
+    private final QuickQSPanelController mQuickQSPanelController;
     private final OngoingPrivacyChip mPrivacyChip;
     private final Clock mClockView;
     private final StatusBarIconController mStatusBarIconController;
@@ -70,6 +71,7 @@
     private final PrivacyLogger mPrivacyLogger;
     private final PrivacyDialogController mPrivacyDialogController;
     private final QSExpansionPathInterpolator mQSExpansionPathInterpolator;
+    private final BatteryMeterViewController mBatteryMeterViewController;
     private final FeatureFlags mFeatureFlags;
 
     private final VariableDateViewController mVariableDateViewControllerDateView;
@@ -138,6 +140,7 @@
             SysuiColorExtractor colorExtractor,
             PrivacyDialogController privacyDialogController,
             QSExpansionPathInterpolator qsExpansionPathInterpolator,
+            BatteryMeterViewController batteryMeterViewController,
             FeatureFlags featureFlags,
             VariableDateViewController.Factory variableDateViewControllerFactory) {
         super(view);
@@ -146,10 +149,11 @@
         mUiEventLogger = uiEventLogger;
         mStatusBarIconController = statusBarIconController;
         mDemoModeController = demoModeController;
-        mHeaderQsPanelController = quickQSPanelController;
+        mQuickQSPanelController = quickQSPanelController;
         mPrivacyLogger = privacyLogger;
         mPrivacyDialogController = privacyDialogController;
         mQSExpansionPathInterpolator = qsExpansionPathInterpolator;
+        mBatteryMeterViewController = batteryMeterViewController;
         mFeatureFlags = featureFlags;
 
         mQSCarrierGroupController = qsCarrierGroupControllerBuilder
@@ -178,6 +182,14 @@
         mCameraSlot = getResources().getString(com.android.internal.R.string.status_bar_camera);
         mMicSlot = getResources().getString(com.android.internal.R.string.status_bar_microphone);
         mLocationSlot = getResources().getString(com.android.internal.R.string.status_bar_location);
+
+        // Don't need to worry about tuner settings for this icon
+        mBatteryMeterViewController.ignoreTunerUpdates();
+    }
+
+    @Override
+    protected void onInit() {
+        mBatteryMeterViewController.init();
     }
 
     @Override
@@ -239,12 +251,12 @@
         }
         mListening = listening;
 
-        mHeaderQsPanelController.setListening(listening);
-        if (mHeaderQsPanelController.isListening()) {
-            mHeaderQsPanelController.refreshAllTiles();
+        mQuickQSPanelController.setListening(listening);
+        if (mQuickQSPanelController.isListening()) {
+            mQuickQSPanelController.refreshAllTiles();
         }
 
-        if (mHeaderQsPanelController.switchTileLayout(false)) {
+        if (mQuickQSPanelController.switchTileLayout(false)) {
             mView.updateResources();
         }
 
@@ -300,7 +312,7 @@
     }
 
     public void setContentMargins(int marginStart, int marginEnd) {
-        mHeaderQsPanelController.setContentMargins(marginStart, marginEnd);
+        mQuickQSPanelController.setContentMargins(marginStart, marginEnd);
     }
 
     private static class ClockDemoModeReceiver implements DemoMode {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java
index 67c4d33..fec61d9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java
@@ -40,10 +40,10 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
 import com.android.systemui.util.CarrierConfigTracker;
 
 import java.util.function.Consumer;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index 7518b20..1a6d490 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -33,9 +33,9 @@
 
 import com.android.systemui.R;
 import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.qs.QSDetailClipper;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 
 /**
  * Allows full-screen customization of QS, through show() and hide().
@@ -54,7 +54,7 @@
     private boolean isShown;
     private final RecyclerView mRecyclerView;
     private boolean mCustomizing;
-    private NotificationsQuickSettingsContainer mNotifQsContainer;
+    private QSContainerController mQsContainerController;
     private QS mQs;
     private int mX;
     private int mY;
@@ -84,8 +84,8 @@
 
     void updateResources() {
         LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams();
-        lp.height = mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.quick_qs_offset_height);
+        lp.height = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.qs_header_system_icons_area_height);
         mTransparentView.setLayoutParams(lp);
     }
 
@@ -103,8 +103,8 @@
         lightBarController.setQsCustomizing(mIsShowingNavBackdrop && isShown);
     }
 
-    public void setContainer(NotificationsQuickSettingsContainer notificationsQsContainer) {
-        mNotifQsContainer = notificationsQsContainer;
+    public void setContainerController(QSContainerController controller) {
+        mQsContainerController = controller;
     }
 
     public void setQs(QS qs) {
@@ -123,8 +123,8 @@
             mOpening = true;
             setVisibility(View.VISIBLE);
             mClipper.animateCircularClip(mX, mY, true, new ExpandAnimatorListener(tileAdapter));
-            mNotifQsContainer.setCustomizerAnimating(true);
-            mNotifQsContainer.setCustomizerShowing(true);
+            mQsContainerController.setCustomizerAnimating(true);
+            mQsContainerController.setCustomizerShowing(true);
         }
     }
 
@@ -136,8 +136,8 @@
             mClipper.showBackground();
             isShown = true;
             setCustomizing(true);
-            mNotifQsContainer.setCustomizerAnimating(false);
-            mNotifQsContainer.setCustomizerShowing(true);
+            mQsContainerController.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerShowing(true);
         }
     }
 
@@ -154,8 +154,8 @@
             } else {
                 setVisibility(View.GONE);
             }
-            mNotifQsContainer.setCustomizerAnimating(animate);
-            mNotifQsContainer.setCustomizerShowing(false);
+            mQsContainerController.setCustomizerAnimating(animate);
+            mQsContainerController.setCustomizerShowing(false);
         }
     }
 
@@ -193,7 +193,7 @@
                 setCustomizing(true);
             }
             mOpening = false;
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
             mRecyclerView.setAdapter(mTileAdapter);
         }
 
@@ -201,7 +201,7 @@
         public void onAnimationCancel(Animator animation) {
             mOpening = false;
             mQs.notifyCustomizeChanged();
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
         }
     }
 
@@ -211,7 +211,7 @@
             if (!isShown) {
                 setVisibility(View.GONE);
             }
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
         }
 
         @Override
@@ -219,7 +219,7 @@
             if (!isShown) {
                 setVisibility(View.GONE);
             }
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
index 49d18e6..618a429 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
@@ -35,13 +35,13 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSEditEvent;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -233,8 +233,8 @@
     }
 
     /** */
-    public void setContainer(NotificationsQuickSettingsContainer container) {
-        mView.setContainer(container);
+    public void setContainerController(QSContainerController controller) {
+        mView.setContainerController(controller);
     }
 
     public boolean isShown() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index f2832b3..993bbd0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -34,6 +34,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTile.State;
 import com.android.systemui.qs.QSTileHost;
@@ -41,7 +42,6 @@
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.util.leak.GarbageMonitor;
 
 import java.util.ArrayList;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlagsModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlagsModule.java
index 6fa44eb..103ac65 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlagsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlagsModule.java
@@ -20,7 +20,7 @@
 import android.hardware.display.ColorDisplayManager;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.util.settings.GlobalSettings;
 
 import javax.inject.Named;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
index 4d63349..b11420a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
@@ -23,8 +23,13 @@
 import android.view.View;
 
 import com.android.systemui.R;
+import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.FooterActionsController;
+import com.android.systemui.qs.FooterActionsController.ExpansionState;
+import com.android.systemui.qs.FooterActionsControllerBuilder;
+import com.android.systemui.qs.FooterActionsView;
 import com.android.systemui.qs.QSContainerImpl;
 import com.android.systemui.qs.QSFooter;
 import com.android.systemui.qs.QSFooterView;
@@ -33,9 +38,7 @@
 import com.android.systemui.qs.QSPanel;
 import com.android.systemui.qs.QuickQSPanel;
 import com.android.systemui.qs.QuickStatusBarHeader;
-import com.android.systemui.qs.carrier.QSCarrierGroupController;
 import com.android.systemui.qs.customize.QSCustomizer;
-import com.android.systemui.statusbar.phone.MultiUserSwitch;
 
 import javax.inject.Named;
 
@@ -49,6 +52,8 @@
 @Module
 public interface QSFragmentModule {
     String QS_SECURITY_FOOTER_VIEW = "qs_security_footer";
+    String QQS_FOOTER = "qqs_footer";
+    String QS_FOOTER = "qs_footer";
     String QS_USING_MEDIA_PLAYER = "qs_using_media_player";
 
     /**
@@ -76,12 +81,6 @@
 
     /** */
     @Provides
-    static MultiUserSwitch providesMultiUserSWitch(QSFooterView qsFooterView) {
-        return qsFooterView.findViewById(R.id.multi_user_switch);
-    }
-
-    /** */
-    @Provides
     static QSPanel provideQSPanel(@RootView View view) {
         return view.findViewById(R.id.quick_settings_panel);
     }
@@ -110,12 +109,56 @@
 
     /** */
     @Provides
+    static BatteryMeterView providesBatteryMeterView(QuickStatusBarHeader quickStatusBarHeader) {
+        return quickStatusBarHeader.findViewById(R.id.batteryRemainingIcon);
+    }
+
+    /** */
+    @Provides
     static QSFooterView providesQSFooterView(@RootView View view) {
         return view.findViewById(R.id.qs_footer);
     }
 
     /** */
     @Provides
+    @Named(QS_FOOTER)
+    static FooterActionsView providesQSFooterActionsView(@RootView View view) {
+        return view.findViewById(R.id.qs_footer_actions);
+    }
+
+    /** */
+    @Provides
+    @Named(QQS_FOOTER)
+    static FooterActionsView providesQQSFooterActionsView(@RootView View view) {
+        return view.findViewById(R.id.qqs_footer_actions);
+    }
+
+    /** */
+    @Provides
+    @Named(QQS_FOOTER)
+    static FooterActionsController providesQQSFooterActionsController(
+            FooterActionsControllerBuilder footerActionsControllerBuilder,
+            @Named(QQS_FOOTER) FooterActionsView qqsFooterActionsView) {
+        return footerActionsControllerBuilder
+                .withView(qqsFooterActionsView)
+                .withButtonsVisibleWhen(ExpansionState.COLLAPSED)
+                .build();
+    }
+
+    /** */
+    @Provides
+    @Named(QS_FOOTER)
+    static FooterActionsController providesQSFooterActionsController(
+            FooterActionsControllerBuilder footerActionsControllerBuilder,
+            @Named(QS_FOOTER) FooterActionsView qsFooterActionsView) {
+        return footerActionsControllerBuilder
+                .withView(qsFooterActionsView)
+                .withButtonsVisibleWhen(ExpansionState.EXPANDED)
+                .build();
+    }
+
+    /** */
+    @Provides
     @QSScope
     static QSFooter providesQSFooter(QSFooterViewController qsFooterViewController) {
         qsFooterViewController.init();
@@ -146,9 +189,4 @@
     static boolean providesQSUsingMediaPlayer(Context context) {
         return useQsMediaPlayer(context);
     }
-
-    /** */
-    @Binds
-    QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
-            QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl);
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index 04f089d..9de6ceb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -51,13 +51,13 @@
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.CastController.CastDevice;
 import com.android.systemui.statusbar.policy.HotspotController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
 
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
index 8e886e8..35dadd4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
@@ -56,11 +56,11 @@
 import com.android.systemui.qs.SignalTileView;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
 
 import javax.inject.Inject;
 
@@ -275,7 +275,7 @@
                 return;
             }
             mInfo.dataSubscriptionName = mController.getMobileDataNetworkName();
-            mInfo.dataContentDescription = indicators.description != null
+            mInfo.dataContentDescription = indicators.qsDescription != null
                     ? indicators.typeContentDescriptionHtml : null;
             mInfo.activityIn = indicators.activityIn;
             mInfo.activityOut = indicators.activityOut;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index cc9e748..98d0a72 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -52,13 +52,13 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.AccessPointController;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
-import com.android.systemui.statusbar.policy.WifiIcons;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.AccessPointController;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
+import com.android.systemui.statusbar.connectivity.WifiIcons;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -279,9 +279,9 @@
                 // Not data sim, don't display.
                 return;
             }
-            mCellularInfo.mDataSubscriptionName = indicators.description == null
-                    ? mController.getMobileDataNetworkName() : indicators.description;
-            mCellularInfo.mDataContentDescription = indicators.description != null
+            mCellularInfo.mDataSubscriptionName = indicators.qsDescription == null
+                    ? mController.getMobileDataNetworkName() : indicators.qsDescription;
+            mCellularInfo.mDataContentDescription = indicators.qsDescription != null
                     ? indicators.typeContentDescriptionHtml : null;
             mCellularInfo.mMobileSignalIconId = indicators.qsIcon.icon;
             mCellularInfo.mQsTypeIcon = indicators.qsType;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index 82b6c0c..d9919bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -131,22 +131,16 @@
                 Intent intent = new Intent(mContext, WalletActivity.class)
                         .setAction(Intent.ACTION_VIEW)
                         .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
-                if (mKeyguardStateController.isUnlocked()) {
-                    mActivityStarter.startActivity(intent, true /* dismissShade */,
-                            animationController);
-                } else {
-                    mHost.collapsePanels();
-                    // Do not use ActivityStarter here because the WalletActivity is required to be
-                    // started without prompting keyguard when the device is locked.
-                    mContext.startActivity(intent);
-                }
+                mActivityStarter.startActivity(intent, true /* dismissShade */,
+                        animationController, true /* showOverLockscreenWhenLocked */);
             } else {
-                if (mController.getWalletClient().createWalletIntent() == null) {
+                Intent intent = mController.getWalletClient().createWalletIntent();
+                if (intent == null) {
                     Log.w(TAG, "Could not get intent of the wallet app.");
                     return;
                 }
                 mActivityStarter.postStartActivityDismissingKeyguard(
-                        mController.getWalletClient().createWalletIntent(),
+                        intent,
                         /* delay= */ 0,
                         animationController);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
index 4e936b8..0bbb5bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -16,19 +16,12 @@
 
 package com.android.systemui.qs.tiles;
 
-import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
-
-import android.Manifest;
-import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.hardware.SensorPrivacyManager;
 import android.os.Handler;
 import android.os.Looper;
 import android.provider.Settings;
-import android.provider.Settings.Secure;
 import android.service.quicksettings.Tile;
 import android.view.View;
 import android.widget.Switch;
@@ -45,25 +38,18 @@
 import com.android.systemui.plugins.qs.QSTile.BooleanState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.SecureSetting;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.RotationLockController;
 import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback;
-import com.android.systemui.util.settings.SecureSettings;
 
 import javax.inject.Inject;
 
 /** Quick settings tile: Rotation **/
-public class RotationLockTile extends QSTileImpl<BooleanState> implements
-        BatteryController.BatteryStateChangeCallback {
+public class RotationLockTile extends QSTileImpl<BooleanState> {
 
     private final Icon mIcon = ResourceIcon.get(com.android.internal.R.drawable.ic_qs_auto_rotate);
     private final RotationLockController mController;
-    private final SensorPrivacyManager mPrivacyManager;
-    private final BatteryController mBatteryController;
-    private final SecureSetting mSetting;
 
     @Inject
     public RotationLockTile(
@@ -75,38 +61,12 @@
             StatusBarStateController statusBarStateController,
             ActivityStarter activityStarter,
             QSLogger qsLogger,
-            RotationLockController rotationLockController,
-            SensorPrivacyManager privacyManager,
-            BatteryController batteryController,
-            SecureSettings secureSettings
+            RotationLockController rotationLockController
     ) {
         super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
         mController = rotationLockController;
         mController.observe(this, mCallback);
-        mPrivacyManager = privacyManager;
-        mBatteryController = batteryController;
-        mPrivacyManager
-                .addSensorPrivacyListener(CAMERA, (sensor, enabled) -> refreshState());
-        int currentUser = host.getUserContext().getUserId();
-        mSetting = new SecureSetting(
-                secureSettings,
-                mHandler,
-                Secure.CAMERA_AUTOROTATE,
-                currentUser
-        ) {
-            @Override
-            protected void handleValueChanged(int value, boolean observedChange) {
-                // mHandler is the background handler so calling this is OK
-                handleRefreshState(value);
-            }
-        };
-        mBatteryController.observe(getLifecycle(), this);
-    }
-
-    @Override
-    public void onPowerSaveChanged(boolean isPowerSave) {
-        refreshState();
     }
 
     @Override
@@ -135,33 +95,14 @@
     protected void handleUpdateState(BooleanState state, Object arg) {
         final boolean rotationLocked = mController.isRotationLocked();
 
-        final boolean powerSave = mBatteryController.isPowerSave();
-        final boolean cameraLocked = mPrivacyManager.isSensorPrivacyEnabled(
-                SensorPrivacyManager.Sensors.CAMERA);
-        final boolean cameraRotation =
-                !powerSave && !cameraLocked && hasSufficientPermission(mContext)
-                        && mController.isCameraRotationEnabled();
         state.value = !rotationLocked;
         state.label = mContext.getString(R.string.quick_settings_rotation_unlocked_label);
         state.icon = mIcon;
         state.contentDescription = getAccessibilityString(rotationLocked);
-        if (!rotationLocked && cameraRotation) {
-            state.secondaryLabel = mContext.getResources().getString(
-                    R.string.rotation_lock_camera_rotation_on);
-        } else {
-            state.secondaryLabel = "";
-        }
-
         state.expandedAccessibilityClassName = Switch.class.getName();
         state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
     }
 
-    @Override
-    protected void handleUserSwitch(int newUserId) {
-        mSetting.setUserId(newUserId);
-        handleRefreshState(mSetting.getValue());
-    }
-
     public static boolean isCurrentOrientationLockPortrait(RotationLockController controller,
             Resources resources) {
         int lockOrientation = controller.getRotationLockOrientation();
@@ -199,11 +140,4 @@
             refreshState(rotationLocked);
         }
     };
-
-    private boolean hasSufficientPermission(Context context) {
-        final PackageManager packageManager = context.getPackageManager();
-        final String rotationPackage = packageManager.getRotationResolverPackageName();
-        return rotationPackage != null && packageManager.checkPermission(
-                Manifest.permission.CAMERA, rotationPackage) == PackageManager.PERMISSION_GRANTED;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 04437ea..821bd51 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -39,6 +39,8 @@
 import com.android.systemui.qs.QSUserSwitcherEvent;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 
+import java.util.function.Consumer;
+
 import javax.inject.Inject;
 
 /**
@@ -75,6 +77,7 @@
         private View mCurrentUserView;
         private final UiEventLogger mUiEventLogger;
         private final FalsingManager mFalsingManager;
+        private Consumer<UserSwitcherController.UserRecord> mClickCallback;
 
         @Inject
         public Adapter(Context context, UserSwitcherController controller,
@@ -92,6 +95,10 @@
             return createUserDetailItemView(convertView, parent, item);
         }
 
+        public void injectCallback(Consumer<UserSwitcherController.UserRecord> clickCallback) {
+            mClickCallback = clickCallback;
+        }
+
         public UserDetailItemView createUserDetailItemView(View convertView, ViewGroup parent,
                 UserSwitcherController.UserRecord item) {
             UserDetailItemView v = UserDetailItemView.convertOrInflate(
@@ -167,6 +174,13 @@
                 }
                 onUserListItemClicked(tag);
             }
+            if (mClickCallback != null) {
+                mClickCallback.accept(tag);
+            }
+        }
+
+        public void linkToViewGroup(ViewGroup viewGroup) {
+            PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
index 41a3020..e6e7e21 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
@@ -52,11 +52,11 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSIconViewImpl;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.AccessPointController;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
-import com.android.systemui.statusbar.policy.WifiIcons;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.AccessPointController;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
+import com.android.systemui.statusbar.connectivity.WifiIcons;
 import com.android.wifitrackerlib.WifiEntry;
 
 import java.util.List;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index dae357e..11430d9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -36,7 +36,6 @@
 import android.telephony.TelephonyManager;
 import android.text.Html;
 import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
@@ -64,7 +63,6 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
-import com.android.systemui.accessibility.floatingmenu.AnnotationLinkSpan;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -112,8 +110,6 @@
     private LinearLayout mTurnWifiOnLayout;
     private LinearLayout mEthernetLayout;
     private TextView mWifiToggleTitleText;
-    private LinearLayout mWifiScanNotifyLayout;
-    private TextView mWifiScanNotifyText;
     private LinearLayout mSeeAllLayout;
     private RecyclerView mWifiRecyclerView;
     private ImageView mConnectedWifiIcon;
@@ -210,7 +206,7 @@
         window.setContentView(mDialogView);
         //Only fix the width for large screen or tablet.
         window.setLayout(mContext.getResources().getDimensionPixelSize(
-                R.dimen.internet_dialog_list_max_width), ViewGroup.LayoutParams.WRAP_CONTENT);
+                R.dimen.large_dialog_width), ViewGroup.LayoutParams.WRAP_CONTENT);
         window.setWindowAnimations(R.style.Animation_InternetDialog);
         window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
         window.addFlags(FLAG_LAYOUT_NO_LIMITS);
@@ -224,8 +220,6 @@
         mMobileNetworkLayout = mDialogView.requireViewById(R.id.mobile_network_layout);
         mTurnWifiOnLayout = mDialogView.requireViewById(R.id.turn_on_wifi_layout);
         mWifiToggleTitleText = mDialogView.requireViewById(R.id.wifi_toggle_title);
-        mWifiScanNotifyLayout = mDialogView.requireViewById(R.id.wifi_scan_notify_layout);
-        mWifiScanNotifyText = mDialogView.requireViewById(R.id.wifi_scan_notify_text);
         mConnectedWifListLayout = mDialogView.requireViewById(R.id.wifi_connected_layout);
         mConnectedWifiIcon = mDialogView.requireViewById(R.id.wifi_connected_icon);
         mConnectedWifiTitleText = mDialogView.requireViewById(R.id.wifi_connected_title);
@@ -319,10 +313,8 @@
         showProgressBar();
         final boolean isDeviceLocked = mInternetDialogController.isDeviceLocked();
         final boolean isWifiEnabled = mWifiManager.isWifiEnabled();
-        final boolean isWifiScanEnabled = mWifiManager.isScanAlwaysAvailable();
         updateWifiToggle(isWifiEnabled, isDeviceLocked);
         updateConnectedWifi(isWifiEnabled, isDeviceLocked);
-        updateWifiScanNotify(isWifiEnabled, isWifiScanEnabled, isDeviceLocked);
 
         final int visibility = (isDeviceLocked || !isWifiEnabled || mWifiEntriesCount <= 0)
                 ? View.GONE : View.VISIBLE;
@@ -419,24 +411,6 @@
                 mContext.getColor(R.color.connected_network_primary_color));
     }
 
-    @MainThread
-    private void updateWifiScanNotify(boolean isWifiEnabled, boolean isWifiScanEnabled,
-            boolean isDeviceLocked) {
-        if (isWifiEnabled || !isWifiScanEnabled || isDeviceLocked) {
-            mWifiScanNotifyLayout.setVisibility(View.GONE);
-            return;
-        }
-        if (TextUtils.isEmpty(mWifiScanNotifyText.getText())) {
-            final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
-                    AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
-                    v -> mInternetDialogController.launchWifiScanningSetting());
-            mWifiScanNotifyText.setText(AnnotationLinkSpan.linkify(
-                    getContext().getText(R.string.wifi_scan_notify_message), linkInfo));
-            mWifiScanNotifyText.setMovementMethod(LinkMovementMethod.getInstance());
-        }
-        mWifiScanNotifyLayout.setVisibility(View.VISIBLE);
-    }
-
     void onClickConnectedWifi() {
         if (mConnectedWifiEntry == null) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 276c0be..b9cd08e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -75,9 +75,9 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.AccessPointController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.AccessPointController;
 import com.android.systemui.toast.SystemUIToast;
 import com.android.systemui.toast.ToastFactory;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -103,8 +103,6 @@
     private static final String TAG = "InternetDialogController";
     private static final String ACTION_NETWORK_PROVIDER_SETTINGS =
             "android.settings.NETWORK_PROVIDER_SETTINGS";
-    private static final String ACTION_WIFI_SCANNING_SETTINGS =
-            "android.settings.WIFI_SCANNING_SETTINGS";
     private static final String EXTRA_CHOSEN_WIFI_ENTRY_KEY = "key_chosen_wifientry_key";
     public static final Drawable EMPTY_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
     public static final int NO_CELL_DATA_TYPE_ICON = 0;
@@ -605,13 +603,6 @@
         }
     }
 
-    void launchWifiScanningSetting() {
-        mCallback.dismissDialog();
-        final Intent intent = new Intent(ACTION_WIFI_SCANNING_SETTINGS);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
-    }
-
     void connectCarrierNetwork() {
         final MergedCarrierEntry mergedCarrierEntry =
                 mAccessPointController.getMergedCarrierEntry();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserDialog.kt
new file mode 100644
index 0000000..01afa56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserDialog.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.user
+
+import android.content.Context
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.view.WindowManager
+import com.android.systemui.qs.PseudoGridView
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.R
+
+/**
+ * Dialog for switching users or creating new ones.
+ */
+class UserDialog(
+    context: Context
+) : SystemUIDialog(context) {
+
+    // create() is no-op after creation
+    private lateinit var _doneButton: View
+    /**
+     * Button with text "Done" in dialog.
+     */
+    val doneButton: View
+        get() {
+            create()
+            return _doneButton
+        }
+
+    private lateinit var _settingsButton: View
+    /**
+     * Button with text "User Settings" in dialog.
+     */
+    val settingsButton: View
+        get() {
+            create()
+            return _settingsButton
+        }
+
+    private lateinit var _grid: PseudoGridView
+    /**
+     * Grid to populate with user avatar from adapter
+     */
+    val grid: ViewGroup
+        get() {
+            create()
+            return _grid
+        }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        window?.apply {
+            setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
+            attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
+            attributes.receiveInsetsIgnoringZOrder = true
+            setLayout(
+                    context.resources.getDimensionPixelSize(R.dimen.notification_panel_width),
+                    ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+            setGravity(Gravity.CENTER)
+        }
+        setContentView(R.layout.qs_user_dialog_content)
+
+        _doneButton = requireViewById(R.id.done)
+        _settingsButton = requireViewById(R.id.settings)
+        _grid = requireViewById(R.id.grid)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
new file mode 100644
index 0000000..bae7996
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.user
+
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.tiles.UserDetailView
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Controller for [UserDialog].
+ */
+@SysUISingleton
+class UserSwitchDialogController @VisibleForTesting constructor(
+    private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
+    private val activityStarter: ActivityStarter,
+    private val falsingManager: FalsingManager,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+    private val dialogFactory: (Context) -> UserDialog
+) {
+
+    @Inject
+    constructor(
+        userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
+        activityStarter: ActivityStarter,
+        falsingManager: FalsingManager,
+        dialogLaunchAnimator: DialogLaunchAnimator
+    ) : this(
+        userDetailViewAdapterProvider,
+        activityStarter,
+        falsingManager,
+        dialogLaunchAnimator,
+        { UserDialog(it) }
+    )
+
+    companion object {
+        private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS)
+    }
+
+    /**
+     * Show a [UserDialog].
+     *
+     * Populate the dialog with information from and adapter obtained from
+     * [userDetailViewAdapterProvider] and show it as launched from [view].
+     */
+    fun showDialog(view: View) {
+        with(dialogFactory(view.context)) {
+            setShowForAllUsers(true)
+            setCanceledOnTouchOutside(true)
+            create() // Needs to be called before we can retrieve views
+
+            settingsButton.setOnClickListener {
+                if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                    dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
+                    activityStarter.postStartActivityDismissingKeyguard(
+                        USER_SETTINGS_INTENT,
+                        0
+                    )
+                }
+                dismiss()
+            }
+            doneButton.setOnClickListener { dismiss() }
+
+            val adapter = userDetailViewAdapterProvider.get()
+            adapter.injectCallback {
+                dismiss()
+            }
+            adapter.linkToViewGroup(grid)
+
+            dialogLaunchAnimator.showFromView(this, view)
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
index bbeff6e..77c61a4 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
@@ -42,7 +42,7 @@
 
     private final static String TAG = "OverviewProxyRecentsImpl";
     @Nullable
-    private final Lazy<StatusBar> mStatusBarLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
 
     private Context mContext;
     private Handler mHandler;
@@ -51,8 +51,8 @@
 
     @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
     @Inject
-    public OverviewProxyRecentsImpl(Optional<Lazy<StatusBar>> statusBarLazy) {
-        mStatusBarLazy = statusBarLazy.orElse(null);
+    public OverviewProxyRecentsImpl(Lazy<Optional<StatusBar>> statusBarOptionalLazy) {
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
     }
 
     @Override
@@ -109,8 +109,9 @@
                 }
             };
             // Preload only if device for current user is unlocked
-            if (mStatusBarLazy != null && mStatusBarLazy.get().isKeyguardShowing()) {
-                mStatusBarLazy.get().executeRunnableDismissingKeyguard(() -> {
+            final Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
+            if (statusBarOptional.map(StatusBar::isKeyguardShowing).orElse(false)) {
+                statusBarOptional.get().executeRunnableDismissingKeyguard(() -> {
                         // Flush trustmanager before checking device locked per user
                         mTrustManager.reportKeyguardShowingChanged();
                         mHandler.post(toggleRecents);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index cb0c411..721a6af 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -34,6 +34,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_WINDOW_CORNER_RADIUS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
@@ -77,6 +78,8 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationBar;
 import com.android.systemui.navigationbar.NavigationBarController;
@@ -92,6 +95,7 @@
 import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
 import com.android.systemui.statusbar.policy.CallbackController;
@@ -109,6 +113,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.function.BiConsumer;
+import java.util.function.Supplier;
 
 import javax.inject.Inject;
 
@@ -134,7 +139,7 @@
 
     private final Context mContext;
     private final Optional<Pip> mPipOptional;
-    private final Optional<Lazy<StatusBar>> mStatusBarOptionalLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private final Optional<LegacySplitScreen> mLegacySplitScreenOptional;
     private final Optional<SplitScreen> mSplitScreenOptional;
     private SysUiState mSysUiState;
@@ -171,55 +176,33 @@
     public ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() {
         @Override
         public void startScreenPinning(int taskId) {
-            if (!verifyCaller("startScreenPinning")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> {
-                    mStatusBarOptionalLazy.ifPresent(
-                            statusBarLazy -> statusBarLazy.get().showScreenPinningRequest(taskId,
-                                    false /* allowCancel */));
-                });
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("startScreenPinning", () ->
+                    mStatusBarOptionalLazy.get().ifPresent(
+                            statusBar -> statusBar.showScreenPinningRequest(taskId,
+                                    false /* allowCancel */)));
         }
 
         @Override
         public void stopScreenPinning() {
-            if (!verifyCaller("stopScreenPinning")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> {
-                    try {
-                        ActivityTaskManager.getService().stopSystemLockTaskMode();
-                    } catch (RemoteException e) {
-                        Log.e(TAG_OPS, "Failed to stop screen pinning");
-                    }
-                });
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("stopScreenPinning", () -> {
+                try {
+                    ActivityTaskManager.getService().stopSystemLockTaskMode();
+                } catch (RemoteException e) {
+                    Log.e(TAG_OPS, "Failed to stop screen pinning");
+                }
+            });
         }
 
         // TODO: change the method signature to use (boolean inputFocusTransferStarted)
         @Override
         public void onStatusBarMotionEvent(MotionEvent event) {
-            if (!verifyCaller("onStatusBarMotionEvent")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
+            verifyCallerAndClearCallingIdentity("onStatusBarMotionEvent", () -> {
                 // TODO move this logic to message queue
-                mStatusBarOptionalLazy.ifPresent(statusBarLazy -> {
-                    StatusBar statusBar = statusBarLazy.get();
+                mStatusBarOptionalLazy.get().ifPresent(statusBar -> {
                     if (event.getActionMasked() == ACTION_DOWN) {
                         statusBar.getPanelController().startExpandLatencyTracking();
                     }
-                    mHandler.post(()-> {
+                    mHandler.post(() -> {
                         int action = event.getActionMasked();
                         if (action == ACTION_DOWN) {
                             mInputFocusTransferStarted = true;
@@ -231,50 +214,44 @@
                         }
                         if (action == ACTION_UP || action == ACTION_CANCEL) {
                             mInputFocusTransferStarted = false;
+                            float velocity = (event.getY() - mInputFocusTransferStartY)
+                                    / (event.getEventTime() - mInputFocusTransferStartMillis);
                             statusBar.onInputFocusTransfer(mInputFocusTransferStarted,
                                     action == ACTION_CANCEL,
-                                    (event.getY() - mInputFocusTransferStartY)
-                                    / (event.getEventTime() - mInputFocusTransferStartMillis));
+                                    velocity);
                         }
                         event.recycle();
                     });
                 });
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            });
         }
 
         @Override
         public void onBackPressed() throws RemoteException {
-            if (!verifyCaller("onBackPressed")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> {
-                    sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
-                    sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
+            verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> {
+                sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
+                sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
 
-                    notifyBackAction(true, -1, -1, true, false);
-                });
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+                notifyBackAction(true, -1, -1, true, false);
+            });
         }
 
         @Override
         public void setHomeRotationEnabled(boolean enabled) {
-            if (!verifyCaller("setHomeRotationEnabled")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> {
-                    mHandler.post(() -> notifyHomeRotationEnabled(enabled));
-                });
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("setHomeRotationEnabled", () ->
+                    mHandler.post(() -> notifyHomeRotationEnabled(enabled)));
+        }
+
+        @Override
+        public void notifyTaskbarStatus(boolean visible, boolean stashed) {
+            verifyCallerAndClearCallingIdentityPostMain("notifyTaskbarStatus", () ->
+                    onTaskbarStatusUpdated(visible, stashed));
+        }
+
+        @Override
+        public void notifyTaskbarAutohideSuspend(boolean suspend) {
+            verifyCallerAndClearCallingIdentityPostMain("notifyTaskbarAutohideSuspend", () ->
+                    onTaskbarAutohideSuspend(suspend));
         }
 
         private boolean sendEvent(int action, int code) {
@@ -291,124 +268,74 @@
 
         @Override
         public void onOverviewShown(boolean fromHome) {
-            if (!verifyCaller("onOverviewShown")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> {
-                    for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
-                        mConnectionCallbacks.get(i).onOverviewShown(fromHome);
-                    }
-                });
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("onOverviewShown", () -> {
+                for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
+                    mConnectionCallbacks.get(i).onOverviewShown(fromHome);
+                }
+            });
         }
 
         @Override
         public Rect getNonMinimizedSplitScreenSecondaryBounds() {
-            if (!verifyCaller("getNonMinimizedSplitScreenSecondaryBounds")) {
-                return null;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                return mLegacySplitScreenOptional.map(splitScreen ->
-                        splitScreen.getDividerView().getNonMinimizedSplitScreenSecondaryBounds())
-                        .orElse(null);
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            return verifyCallerAndClearCallingIdentity(
+                    "getNonMinimizedSplitScreenSecondaryBounds",
+                    () -> mLegacySplitScreenOptional.map(splitScreen ->
+                            splitScreen
+                                    .getDividerView()
+                                    .getNonMinimizedSplitScreenSecondaryBounds())
+                            .orElse(null)
+            );
         }
 
         @Override
         public void setNavBarButtonAlpha(float alpha, boolean animate) {
-            if (!verifyCaller("setNavBarButtonAlpha")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mNavBarButtonAlpha = alpha;
-                mHandler.post(() -> notifyNavBarButtonAlphaChanged(alpha, animate));
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("setNavBarButtonAlpha", () ->
+                    notifyNavBarButtonAlphaChanged(alpha, animate));
         }
 
         @Override
         public void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {
-            if (!verifyCaller("onAssistantProgress")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> notifyAssistantProgress(progress));
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("onAssistantProgress", () ->
+                    notifyAssistantProgress(progress));
         }
 
         @Override
         public void onAssistantGestureCompletion(float velocity) {
-            if (!verifyCaller("onAssistantGestureCompletion")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> notifyAssistantGestureCompletion(velocity));
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("onAssistantGestureCompletion", () ->
+                    notifyAssistantGestureCompletion(velocity));
         }
 
         @Override
         public void startAssistant(Bundle bundle) {
-            if (!verifyCaller("startAssistant")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> notifyStartAssistant(bundle));
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("startAssistant", () ->
+                    notifyStartAssistant(bundle));
         }
 
         @Override
         public void notifyAccessibilityButtonClicked(int displayId) {
-            if (!verifyCaller("notifyAccessibilityButtonClicked")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                AccessibilityManager.getInstance(mContext)
-                        .notifyAccessibilityButtonClicked(displayId);
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentity("notifyAccessibilityButtonClicked", () ->
+                    AccessibilityManager.getInstance(mContext)
+                            .notifyAccessibilityButtonClicked(displayId));
         }
 
         @Override
         public void notifyAccessibilityButtonLongClicked() {
-            if (!verifyCaller("notifyAccessibilityButtonLongClicked")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                final Intent intent =
-                        new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
-                final String chooserClassName = AccessibilityButtonChooserActivity.class.getName();
-                intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
-                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentity("notifyAccessibilityButtonLongClicked",
+                    () -> {
+                        final Intent intent =
+                                new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
+                        final String chooserClassName = AccessibilityButtonChooserActivity
+                                .class.getName();
+                        intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
+                        intent.addFlags(
+                                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                    });
         }
 
         @Override
         public void handleImageAsScreenshot(Bitmap screenImage, Rect locationInScreen,
-                Insets visibleInsets, int taskId) {
+                                            Insets visibleInsets, int taskId) {
             // Deprecated
         }
 
@@ -420,43 +347,22 @@
 
         @Override
         public void notifySwipeToHomeFinished() {
-            if (!verifyCaller("notifySwipeToHomeFinished")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mPipOptional.ifPresent(
-                        pip -> pip.setPinnedStackAnimationType(
-                                PipAnimationController.ANIM_TYPE_ALPHA));
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentity("notifySwipeToHomeFinished", () ->
+                    mPipOptional.ifPresent(
+                            pip -> pip.setPinnedStackAnimationType(
+                                    PipAnimationController.ANIM_TYPE_ALPHA)));
         }
 
         @Override
         public void notifySwipeUpGestureStarted() {
-            if (!verifyCaller("notifySwipeUpGestureStarted")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> notifySwipeUpGestureStartedInternal());
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () ->
+                    notifySwipeUpGestureStartedInternal());
         }
 
         @Override
         public void notifyPrioritizedRotation(@Surface.Rotation int rotation) {
-            if (!verifyCaller("notifyPrioritizedRotation")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mHandler.post(() -> notifyPrioritizedRotationInternal(rotation));
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentityPostMain("notifyPrioritizedRotation", () ->
+                    notifyPrioritizedRotationInternal(rotation));
         }
 
         @Override
@@ -476,15 +382,8 @@
 
         @Override
         public void expandNotificationPanel() {
-            if (!verifyCaller("expandNotificationPanel")) {
-                return;
-            }
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mCommandQueue.handleSystemKey(KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
+            verifyCallerAndClearCallingIdentity("expandNotificationPanel",
+                    () -> mCommandQueue.handleSystemKey(KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN));
         }
 
         private boolean verifyCaller(String reason) {
@@ -496,6 +395,29 @@
             }
             return true;
         }
+
+        private <T> T verifyCallerAndClearCallingIdentity(String reason, Supplier<T> supplier) {
+            if (!verifyCaller(reason)) {
+                return null;
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                return supplier.get();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        private void verifyCallerAndClearCallingIdentity(String reason, Runnable runnable) {
+            verifyCallerAndClearCallingIdentity(reason, () -> {
+                runnable.run();
+                return null;
+            });
+        }
+
+        private void verifyCallerAndClearCallingIdentityPostMain(String reason, Runnable runnable) {
+            verifyCallerAndClearCallingIdentity(reason, () -> mHandler.post(runnable));
+        }
     };
 
     private final Runnable mDeferredConnectionCallback = () -> {
@@ -613,12 +535,14 @@
             Optional<Pip> pipOptional,
             Optional<LegacySplitScreen> legacySplitScreenOptional,
             Optional<SplitScreen> splitScreenOptional,
-            Optional<Lazy<StatusBar>> statusBarOptionalLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             Optional<OneHanded> oneHandedOptional,
             BroadcastDispatcher broadcastDispatcher,
             ShellTransitions shellTransitions,
+            ScreenLifecycle screenLifecycle,
             Optional<StartingSurface> startingSurface,
-            SmartspaceTransitionController smartspaceTransitionController) {
+            SmartspaceTransitionController smartspaceTransitionController,
+            DumpManager dumpManager) {
         super(broadcastDispatcher);
         mContext = context;
         mPipOptional = pipOptional;
@@ -631,7 +555,7 @@
                 com.android.internal.R.string.config_recentsComponentName));
         mQuickStepIntent = new Intent(ACTION_QUICKSTEP)
                 .setPackage(mRecentsComponentName.getPackageName());
-        mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext.getResources());
+        mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
         mSupportsRoundedCornersOnWindows = ScreenDecorationsUtils
                 .supportsRoundedCornersOnWindows(mContext.getResources());
         mSysUiState = sysUiState;
@@ -642,6 +566,8 @@
         // Assumes device always starts with back button until launcher tells it that it does not
         mNavBarButtonAlpha = 1.0f;
 
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
         // Listen for nav bar mode changes
         mNavBarMode = navModeController.addListener(this);
 
@@ -675,6 +601,13 @@
         // Listen for user setup
         startTracking();
 
+        screenLifecycle.addObserver(new ScreenLifecycle.Observer() {
+            @Override
+            public void onScreenTurnedOn() {
+                notifyScreenTurnedOn();
+            }
+        });
+
         // Connect to the service
         updateEnabledState();
         startConnectionToCurrentUser();
@@ -704,18 +637,22 @@
                 mNavBarControllerLazy.get().getDefaultNavigationBar();
         final NavigationBarView navBarView =
                 mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId());
+        final NotificationPanelViewController panelController =
+                mStatusBarOptionalLazy.get().get().getPanelController();
         if (SysUiState.DEBUG) {
             Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment
-                    + " navBarView=" + navBarView);
+                    + " navBarView=" + navBarView + " panelController=" + panelController);
         }
 
         if (navBarFragment != null) {
             navBarFragment.updateSystemUiStateFlags(-1);
         }
         if (navBarView != null) {
-            navBarView.updatePanelSystemUiStateFlags();
             navBarView.updateDisabledSystemUiStateFlags();
         }
+        if (panelController != null) {
+            panelController.updateSystemUiStateFlags();
+        }
         if (mStatusBarWinController != null) {
             mStatusBarWinController.notifyStateChangedCallbacks();
         }
@@ -736,12 +673,13 @@
     }
 
     private void onStatusBarStateChanged(boolean keyguardShowing, boolean keyguardOccluded,
-            boolean bouncerShowing) {
+            boolean bouncerShowing, boolean isDozing) {
         mSysUiState.setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING,
                         keyguardShowing && !keyguardOccluded)
                 .setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED,
                         keyguardShowing && keyguardOccluded)
                 .setFlag(SYSUI_STATE_BOUNCER_SHOWING, bouncerShowing)
+                .setFlag(SYSUI_STATE_DEVICE_DOZING, isDozing)
                 .commitUpdate(mContext.getDisplayId());
     }
 
@@ -766,10 +704,9 @@
     public void cleanupAfterDeath() {
         if (mInputFocusTransferStarted) {
             mHandler.post(() -> {
-                mStatusBarOptionalLazy.ifPresent(statusBarLazy -> {
+                mStatusBarOptionalLazy.get().ifPresent(statusBar -> {
                     mInputFocusTransferStarted = false;
-                    statusBarLazy.get().onInputFocusTransfer(false, true /* cancel */,
-                            0 /* velocity */);
+                    statusBar.onInputFocusTransfer(false, true /* cancel */, 0 /* velocity */);
                 });
             });
         }
@@ -881,6 +818,18 @@
         }
     }
 
+    private void onTaskbarStatusUpdated(boolean visible, boolean stashed) {
+        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
+            mConnectionCallbacks.get(i).onTaskbarStatusUpdated(visible, stashed);
+        }
+    }
+
+    private void onTaskbarAutohideSuspend(boolean suspend) {
+        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
+            mConnectionCallbacks.get(i).onTaskbarAutohideSuspend(suspend);
+        }
+    }
+
     private void notifyConnectionChanged() {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onConnectionChanged(mOverviewProxy != null);
@@ -961,25 +910,61 @@
         }
     }
 
+    /**
+     * Notifies the Launcher that screen turned on and ready to use
+     */
+    public void notifyScreenTurnedOn() {
+        try {
+            if (mOverviewProxy != null) {
+                mOverviewProxy.onScreenTurnedOn();
+            } else {
+                Log.e(TAG_OPS, "Failed to get overview proxy for screen turned on event.");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG_OPS, "Failed to call notifyScreenTurnedOn()", e);
+        }
+    }
+
     void notifyToggleRecentApps() {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onToggleRecentApps();
         }
     }
 
-    public void notifyImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
-            boolean showImeSwitcher) {
+    public void disable(int displayId, int state1, int state2, boolean animate) {
         try {
             if (mOverviewProxy != null) {
-                mOverviewProxy.onImeWindowStatusChanged(displayId, token, vis, backDisposition,
-                        showImeSwitcher);
+                mOverviewProxy.disable(displayId, state1, state2, animate);
             } else {
-                Log.e(TAG_OPS, "Failed to get overview proxy for setting IME status.");
+                Log.e(TAG_OPS, "Failed to get overview proxy for disable flags.");
             }
         } catch (RemoteException e) {
-            Log.e(TAG_OPS, "Failed to call notifyImeWindowStatus()", e);
+            Log.e(TAG_OPS, "Failed to call disable()", e);
         }
+    }
 
+    public void onRotationProposal(int rotation, boolean isValid) {
+        try {
+            if (mOverviewProxy != null) {
+                mOverviewProxy.onRotationProposal(rotation, isValid);
+            } else {
+                Log.e(TAG_OPS, "Failed to get overview proxy for proposing rotation.");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG_OPS, "Failed to call onRotationProposal()", e);
+        }
+    }
+
+    public void onSystemBarAttributesChanged(int displayId, int behavior) {
+        try {
+            if (mOverviewProxy != null) {
+                mOverviewProxy.onSystemBarAttributesChanged(displayId, behavior);
+            } else {
+                Log.e(TAG_OPS, "Failed to get overview proxy for system bar attr change.");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG_OPS, "Failed to call onSystemBarAttributesChanged()", e);
+        }
     }
 
     private void updateEnabledState() {
@@ -1026,11 +1011,11 @@
         /** Notify changes in the nav bar button alpha */
         default void onNavBarButtonAlphaChanged(float alpha, boolean animate) {}
         default void onHomeRotationEnabled(boolean enabled) {}
+        default void onTaskbarStatusUpdated(boolean visible, boolean stashed) {}
+        default void onTaskbarAutohideSuspend(boolean suspend) {}
         default void onSystemUiStateChanged(int sysuiStateFlags) {}
         default void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {}
         default void onAssistantGestureCompletion(float velocity) {}
         default void startAssistant(Bundle bundle) {}
-        default void onImeWindowStatusChanged(int displayId, IBinder token, int vis,
-                int backDisposition, boolean showImeSwitcher) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
index aa8d710..85bf98c 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
@@ -51,10 +51,10 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.util.leak.RotationUtils;
 
@@ -69,7 +69,7 @@
         NavigationModeController.ModeChangedListener {
 
     private final Context mContext;
-    private final Optional<Lazy<StatusBar>> mStatusBarOptionalLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
 
     private final AccessibilityManager mAccessibilityService;
     private final WindowManager mWindowManager;
@@ -82,7 +82,7 @@
     private int taskId;
 
     @Inject
-    public ScreenPinningRequest(Context context, Optional<Lazy<StatusBar>> statusBarOptionalLazy) {
+    public ScreenPinningRequest(Context context, Lazy<Optional<StatusBar>> statusBarOptionalLazy) {
         mContext = context;
         mStatusBarOptionalLazy = statusBarOptionalLazy;
         mAccessibilityService = (AccessibilityManager)
@@ -266,8 +266,9 @@
                         .setVisibility(View.INVISIBLE);
             }
 
-            NavigationBarView navigationBarView = mStatusBarOptionalLazy.map(
-                    statusBarLazy -> statusBarLazy.get().getNavigationBarView()).orElse(null);
+            final Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
+            NavigationBarView navigationBarView =
+                    statusBarOptional.map(StatusBar::getNavigationBarView).orElse(null);
             final boolean recentsVisible = navigationBarView != null
                     && navigationBarView.isRecentsButtonVisible();
             boolean touchExplorationEnabled = mAccessibilityService.isTouchExplorationEnabled();
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
index 0aa9d4d..5a6f2a2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
@@ -27,6 +27,7 @@
 import android.media.MediaRecorder;
 import android.media.projection.MediaProjection;
 import android.util.Log;
+import android.util.MathUtils;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -128,30 +129,64 @@
         mThread = new Thread(() -> {
             short[] bufferInternal = null;
             short[] bufferMic = null;
-            byte[] buffer = null;
+            byte[] buffer = new byte[size];
 
             if (mMic) {
                 bufferInternal = new short[size / 2];
                 bufferMic = new short[size / 2];
-            } else {
-                buffer = new byte[size];
             }
 
+            int readBytes = 0;
+            int readShortsInternal = 0;
+            int offsetShortsInternal = 0;
+            int readShortsMic = 0;
+            int offsetShortsMic = 0;
             while (true) {
-                int readBytes = 0;
-                int readShortsInternal = 0;
-                int readShortsMic = 0;
                 if (mMic) {
-                    readShortsInternal = mAudioRecord.read(bufferInternal, 0,
-                            bufferInternal.length);
-                    readShortsMic = mAudioRecordMic.read(bufferMic, 0, bufferMic.length);
+                    readShortsInternal = mAudioRecord.read(bufferInternal, offsetShortsInternal,
+                            bufferInternal.length - offsetShortsInternal);
+                    readShortsMic = mAudioRecordMic.read(
+                            bufferMic, offsetShortsMic, bufferMic.length - offsetShortsMic);
+
+                    // if both error, end the recording
+                    if (readShortsInternal < 0 && readShortsMic < 0) {
+                        break;
+                    }
+
+                    // if one has an errors, fill its buffer with zeros and assume it is mute
+                    // with the same size as the other buffer
+                    if (readShortsInternal < 0) {
+                        readShortsInternal = readShortsMic;
+                        offsetShortsInternal = offsetShortsMic;
+                        java.util.Arrays.fill(bufferInternal, (short) 0);
+                    }
+
+                    if (readShortsMic < 0) {
+                        readShortsMic = readShortsInternal;
+                        offsetShortsMic = offsetShortsInternal;
+                        java.util.Arrays.fill(bufferMic, (short) 0);
+                    }
+
+                    // Add offset (previous unmixed values) to the buffer
+                    readShortsInternal += offsetShortsInternal;
+                    readShortsMic += offsetShortsMic;
+
+                    int minShorts = Math.min(readShortsInternal, readShortsMic);
+                    readBytes = minShorts * 2;
 
                     // modify the volume
-                    bufferMic = scaleValues(bufferMic,
-                            readShortsMic, MIC_VOLUME_SCALE);
-                    readBytes = Math.min(readShortsInternal, readShortsMic) * 2;
-                    buffer = addAndConvertBuffers(bufferInternal, readShortsInternal, bufferMic,
-                            readShortsMic);
+                    // scale only mixed shorts
+                    scaleValues(bufferMic, minShorts, MIC_VOLUME_SCALE);
+                    // Mix the two buffers
+                    addAndConvertBuffers(bufferInternal, bufferMic, buffer, minShorts);
+
+                    // shift unmixed shorts to the beginning of the buffer
+                    shiftToStart(bufferInternal, minShorts, offsetShortsInternal);
+                    shiftToStart(bufferMic, minShorts, offsetShortsMic);
+
+                    // reset the offset for the next loop
+                    offsetShortsInternal = readShortsInternal - minShorts;
+                    offsetShortsMic = readShortsMic - minShorts;
                 } else {
                     readBytes = mAudioRecord.read(buffer, 0, buffer.length);
                 }
@@ -169,40 +204,31 @@
         });
     }
 
-    private short[] scaleValues(short[] buff, int len, float scale) {
-        for (int i = 0; i < len; i++) {
-            int oldValue = buff[i];
-            int newValue = (int) (buff[i] * scale);
-            if (newValue > Short.MAX_VALUE) {
-                newValue = Short.MAX_VALUE;
-            } else if (newValue < Short.MIN_VALUE) {
-                newValue = Short.MIN_VALUE;
-            }
-            buff[i] = (short) (newValue);
+    /**
+     * moves all bits from start to end to the beginning of the array
+     */
+    private void shiftToStart(short[] target, int start, int end) {
+        for (int i = 0; i  < end - start; i++) {
+            target[i] = target[start + i];
         }
-        return buff;
     }
-    private byte[] addAndConvertBuffers(short[] a1, int a1Limit, short[] a2, int a2Limit) {
-        int size = Math.max(a1Limit, a2Limit);
-        if (size < 0) return new byte[0];
-        byte[] buff = new byte[size * 2];
-        for (int i = 0; i < size; i++) {
-            int sum;
-            if (i > a1Limit) {
-                sum = a2[i];
-            } else if (i > a2Limit) {
-                sum = a1[i];
-            } else {
-                sum = (int) a1[i] + (int) a2[i];
-            }
 
-            if (sum > Short.MAX_VALUE) sum = Short.MAX_VALUE;
-            if (sum < Short.MIN_VALUE) sum = Short.MIN_VALUE;
-            int byteIndex = i * 2;
-            buff[byteIndex] = (byte) (sum & 0xff);
-            buff[byteIndex + 1] = (byte) ((sum >> 8) & 0xff);
+    private void scaleValues(short[] buff, int len, float scale) {
+        for (int i = 0; i < len; i++) {
+            int newValue = (int) (buff[i] * scale);
+            buff[i] = (short) MathUtils.constrain(newValue, Short.MIN_VALUE, Short.MAX_VALUE);
         }
-        return buff;
+    }
+
+    private void addAndConvertBuffers(short[] src1, short[] src2, byte[] dst, int sizeShorts) {
+        for (int i = 0; i < sizeShorts; i++) {
+            int sum;
+            sum = (short) MathUtils.constrain(
+                    (int) src1[i] + (int) src2[i], Short.MIN_VALUE, Short.MAX_VALUE);
+            int byteIndex = i * 2;
+            dst[byteIndex] = (byte) (sum & 0xff);
+            dst[byteIndex + 1] = (byte) ((sum >> 8) & 0xff);
+        }
     }
 
     private void encode(byte[] buffer, int readBytes) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index 31d51f1..a42b34c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -326,18 +326,11 @@
                 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
 
         mTransitionView.setImageBitmap(mOutputBitmap);
+        mTransitionView.setVisibility(View.VISIBLE);
         mTransitionView.setTransitionName(
                 ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
         // TODO: listen for transition completing instead of finishing onStop
         mTransitionStarted = true;
-        int[] locationOnScreen = new int[2];
-        mTransitionView.getLocationOnScreen(locationOnScreen);
-        int[] locationInWindow = new int[2];
-        mTransitionView.getLocationInWindow(locationInWindow);
-        int deltaX = locationOnScreen[0] - locationInWindow[0];
-        int deltaY = locationOnScreen[1] - locationInWindow[1];
-        mTransitionView.setX(mTransitionView.getX() - deltaX);
-        mTransitionView.setY(mTransitionView.getY() - deltaY);
         startActivity(intent,
                 ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView,
                         ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle());
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 8def475..5b4db14 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -940,12 +940,10 @@
      */
     private Supplier<ActionTransition> getActionTransitionSupplier() {
         return () -> {
-            View preview = mScreenshotView.getTransitionView();
-            preview.setX(preview.getX() - mScreenshotView.getStaticLeftMargin());
             Pair<ActivityOptions, ExitTransitionCoordinator> transition =
                     ActivityOptions.startSharedElementAnimation(
                             mWindow, new ScreenshotExitTransitionCallbacksSupplier(true).get(),
-                            null, Pair.create(mScreenshotView.getTransitionView(),
+                            null, Pair.create(mScreenshotView.getScreenshotPreview(),
                                     ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
             transition.second.startExit();
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index dfb39e3..7222b03 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -137,13 +137,11 @@
     private int mNavMode;
     private boolean mOrientationPortrait;
     private boolean mDirectionLTR;
-    private int mStaticLeftMargin;
 
     private ScreenshotSelectorView mScreenshotSelectorView;
     private ImageView mScrollingScrim;
     private View mScreenshotStatic;
     private ImageView mScreenshotPreview;
-    private View mTransitionView;
     private View mScreenshotPreviewBorder;
     private ImageView mScrollablePreview;
     private ImageView mScreenshotFlash;
@@ -341,7 +339,6 @@
         mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
         mScreenshotStatic = requireNonNull(findViewById(R.id.global_screenshot_static));
         mScreenshotPreview = requireNonNull(findViewById(R.id.global_screenshot_preview));
-        mTransitionView = requireNonNull(findViewById(R.id.screenshot_transition_view));
         mScreenshotPreviewBorder = requireNonNull(
                 findViewById(R.id.global_screenshot_preview_border));
         mScreenshotPreview.setClipToOutline(true);
@@ -387,12 +384,8 @@
         requestFocus();
     }
 
-    View getTransitionView() {
-        return mTransitionView;
-    }
-
-    int getStaticLeftMargin() {
-        return mStaticLeftMargin;
+    View getScreenshotPreview() {
+        return mScreenshotPreview;
     }
 
     /**
@@ -442,7 +435,6 @@
                         Math.max(navBarInsets.bottom, waterfall.bottom));
             }
         }
-        mStaticLeftMargin = p.leftMargin;
         mScreenshotStatic.setLayoutParams(p);
         mScreenshotStatic.requestLayout();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 1ad253e..acc6ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -30,7 +30,6 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
@@ -47,15 +46,16 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.RestrictedLockUtilsInternal;
-import com.android.systemui.Dependency;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 
 import java.util.ArrayList;
 
 import javax.inject.Inject;
 
-public class BrightnessController implements ToggleSlider.Listener {
+public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController {
     private static final String TAG = "StatusBar.BrightnessController";
     private static final int SLIDER_ANIMATION_DURATION = 3000;
 
@@ -109,6 +109,11 @@
 
     private ValueAnimator mSliderAnimator;
 
+    @Override
+    public void setMirror(BrightnessMirrorController controller) {
+        mControl.setMirrorControllerAndMirror(controller);
+    }
+
     public interface BrightnessStateChangeCallback {
         /** Indicates that some of the brightness settings have changed */
         void onBrightnessLevelChanged();
@@ -282,12 +287,15 @@
         }
     };
 
-    public BrightnessController(Context context, ToggleSlider control,
-            BroadcastDispatcher broadcastDispatcher) {
+    public BrightnessController(
+            Context context,
+            ToggleSlider control,
+            BroadcastDispatcher broadcastDispatcher,
+            @Background Handler bgHandler) {
         mContext = context;
         mControl = control;
         mControl.setMax(GAMMA_SPACE_MAX);
-        mBackgroundHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER));
+        mBackgroundHandler = bgHandler;
         mUserTracker = new CurrentUserTracker(broadcastDispatcher) {
             @Override
             public void onUserSwitched(int newUserId) {
@@ -385,6 +393,14 @@
         });
     }
 
+    public void hideSlider() {
+        mControl.hideView();
+    }
+
+    public void showSlider() {
+        mControl.showView();
+    }
+
     private void setBrightness(float brightness) {
         mDisplayManager.setTemporaryBrightness(mDisplayId, brightness);
     }
@@ -450,16 +466,25 @@
     public static class Factory {
         private final Context mContext;
         private final BroadcastDispatcher mBroadcastDispatcher;
+        private final Handler mBackgroundHandler;
 
         @Inject
-        public Factory(Context context, BroadcastDispatcher broadcastDispatcher) {
+        public Factory(
+                Context context,
+                BroadcastDispatcher broadcastDispatcher,
+                @Background Handler bgHandler) {
             mContext = context;
             mBroadcastDispatcher = broadcastDispatcher;
+            mBackgroundHandler = bgHandler;
         }
 
         /** Create a {@link BrightnessController} */
         public BrightnessController create(ToggleSlider toggleSlider) {
-            return new BrightnessController(mContext, toggleSlider, mBroadcastDispatcher);
+            return new BrightnessController(
+                    mContext,
+                    toggleSlider,
+                    mBroadcastDispatcher,
+                    mBackgroundHandler);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 0f97e43..8fc831a 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -21,6 +21,7 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.Handler;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.View;
@@ -32,6 +33,7 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Background;
 
 import javax.inject.Inject;
 
@@ -41,13 +43,16 @@
     private BrightnessController mBrightnessController;
     private final BrightnessSlider.Factory mToggleSliderFactory;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final Handler mBackgroundHandler;
 
     @Inject
     public BrightnessDialog(
             BroadcastDispatcher broadcastDispatcher,
-            BrightnessSlider.Factory factory) {
+            BrightnessSlider.Factory factory,
+            @Background Handler bgHandler) {
         mBroadcastDispatcher = broadcastDispatcher;
         mToggleSliderFactory = factory;
+        mBackgroundHandler = bgHandler;
     }
 
 
@@ -76,7 +81,8 @@
         controller.init();
         frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT);
 
-        mBrightnessController = new BrightnessController(this, controller, mBroadcastDispatcher);
+        mBrightnessController = new BrightnessController(
+                this, controller, mBroadcastDispatcher, mBackgroundHandler);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
new file mode 100644
index 0000000..51aa339
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.settings.brightness
+
+import com.android.systemui.statusbar.policy.BrightnessMirrorController
+import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener
+
+class BrightnessMirrorHandler(private val brightnessController: MirroredBrightnessController) {
+
+    private var mirrorController: BrightnessMirrorController? = null
+
+    private val brightnessMirrorListener = BrightnessMirrorListener { updateBrightnessMirror() }
+
+    fun onQsPanelAttached() {
+        mirrorController?.addCallback(brightnessMirrorListener)
+    }
+
+    fun onQsPanelDettached() {
+        mirrorController?.removeCallback(brightnessMirrorListener)
+    }
+
+    fun setController(controller: BrightnessMirrorController) {
+        mirrorController?.removeCallback(brightnessMirrorListener)
+        mirrorController = controller
+        mirrorController?.addCallback(brightnessMirrorListener)
+        updateBrightnessMirror()
+    }
+
+    private fun updateBrightnessMirror() {
+        mirrorController?.let { brightnessController.setMirror(it) }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java
index 0ff6216..b0e320a 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSlider.java
@@ -140,13 +140,7 @@
     @Override
     public void setMirrorControllerAndMirror(BrightnessMirrorController c) {
         mMirrorController = c;
-        if (c != null) {
-            setMirror(c.getToggleSlider());
-        } else {
-            // If there's no mirror, we may be the ones dispatching, events but we should not mirror
-            // them
-            mView.setOnDispatchTouchEventListener(null);
-        }
+        setMirror(c.getToggleSlider());
     }
 
     @Override
@@ -180,6 +174,16 @@
         return mView.getValue();
     }
 
+    @Override
+    public void hideView() {
+        mView.setVisibility(View.GONE);
+    }
+
+    @Override
+    public void showView() {
+        mView.setVisibility(View.VISIBLE);
+    }
+
     private final SeekBar.OnSeekBarChangeListener mSeekListener =
             new SeekBar.OnSeekBarChangeListener() {
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt
new file mode 100644
index 0000000..8d857de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.settings.brightness
+
+import com.android.systemui.statusbar.policy.BrightnessMirrorController
+
+/**
+ * Indicates controller that has brightness slider and uses [BrightnessMirrorController]
+ */
+interface MirroredBrightnessController {
+    fun setMirror(controller: BrightnessMirrorController)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java
index a988c7a..5de22d4 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java
@@ -35,4 +35,7 @@
     int getMax();
     void setValue(int value);
     int getValue();
+
+    void showView();
+    void hideView();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 50911d16..6676901 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -51,6 +51,7 @@
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -78,7 +79,8 @@
  * coalescing these calls so they don't stack up.  For the calls
  * are coalesced, note that they are all idempotent.
  */
-public class CommandQueue extends IStatusBar.Stub implements CallbackController<Callbacks>,
+public class CommandQueue extends IStatusBar.Stub implements
+        CallbackController<Callbacks>,
         DisplayManager.DisplayListener {
     private static final String TAG = CommandQueue.class.getSimpleName();
 
@@ -337,7 +339,8 @@
          */
         default void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, boolean isFullscreen) { }
+                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                String packageName) { }
 
         /**
          * @see IStatusBar#showTransient(int, int[]).
@@ -997,7 +1000,7 @@
     @Override
     public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, boolean isFullscreen) {
+            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
             args.argi1 = displayId;
@@ -1005,7 +1008,8 @@
             args.argi3 = navbarColorManagedByIme ? 1 : 0;
             args.arg1 = appearanceRegions;
             args.argi4 = behavior;
-            args.argi5 = isFullscreen ? 1 : 0;
+            args.arg2 = requestedVisibilities;
+            args.arg3 = packageName;
             mHandler.obtainMessage(MSG_SYSTEM_BAR_CHANGED, args).sendToTarget();
         }
     }
@@ -1389,7 +1393,7 @@
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).onSystemBarAttributesChanged(args.argi1, args.argi2,
                                 (AppearanceRegion[]) args.arg1, args.argi3 == 1, args.argi4,
-                                args.argi5 == 1);
+                                (InsetsVisibilities) args.arg2, (String) args.arg3);
                     }
                     args.recycle();
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt
new file mode 100644
index 0000000..cf34db2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/DisableFlagsLogger.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar
+
+import android.app.StatusBarManager.DISABLE_BACK
+import android.app.StatusBarManager.DISABLE_CLOCK
+import android.app.StatusBarManager.DISABLE_EXPAND
+import android.app.StatusBarManager.DISABLE_HOME
+import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS
+import android.app.StatusBarManager.DISABLE_NOTIFICATION_ALERTS
+import android.app.StatusBarManager.DISABLE_ONGOING_CALL_CHIP
+import android.app.StatusBarManager.DISABLE_RECENT
+import android.app.StatusBarManager.DISABLE_SEARCH
+import android.app.StatusBarManager.DISABLE_SYSTEM_INFO
+import android.app.StatusBarManager.DISABLE2_GLOBAL_ACTIONS
+import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
+import android.app.StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS
+import android.app.StatusBarManager.DISABLE2_SYSTEM_ICONS
+import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/**
+ * A singleton that creates concise but readable strings representing the values of the disable
+ * flags for debugging.
+ *
+ * See [CommandQueue.disable] for information about disable flags.
+ *
+ * Note that, for both lists passed in, each flag must have a distinct [DisableFlag.flagIsSetSymbol]
+ * and distinct [DisableFlag.flagNotSetSymbol] within the list. If this isn't true, the logs could
+ * be ambiguous so an [IllegalArgumentException] is thrown.
+ */
+@SysUISingleton
+class DisableFlagsLogger constructor(
+    private val disable1FlagsList: List<DisableFlag>,
+    private val disable2FlagsList: List<DisableFlag>
+) {
+
+    @Inject
+    constructor() : this(defaultDisable1FlagsList, defaultDisable2FlagsList)
+
+    init {
+        if (flagsListHasDuplicateSymbols(disable1FlagsList)) {
+            throw IllegalArgumentException("disable1 flags must have unique symbols")
+        }
+        if (flagsListHasDuplicateSymbols(disable2FlagsList)) {
+            throw IllegalArgumentException("disable2 flags must have unique symbols")
+        }
+    }
+
+    private fun flagsListHasDuplicateSymbols(list: List<DisableFlag>): Boolean {
+        val numDistinctFlagOffStatus = list.map { it.getFlagStatus(0) }.distinct().count()
+        val numDistinctFlagOnStatus = list
+                .map { it.getFlagStatus(Int.MAX_VALUE) }
+                .distinct()
+                .count()
+        return numDistinctFlagOffStatus < list.count() || numDistinctFlagOnStatus < list.count()
+    }
+
+    /**
+     * Returns a string representing the, old, new, and new-after-modification disable flag states,
+     * as well as the differences between each of the states.
+     *
+     * Example:
+     *   Old: EnaiHbcRso.qINgr | New: EnaihBcRso.qiNGR (hB.iGR) | New after local modification:
+     *   EnaihBcRso.qInGR (.n)
+     *
+     * A capital character signifies the flag is set and a lowercase character signifies that the
+     * flag isn't set. The flag states will be logged in the same order as the passed-in lists.
+     *
+     * The difference between states is written between parentheses, and won't be included if there
+     * is no difference. the new-after-modification state also won't be included if there's no
+     * difference from the new state.
+     *
+     * @param old the disable state that had been previously sent.
+     * @param new the new disable state that has just been sent.
+     * @param newAfterLocalModification the new disable states after a class has locally modified
+     *   them. Null if the class does not locally modify.
+     */
+    fun getDisableFlagsString(
+        old: DisableState,
+        new: DisableState,
+        newAfterLocalModification: DisableState? = null
+    ): String {
+        val builder = StringBuilder("Received new disable state. ")
+        builder.append("Old: ")
+        builder.append(getFlagsString(old))
+        builder.append(" | New: ")
+        if (old != new) {
+            builder.append(getFlagsStringWithDiff(old, new))
+        } else {
+            builder.append(getFlagsString(old))
+        }
+
+        if (newAfterLocalModification != null && new != newAfterLocalModification) {
+            builder.append(" | New after local modification: ")
+            builder.append(getFlagsStringWithDiff(new, newAfterLocalModification))
+        }
+
+        return builder.toString()
+    }
+
+    /**
+     * Returns a string representing [new] state, as well as the difference from [old] to [new]
+     * (if there is one).
+     */
+    private fun getFlagsStringWithDiff(old: DisableState, new: DisableState): String {
+        val builder = StringBuilder()
+        builder.append(getFlagsString(new))
+        builder.append(" ")
+        builder.append(getDiffString(old, new))
+        return builder.toString()
+    }
+
+    /**
+     * Returns a string representing the difference between [old] and [new], or an empty string if
+     * there is no difference.
+     *
+     * For example, if old was "abc.DE" and new was "aBC.De", the difference returned would be
+     * "(BC.e)".
+     */
+    private fun getDiffString(old: DisableState, new: DisableState): String {
+        if (old == new) {
+            return ""
+        }
+
+        val builder = StringBuilder("(")
+        disable1FlagsList.forEach {
+            val newSymbol = it.getFlagStatus(new.disable1)
+            if (it.getFlagStatus(old.disable1) != newSymbol) {
+                builder.append(newSymbol)
+            }
+        }
+        builder.append(".")
+        disable2FlagsList.forEach {
+            val newSymbol = it.getFlagStatus(new.disable2)
+            if (it.getFlagStatus(old.disable2) != newSymbol) {
+                builder.append(newSymbol)
+            }
+        }
+        builder.append(")")
+        return builder.toString()
+    }
+
+    /** Returns a string representing the disable flag states, e.g. "EnaihBcRso.qiNGR".  */
+    private fun getFlagsString(state: DisableState): String {
+        val builder = StringBuilder("")
+        disable1FlagsList.forEach { builder.append(it.getFlagStatus(state.disable1)) }
+        builder.append(".")
+        disable2FlagsList.forEach { builder.append(it.getFlagStatus(state.disable2)) }
+        return builder.toString()
+    }
+
+    /** A POJO representing each disable flag. */
+    class DisableFlag(
+        private val bitMask: Int,
+        private val flagIsSetSymbol: Char,
+        private val flagNotSetSymbol: Char
+    ) {
+
+        /**
+         * Returns a character representing whether or not this flag is set in [state].
+         *
+         * A capital character signifies the flag is set and a lowercase character signifies that
+         * the flag isn't set.
+         */
+        internal fun getFlagStatus(state: Int): Char =
+            if (0 != state and this.bitMask) this.flagIsSetSymbol
+            else this.flagNotSetSymbol
+    }
+
+    /** POJO to hold [disable1] and [disable2]. */
+    data class DisableState(val disable1: Int, val disable2: Int)
+}
+
+// LINT.IfChange
+private val defaultDisable1FlagsList: List<DisableFlagsLogger.DisableFlag> = listOf(
+        DisableFlagsLogger.DisableFlag(DISABLE_EXPAND, 'E', 'e'),
+        DisableFlagsLogger.DisableFlag(DISABLE_NOTIFICATION_ICONS, 'N', 'n'),
+        DisableFlagsLogger.DisableFlag(DISABLE_NOTIFICATION_ALERTS, 'A', 'a'),
+        DisableFlagsLogger.DisableFlag(DISABLE_SYSTEM_INFO, 'I', 'i'),
+        DisableFlagsLogger.DisableFlag(DISABLE_HOME, 'H', 'h'),
+        DisableFlagsLogger.DisableFlag(DISABLE_BACK, 'B', 'b'),
+        DisableFlagsLogger.DisableFlag(DISABLE_CLOCK, 'C', 'c'),
+        DisableFlagsLogger.DisableFlag(DISABLE_RECENT, 'R', 'r'),
+        DisableFlagsLogger.DisableFlag(DISABLE_SEARCH, 'S', 's'),
+        DisableFlagsLogger.DisableFlag(DISABLE_ONGOING_CALL_CHIP, 'O', 'o')
+)
+
+private val defaultDisable2FlagsList: List<DisableFlagsLogger.DisableFlag> = listOf(
+        DisableFlagsLogger.DisableFlag(DISABLE2_QUICK_SETTINGS, 'Q', 'q'),
+        DisableFlagsLogger.DisableFlag(DISABLE2_SYSTEM_ICONS, 'I', 'i'),
+        DisableFlagsLogger.DisableFlag(DISABLE2_NOTIFICATION_SHADE, 'N', 'n'),
+        DisableFlagsLogger.DisableFlag(DISABLE2_GLOBAL_ACTIONS, 'G', 'g'),
+        DisableFlagsLogger.DisableFlag(DISABLE2_ROTATE_SUGGESTIONS, 'R', 'r')
+)
+// LINT.ThenChange(frameworks/base/core/java/android/app/StatusBarManager.java)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
deleted file mode 100644
index 5a42458..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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;
-
-import android.content.Context;
-import android.util.FeatureFlagUtils;
-
-import com.android.systemui.R;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.flags.FeatureFlagReader;
-
-import javax.inject.Inject;
-
-/**
- * Class to manage simple DeviceConfig-based feature flags.
- *
- * See {@link FeatureFlagReader} for instructions on defining and flipping flags.
- */
-@SysUISingleton
-public class FeatureFlags {
-    private final FeatureFlagReader mFlagReader;
-    private final Context mContext;
-
-    @Inject
-    public FeatureFlags(FeatureFlagReader flagReader, Context context) {
-        mFlagReader = flagReader;
-        mContext = context;
-    }
-
-    public boolean isNewNotifPipelineEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_notification_pipeline2);
-    }
-
-    public boolean isNewNotifPipelineRenderingEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_notification_pipeline2_rendering);
-    }
-
-    /** b/171917882 */
-    public boolean isTwoColumnNotificationShadeEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_notification_twocolumn);
-    }
-
-    public boolean isKeyguardLayoutEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_keyguard_layout);
-    }
-
-    public boolean useNewLockscreenAnimations() {
-        return mFlagReader.isEnabled(R.bool.flag_lockscreen_animations);
-    }
-
-    public boolean isPeopleTileEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_conversations);
-    }
-
-    public boolean isMonetEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_monet);
-    }
-
-    public boolean isPMLiteEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_pm_lite);
-    }
-
-    public boolean isChargingRippleEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_charging_ripple);
-    }
-
-    public boolean isOngoingCallStatusBarChipEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_ongoing_call_status_bar_chip);
-    }
-
-    public boolean isSmartspaceEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_smartspace);
-    }
-
-    public boolean isSmartspaceDedupingEnabled() {
-        return isSmartspaceEnabled() && mFlagReader.isEnabled(R.bool.flag_smartspace_deduping);
-    }
-
-    public boolean isNewKeyguardSwipeAnimationEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_new_unlock_swipe_animation);
-    }
-
-    public boolean isSmartSpaceSharedElementTransitionEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_smartspace_shared_element_transition);
-    }
-
-    /** Whether or not to use the provider model behavior for the status bar icons */
-    public boolean isCombinedStatusBarSignalIconsEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_combined_status_bar_signal_icons);
-    }
-
-    /** System setting for provider model behavior */
-    public boolean isProviderModelSettingEnabled() {
-        return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL);
-    }
-
-    /** static method for the system setting */
-    public static boolean isProviderModelSettingEnabled(Context context) {
-        return FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 8a39719..74ebfe5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -147,7 +147,6 @@
     private boolean mBatteryPresent = true;
     private long mChargingTimeRemaining;
     private String mMessageToShowOnScreenOn;
-    protected int mLockScreenMode;
     private boolean mInited;
 
     private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
@@ -600,10 +599,6 @@
         mHideTransientMessageOnScreenOff = hideOnScreenOff && transientIndication != null;
         mHandler.removeMessages(MSG_HIDE_TRANSIENT);
         mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK);
-        if (mDozing && !TextUtils.isEmpty(mTransientIndication)) {
-            // Make sure this doesn't get stuck and burns in. Acquire wakelock until its cleared.
-            mWakeLock.setAcquired(true);
-        }
         hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS);
 
         updateIndication(false);
@@ -623,10 +618,6 @@
     }
 
     protected final void updateIndication(boolean animate) {
-        if (TextUtils.isEmpty(mTransientIndication)) {
-            mWakeLock.setAcquired(false);
-        }
-
         if (!mVisible) {
             return;
         }
@@ -644,24 +635,31 @@
             // colors can be hard to read in low brightness.
             mTopIndicationView.setTextColor(Color.WHITE);
             if (!TextUtils.isEmpty(mTransientIndication)) {
-                mTopIndicationView.switchIndication(mTransientIndication, null);
+                mWakeLock.setAcquired(true);
+                mTopIndicationView.switchIndication(mTransientIndication, null,
+                        true, () -> mWakeLock.setAcquired(false));
             } else if (!mBatteryPresent) {
                 // If there is no battery detected, hide the indication and bail
                 mIndicationArea.setVisibility(GONE);
             } else if (!TextUtils.isEmpty(mAlignmentIndication)) {
-                mTopIndicationView.switchIndication(mAlignmentIndication, null);
+                mTopIndicationView.switchIndication(mAlignmentIndication, null,
+                        false /* animate */, null /* onAnimationEndCallback */);
                 mTopIndicationView.setTextColor(mContext.getColor(R.color.misalignment_text_color));
             } else if (mPowerPluggedIn || mEnableBatteryDefender) {
                 String indication = computePowerIndication();
                 if (animate) {
-                    animateText(mTopIndicationView, indication);
+                    mWakeLock.setAcquired(true);
+                    mTopIndicationView.switchIndication(indication, null, true /* animate */,
+                            () -> mWakeLock.setAcquired(false));
                 } else {
-                    mTopIndicationView.switchIndication(indication, null);
+                    mTopIndicationView.switchIndication(indication, null, false /* animate */,
+                            null /* onAnimationEndCallback */);
                 }
             } else {
                 String percentage = NumberFormat.getPercentInstance()
                         .format(mBatteryLevel / 100f);
-                mTopIndicationView.switchIndication(percentage, null);
+                mTopIndicationView.switchIndication(percentage, null /* indication */,
+                        false /* animate */, null /* onAnimationEnd*/);
             }
             return;
         }
@@ -819,12 +817,15 @@
         }
     }
 
-    private void showTryFingerprintMsg(String a11yString) {
+    private void showTryFingerprintMsg(int msgId, String a11yString) {
         if (mKeyguardUpdateMonitor.isUdfpsAvailable()) {
             // if udfps available, there will always be a tappable affordance to unlock
             // For example, the lock icon
             if (mKeyguardBypassController.getUserHasDeviceEntryIntent()) {
                 showTransientIndication(R.string.keyguard_unlock_press);
+            } else if (msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) {
+                // since face is locked out, simply show "try fingerprint"
+                showTransientIndication(R.string.keyguard_try_fingerprint);
             } else {
                 showTransientIndication(R.string.keyguard_face_failed_use_fp);
             }
@@ -860,11 +861,6 @@
         public static final int HIDE_DELAY_MS = 5000;
 
         @Override
-        public void onLockScreenModeChanged(int mode) {
-            mLockScreenMode = mode;
-        }
-
-        @Override
         public void onRefreshBatteryInfo(BatteryStatus status) {
             boolean isChargingOrFull = status.status == BatteryManager.BATTERY_STATUS_CHARGING
                     || status.status == BatteryManager.BATTERY_STATUS_FULL;
@@ -916,7 +912,7 @@
             } else if (mKeyguardUpdateMonitor.isScreenOn()) {
                 if (biometricSourceType == BiometricSourceType.FACE
                         && shouldSuppressFaceMsgAndShowTryFingerprintMsg()) {
-                    showTryFingerprintMsg(helpString);
+                    showTryFingerprintMsg(msgId, helpString);
                     return;
                 }
                 showTransientIndication(helpString, false /* isError */, showActionToUnlock);
@@ -936,7 +932,7 @@
                     && shouldSuppressFaceMsgAndShowTryFingerprintMsg()
                     && !mStatusBarKeyguardViewManager.isBouncerShowing()
                     && mKeyguardUpdateMonitor.isScreenOn()) {
-                showTryFingerprintMsg(errString);
+                showTryFingerprintMsg(msgId, errString);
                 return;
             }
             if (msgId == FaceManager.FACE_ERROR_TIMEOUT) {
@@ -945,7 +941,7 @@
                 if (!mStatusBarKeyguardViewManager.isBouncerShowing()
                         && mKeyguardUpdateMonitor.isUdfpsEnrolled()
                         && mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
-                    showTryFingerprintMsg(errString);
+                    showTryFingerprintMsg(msgId, errString);
                 } else if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
                     mStatusBarKeyguardViewManager.showBouncerMessage(
                             mContext.getResources().getString(R.string.keyguard_unlock_press),
@@ -989,8 +985,8 @@
         private boolean shouldSuppressFaceMsgAndShowTryFingerprintMsg() {
             // For dual biometric, don't show face auth messages
             return mKeyguardUpdateMonitor.isFingerprintDetectionRunning()
-                && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                    true /* isStrongBiometric */);
+                    && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                            true /* isStrongBiometric */);
         }
 
         private boolean shouldSuppressFaceError(int msgId, KeyguardUpdateMonitor updateMonitor) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 21ed9da..77e329f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -12,8 +12,10 @@
 import android.graphics.RadialGradient
 import android.graphics.Shader
 import android.util.AttributeSet
+import android.util.MathUtils.lerp
 import android.view.View
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold
 import java.util.function.Consumer
 
 /**
@@ -63,12 +65,12 @@
     override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
         val interpolatedAmount = INTERPOLATOR.getInterpolation(amount)
         val ovalWidthIncreaseAmount =
-                LightRevealEffect.getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD)
+                getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD)
 
         val initialWidthMultiplier = (1f - OVAL_INITIAL_WIDTH_PERCENT) / 2f
 
         with(scrim) {
-            revealGradientEndColorAlpha = 1f - LightRevealEffect.getPercentPastThreshold(
+            revealGradientEndColorAlpha = 1f - getPercentPastThreshold(
                     amount, FADE_END_COLOR_OUT_THRESHOLD)
             setRevealGradientBounds(
                     scrim.width * initialWidthMultiplier +
@@ -83,6 +85,58 @@
     }
 }
 
+class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
+
+    private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE
+
+    override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
+        val interpolatedAmount = INTERPOLATOR.getInterpolation(amount)
+
+        scrim.startColorAlpha =
+            getPercentPastThreshold(1 - interpolatedAmount,
+                threshold = 1 - START_COLOR_REVEAL_PERCENTAGE)
+
+        scrim.revealGradientEndColorAlpha =
+            1f - getPercentPastThreshold(interpolatedAmount,
+                threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE)
+
+        // Start changing gradient bounds later to avoid harsh gradient in the beginning
+        val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1.0f, interpolatedAmount)
+
+        if (isVertical) {
+            scrim.setRevealGradientBounds(
+                left = scrim.width / 2 - (scrim.width / 2) * gradientBoundsAmount,
+                top = 0f,
+                right = scrim.width / 2 + (scrim.width / 2) * gradientBoundsAmount,
+                bottom = scrim.height.toFloat()
+            )
+        } else {
+            scrim.setRevealGradientBounds(
+                left = 0f,
+                top = scrim.height / 2 - (scrim.height / 2) * gradientBoundsAmount,
+                right = scrim.width.toFloat(),
+                bottom = scrim.height / 2 + (scrim.height / 2) * gradientBoundsAmount
+            )
+        }
+    }
+
+    private companion object {
+        // From which percentage we should start the gradient reveal width
+        // E.g. if 0 - starts with 0px width, 0.3f - starts with 30% width
+        private const val GRADIENT_START_BOUNDS_PERCENTAGE = 0.3f
+
+        // When to start changing alpha color of the gradient scrim
+        // E.g. if 0.6f - starts fading the gradient away at 60% and becomes completely
+        // transparent at 100%
+        private const val REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE = 0.6f
+
+        // When to finish displaying start color fill that reveals the content
+        // E.g. if 0.3f - the content won't be visible at 0% and it will gradually
+        // reduce the alpha until 30% (at this point the color fill is invisible)
+        private const val START_COLOR_REVEAL_PERCENTAGE = 0.3f
+    }
+}
+
 class CircleReveal(
     /** X-value of the circle center of the reveal. */
     val centerX: Float,
@@ -96,7 +150,7 @@
     override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
         // reveal amount updates already have an interpolator, so we intentionally use the
         // non-interpolated amount
-        val fadeAmount = LightRevealEffect.getPercentPastThreshold(amount, 0.5f)
+        val fadeAmount = getPercentPastThreshold(amount, 0.5f)
         val radius = startRadius + ((endRadius - startRadius) * amount)
         scrim.revealGradientEndColorAlpha = 1f - fadeAmount
         scrim.setRevealGradientBounds(
@@ -124,8 +178,7 @@
 
     override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
         val interpolatedAmount = Interpolators.FAST_OUT_SLOW_IN_REVERSE.getInterpolation(amount)
-        val fadeAmount =
-                LightRevealEffect.getPercentPastThreshold(interpolatedAmount, 0.5f)
+        val fadeAmount = getPercentPastThreshold(interpolatedAmount, 0.5f)
 
         with(scrim) {
             revealGradientEndColorAlpha = 1f - fadeAmount
@@ -155,6 +208,11 @@
     lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
 
     /**
+     * A runnable to call when the scrim has been fully revealed. This is only invoked once
+     */
+    var fullyRevealedRunnable: Runnable? = null
+
+    /**
      * How much of the underlying views are revealed, in percent. 0 means they will be completely
      * obscured and 1 means they'll be fully visible.
      */
@@ -165,10 +223,20 @@
 
                 revealEffect.setRevealAmountOnScrim(value, this)
                 updateScrimOpaque()
+                maybeTriggerFullyRevealedRunnable()
                 invalidate()
             }
         }
 
+    private fun maybeTriggerFullyRevealedRunnable() {
+        if (revealAmount == 1.0f) {
+            fullyRevealedRunnable?.let {
+                it.run()
+                fullyRevealedRunnable = null
+            }
+        }
+    }
+
     /**
      * The [LightRevealEffect] used to manipulate the radial gradient whenever [revealAmount]
      * changes.
@@ -187,6 +255,23 @@
     var revealGradientWidth: Float = 0f
     var revealGradientHeight: Float = 0f
 
+    /**
+     * Alpha of the fill that can be used in the beginning of the animation to hide the content.
+     * Normally the gradient bounds are animated from small size so the content is not visible,
+     * but if the start gradient bounds allow to see some content this could be used to make the
+     * reveal smoother. It can help to add fade in effect in the beginning of the animation.
+     * The color of the fill is determined by [revealGradientEndColor].
+     *
+     * 0 - no fill and content is visible, 1 - the content is covered with the start color
+     */
+    var startColorAlpha = 0f
+        set(value) {
+            if (field != value) {
+                field = value
+                invalidate()
+            }
+        }
+
     var revealGradientEndColor: Int = Color.BLACK
         set(value) {
             if (field != value) {
@@ -280,6 +365,10 @@
             return
         }
 
+        if (startColorAlpha > 0f) {
+            canvas.drawColor(updateColorAlpha(revealGradientEndColor, startColorAlpha))
+        }
+
         with(shaderGradientMatrix) {
             setScale(revealGradientWidth, revealGradientHeight, 0f, 0f)
             postTranslate(revealGradientCenter.x, revealGradientCenter.y)
@@ -293,11 +382,15 @@
 
     private fun setPaintColorFilter() {
         gradientPaint.colorFilter = PorterDuffColorFilter(
-                Color.argb(
-                        (revealGradientEndColorAlpha * 255).toInt(),
-                        Color.red(revealGradientEndColor),
-                        Color.green(revealGradientEndColor),
-                        Color.blue(revealGradientEndColor)),
-                PorterDuff.Mode.MULTIPLY)
+            updateColorAlpha(revealGradientEndColor, revealGradientEndColorAlpha),
+            PorterDuff.Mode.MULTIPLY)
     }
+
+    private fun updateColorAlpha(color: Int, alpha: Float): Int =
+        Color.argb(
+            (alpha * 255).toInt(),
+            Color.red(color),
+            Color.green(color),
+            Color.blue(color)
+        )
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 6fa06a7..dca7f70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -60,7 +60,6 @@
     private val mediaHierarchyManager: MediaHierarchyManager,
     private val scrimController: ScrimController,
     private val depthController: NotificationShadeDepthController,
-    private val featureFlags: FeatureFlags,
     private val context: Context,
     configurationController: ConfigurationController,
     falsingManager: FalsingManager
@@ -147,7 +146,7 @@
                 R.dimen.lockscreen_shade_scrim_transition_distance)
         fullTransitionDistance = context.resources.getDimensionPixelSize(
                 R.dimen.lockscreen_shade_qs_transition_distance)
-        useSplitShade = Utils.shouldUseSplitNotificationShade(featureFlags, context.resources)
+        useSplitShade = Utils.shouldUseSplitNotificationShade(context.resources)
     }
 
     fun setStackScroller(nsslController: NotificationStackScrollLayoutController) {
@@ -299,9 +298,8 @@
                     nsslController.setTransitionToFullShadeAmount(field)
                     notificationPanelController.setTransitionToFullShadeAmount(field,
                             false /* animate */, 0 /* delay */)
-                    // TODO: appear qs also in split shade
-                    val qsAmount = if (useSplitShade) 0f else field
-                    qS.setTransitionToFullShadeAmount(qsAmount, false /* animate */)
+                    val progress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance)
+                    qS.setTransitionToFullShadeAmount(field, progress)
                     // TODO: appear media also in split shade
                     val mediaAmount = if (useSplitShade) 0f else field
                     mediaHierarchyManager.setTransitionToFullShadeAmount(mediaAmount)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index e7e9404..db7d5c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -50,6 +50,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.recents.OverviewProxyService;
@@ -72,7 +73,9 @@
  */
 @SysUISingleton
 public class NotificationLockscreenUserManagerImpl implements
-        Dumpable, NotificationLockscreenUserManager, StateListener {
+        Dumpable,
+        NotificationLockscreenUserManager,
+        StateListener {
     private static final String TAG = "LockscreenUserManager";
     private static final boolean ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT = false;
 
@@ -202,7 +205,8 @@
             StatusBarStateController statusBarStateController,
             @Main Handler mainHandler,
             DeviceProvisionedController deviceProvisionedController,
-            KeyguardStateController keyguardStateController) {
+            KeyguardStateController keyguardStateController,
+            DumpManager dumpManager) {
         mContext = context;
         mMainHandler = mainHandler;
         mDevicePolicyManager = devicePolicyManager;
@@ -215,6 +219,8 @@
         mBroadcastDispatcher = broadcastDispatcher;
         mDeviceProvisionedController = deviceProvisionedController;
         mKeyguardStateController = keyguardStateController;
+
+        dumpManager.registerDumpable(this);
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 25cbdc5..dcb1e4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -52,6 +52,8 @@
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaData;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.SmartspaceMediaData;
@@ -72,7 +74,6 @@
 import com.android.systemui.statusbar.phone.ScrimState;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.Utils;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
@@ -84,6 +85,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 import dagger.Lazy;
@@ -132,7 +134,7 @@
     private final Context mContext;
     private final MediaSessionManager mMediaSessionManager;
     private final ArrayList<MediaListener> mMediaListeners;
-    private final Lazy<StatusBar> mStatusBarLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private final MediaArtworkProcessor mMediaArtworkProcessor;
     private final Set<AsyncTask<?, ?, ?>> mProcessArtworkTasks = new ArraySet<>();
 
@@ -177,7 +179,7 @@
      */
     public NotificationMediaManager(
             Context context,
-            Lazy<StatusBar> statusBarLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             Lazy<NotificationShadeWindowController> notificationShadeWindowController,
             NotificationEntryManager notificationEntryManager,
             MediaArtworkProcessor mediaArtworkProcessor,
@@ -186,8 +188,8 @@
             NotifCollection notifCollection,
             FeatureFlags featureFlags,
             @Main DelayableExecutor mainExecutor,
-            DeviceConfigProxy deviceConfig,
-            MediaDataManager mediaDataManager) {
+            MediaDataManager mediaDataManager,
+            DumpManager dumpManager) {
         mContext = context;
         mMediaArtworkProcessor = mediaArtworkProcessor;
         mKeyguardBypassController = keyguardBypassController;
@@ -197,7 +199,7 @@
         mMediaSessionManager = (MediaSessionManager) mContext.getSystemService(
                 Context.MEDIA_SESSION_SERVICE);
         // TODO: use KeyguardStateController#isOccluded to remove this dependency
-        mStatusBarLazy = statusBarLazy;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
         mNotificationShadeWindowController = notificationShadeWindowController;
         mEntryManager = notificationEntryManager;
         mMainExecutor = mainExecutor;
@@ -212,6 +214,8 @@
             setupNotifPipeline();
             mUsingNotifPipeline = true;
         }
+
+        dumpManager.registerDumpable(this);
     }
 
     private void setupNotifPipeline() {
@@ -694,7 +698,8 @@
 
         NotificationShadeWindowController windowController =
                 mNotificationShadeWindowController.get();
-        boolean hideBecauseOccluded = mStatusBarLazy.get().isOccluded();
+        boolean hideBecauseOccluded =
+                mStatusBarOptionalLazy.get().map(StatusBar::isOccluded).orElse(false);
 
         final boolean hasArtwork = artworkDrawable != null;
         mColorExtractor.setHasMediaArtwork(hasMediaArtwork);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 4552138..18a3d86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -45,6 +45,7 @@
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.widget.RemoteViews;
+import android.widget.RemoteViews.InteractionHandler;
 import android.widget.TextView;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -53,6 +54,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.dagger.StatusBarDependenciesModule;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -69,7 +71,9 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Stream;
 
@@ -118,7 +122,7 @@
     private final Handler mMainHandler;
     private final ActionClickLogger mLogger;
 
-    private final Lazy<StatusBar> mStatusBarLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
 
     protected final Context mContext;
     private final UserManager mUserManager;
@@ -134,14 +138,16 @@
     protected Callback mCallback;
     protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
 
-    private final RemoteViews.InteractionHandler
-            mInteractionHandler = new RemoteViews.InteractionHandler() {
+    private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>();
+
+    private final InteractionHandler mInteractionHandler = new InteractionHandler() {
 
         @Override
         public boolean onInteraction(
                 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
-            mStatusBarLazy.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
-                    "NOTIFICATION_CLICK");
+            mStatusBarOptionalLazy.get().ifPresent(
+                    statusBar -> statusBar.wakeUpIfDozing(
+                            SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK"));
 
             final NotificationEntry entry = getNotificationForParent(view.getParent());
             mLogger.logInitialClick(entry, pendingIntent);
@@ -280,17 +286,18 @@
             NotificationLockscreenUserManager lockscreenUserManager,
             SmartReplyController smartReplyController,
             NotificationEntryManager notificationEntryManager,
-            Lazy<StatusBar> statusBarLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             StatusBarStateController statusBarStateController,
             @Main Handler mainHandler,
             RemoteInputUriController remoteInputUriController,
             NotificationClickNotifier clickNotifier,
-            ActionClickLogger logger) {
+            ActionClickLogger logger,
+            DumpManager dumpManager) {
         mContext = context;
         mLockscreenUserManager = lockscreenUserManager;
         mSmartReplyController = smartReplyController;
         mEntryManager = notificationEntryManager;
-        mStatusBarLazy = statusBarLazy;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
         mMainHandler = mainHandler;
         mLogger = logger;
         mBarService = IStatusBarService.Stub.asInterface(
@@ -302,6 +309,8 @@
         mRemoteInputUriController = remoteInputUriController;
         mClickNotifier = clickNotifier;
 
+        dumpManager.registerDumpable(this);
+
         notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
             @Override
             public void onPreEntryUpdated(NotificationEntry entry) {
@@ -330,6 +339,11 @@
     public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
         mCallback = callback;
         mRemoteInputController = new RemoteInputController(delegate, mRemoteInputUriController);
+        // Register all stored callbacks from before the Controller was initialized.
+        for (RemoteInputController.Callback cb : mControllerCallbacks) {
+            mRemoteInputController.addCallback(cb);
+        }
+        mControllerCallbacks.clear();
         mRemoteInputController.addCallback(new RemoteInputController.Callback() {
             @Override
             public void onRemoteInputSent(NotificationEntry entry) {
@@ -375,6 +389,22 @@
         });
     }
 
+    public void addControllerCallback(RemoteInputController.Callback callback) {
+        if (mRemoteInputController != null) {
+            mRemoteInputController.addCallback(callback);
+        } else {
+            mControllerCallbacks.add(callback);
+        }
+    }
+
+    public void removeControllerCallback(RemoteInputController.Callback callback) {
+        if (mRemoteInputController != null) {
+            mRemoteInputController.removeCallback(callback);
+        } else {
+            mControllerCallbacks.remove(callback);
+        }
+    }
+
     /**
      * Activates a given {@link RemoteInput}
      *
@@ -561,17 +591,12 @@
         return mLifetimeExtenders;
     }
 
-    @Nullable
-    public RemoteInputController getController() {
-        return mRemoteInputController;
-    }
-
     @VisibleForTesting
     void onPerformRemoveNotification(NotificationEntry entry, final String key) {
         if (mKeysKeptForRemoteInputHistory.contains(key)) {
             mKeysKeptForRemoteInputHistory.remove(key);
         }
-        if (mRemoteInputController.isRemoteInputActive(entry)) {
+        if (isRemoteInputActive(entry)) {
             entry.mRemoteEditImeVisible = false;
             mRemoteInputController.removeRemoteInput(entry, null);
         }
@@ -580,7 +605,9 @@
     public void onPanelCollapsed() {
         for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
             NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
-            mRemoteInputController.removeRemoteInput(entry, null);
+            if (mRemoteInputController != null) {
+                mRemoteInputController.removeRemoteInput(entry, null);
+            }
             if (mNotificationLifetimeFinishedCallback != null) {
                 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
             }
@@ -596,8 +623,7 @@
         if (!FORCE_REMOTE_INPUT_HISTORY) {
             return false;
         }
-        return (mRemoteInputController.isSpinning(entry.getKey())
-                || entry.hasJustSentRemoteInput());
+        return isSpinning(entry.getKey()) || entry.hasJustSentRemoteInput();
     }
 
     /**
@@ -630,8 +656,8 @@
     public void checkRemoteInputOutside(MotionEvent event) {
         if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
                 && event.getX() == 0 && event.getY() == 0  // a touch outside both bars
-                && mRemoteInputController.isRemoteInputActive()) {
-            mRemoteInputController.closeRemoteInputs();
+                && isRemoteInputActive()) {
+            closeRemoteInputs();
         }
     }
 
@@ -713,6 +739,24 @@
         return mEntriesKeptForRemoteInputActive;
     }
 
+    public boolean isRemoteInputActive() {
+        return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive();
+    }
+
+    public boolean isRemoteInputActive(NotificationEntry entry) {
+        return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive(entry);
+    }
+
+    public boolean isSpinning(String entryKey) {
+        return mRemoteInputController != null && mRemoteInputController.isSpinning(entryKey);
+    }
+
+    public void closeRemoteInputs() {
+        if (mRemoteInputController != null) {
+            mRemoteInputController.closeRemoteInputs();
+        }
+    }
+
     /**
      * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
      * so we implement multiple NotificationLifetimeExtenders
@@ -820,7 +864,7 @@
     protected class RemoteInputActiveExtender extends RemoteInputExtender {
         @Override
         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
-            return mRemoteInputController.isRemoteInputActive(entry);
+            return isRemoteInputActive(entry);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 2a8771e..5648741e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -19,7 +19,6 @@
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
-import android.app.WallpaperManager
 import android.os.SystemClock
 import android.os.Trace
 import android.util.IndentingPrintWriter
@@ -33,15 +32,16 @@
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.systemui.Dumpable
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
-import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.PanelExpansionListener
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.WallpaperController
 import java.io.FileDescriptor
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -58,7 +58,7 @@
     private val biometricUnlockController: BiometricUnlockController,
     private val keyguardStateController: KeyguardStateController,
     private val choreographer: Choreographer,
-    private val wallpaperManager: WallpaperManager,
+    private val wallpaperController: WallpaperController,
     private val notificationShadeWindowController: NotificationShadeWindowController,
     private val dozeParameters: DozeParameters,
     dumpManager: DumpManager
@@ -73,6 +73,10 @@
         private const val TAG = "DepthController"
     }
 
+    /**
+     * Did we already unblur while dozing?
+     */
+    private var alreadyUnblurredWhileDozing = false
     lateinit var root: View
     private var blurRoot: View? = null
     private var keyguardAnimator: Animator? = null
@@ -181,12 +185,12 @@
         val animationRadius = MathUtils.constrain(shadeAnimation.radius,
                 blurUtils.minBlurRadius.toFloat(), blurUtils.maxBlurRadius.toFloat())
         val expansionRadius = blurUtils.blurRadiusOfRatio(
-                Interpolators.getNotificationScrimAlpha(
-                        if (shouldApplyShadeBlur()) shadeExpansion else 0f, false))
+                ShadeInterpolation.getNotificationScrimAlpha(
+                        if (shouldApplyShadeBlur()) shadeExpansion else 0f))
         var combinedBlur = (expansionRadius * INTERACTION_BLUR_FRACTION +
                 animationRadius * ANIMATION_BLUR_FRACTION)
-        val qsExpandedRatio = Interpolators.getNotificationScrimAlpha(qsPanelExpansion,
-                false /* notification */) * shadeExpansion
+        val qsExpandedRatio = ShadeInterpolation.getNotificationScrimAlpha(qsPanelExpansion) *
+                shadeExpansion
         combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(qsExpandedRatio))
         combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress))
         var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius)
@@ -215,15 +219,7 @@
         Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur)
         blurUtils.applyBlur(blurRoot?.viewRootImpl ?: root.viewRootImpl, blur, opaque)
         lastAppliedBlur = blur
-        try {
-            if (root.isAttachedToWindow && root.windowToken != null) {
-                wallpaperManager.setWallpaperZoomOut(root.windowToken, zoomOut)
-            } else {
-                Log.i(TAG, "Won't set zoom. Window not attached $root")
-            }
-        } catch (e: IllegalArgumentException) {
-            Log.w(TAG, "Can't set zoom. Window is gone: ${root.windowToken}", e)
-        }
+        wallpaperController.setNotificationShadeZoom(zoomOut)
         listeners.forEach {
             it.onWallpaperZoomOutChanged(zoomOut)
             it.onBlurRadiusChanged(blur)
@@ -237,9 +233,11 @@
     private val keyguardStateCallback = object : KeyguardStateController.Callback {
         override fun onKeyguardFadingAwayChanged() {
             if (!keyguardStateController.isKeyguardFadingAway ||
-                    biometricUnlockController.mode != MODE_WAKE_AND_UNLOCK) {
+                    !biometricUnlockController.isWakeAndUnlock) {
                 return
             }
+            // When wakeAndUnlocking the screen remains dozing, so we have to manually trigger
+            // the unblur earlier
 
             keyguardAnimator?.cancel()
             keyguardAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
@@ -261,6 +259,7 @@
                 })
                 start()
             }
+            alreadyUnblurredWhileDozing = statusBarStateController.dozeAmount != 0.0f
         }
 
         override fun onKeyguardShowingChanged() {
@@ -282,10 +281,24 @@
             if (isDozing) {
                 shadeAnimation.finishIfRunning()
                 brightnessMirrorSpring.finishIfRunning()
+
+                // unset this for safety, to be ready for the next wakeup
+                alreadyUnblurredWhileDozing = false
             }
         }
 
         override fun onDozeAmountChanged(linear: Float, eased: Float) {
+            if (alreadyUnblurredWhileDozing) {
+                if (linear == 0.0f) {
+                    // We finished waking up, let's reset
+                    alreadyUnblurredWhileDozing = false
+                } else {
+                    // We've already handled the unbluring from the keyguardAnimator above.
+                    // if we would continue, we'd play another unzoom / blur animation from the
+                    // dozing changing.
+                    return
+                }
+            }
             wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(eased)
             scheduleUpdate()
         }
@@ -443,6 +456,7 @@
             it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch")
             it.println("qsPanelExpansion: $qsPanelExpansion")
             it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress")
+            it.println("alreadyUnblurredWhileDozing: $alreadyUnblurredWhileDozing")
             it.println("lastAppliedBlur: $lastAppliedBlur")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index cd5cce4..0b93fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -31,7 +31,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
+import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -168,8 +168,8 @@
             viewState.clipTopAmount = 0;
 
             if (ambientState.isExpansionChanging() && !ambientState.isOnKeyguard()) {
-                viewState.alpha = Interpolators.getNotificationScrimAlpha(
-                        ambientState.getExpansionFraction(), true /* notification */);
+                float expansion = ambientState.getExpansionFraction();
+                viewState.alpha = ShadeInterpolation.getContentAlpha(expansion);
             } else {
                 viewState.alpha = 1f - ambientState.getHideAmount();
             }
@@ -183,7 +183,6 @@
             }
 
             viewState.hidden = !mAmbientState.isShadeExpanded()
-                    || mAmbientState.isQsCustomizerShowing()
                     || algorithmState.firstViewInShelf == null;
 
             final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameView.java b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameView.java
index cc7a4f8..4a6d7e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameView.java
@@ -15,46 +15,18 @@
 package com.android.systemui.statusbar;
 
 import android.content.Context;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.telephony.ServiceState;
-import android.telephony.SubscriptionInfo;
-import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.widget.TextView;
 
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.settingslib.WirelessUtils;
-import com.android.systemui.Dependency;
-import com.android.systemui.demomode.DemoModeCommandReceiver;
-import com.android.systemui.plugins.DarkIconDispatcher;
-import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
-import com.android.systemui.tuner.TunerService;
-import com.android.systemui.tuner.TunerService.Tunable;
 
 import java.util.List;
 
 /** Shows the operator name */
-public class OperatorNameView extends TextView implements DemoModeCommandReceiver, DarkReceiver,
-        SignalCallback, Tunable {
-
-    private static final String KEY_SHOW_OPERATOR_NAME = "show_operator_name";
-
-    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+public class OperatorNameView extends TextView {
     private boolean mDemoMode;
 
-    private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
-        @Override
-        public void onRefreshCarrierInfo() {
-            updateText();
-        }
-    };
-
     public OperatorNameView(Context context) {
         this(context, null);
     }
@@ -67,62 +39,14 @@
         super(context, attrs, defStyle);
     }
 
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
-        mKeyguardUpdateMonitor.registerCallback(mCallback);
-        Dependency.get(DarkIconDispatcher.class).addDarkReceiver(this);
-        Dependency.get(NetworkController.class).addCallback(this);
-        Dependency.get(TunerService.class).addTunable(this, KEY_SHOW_OPERATOR_NAME);
+    void setDemoMode(boolean demoMode) {
+        mDemoMode = demoMode;
     }
 
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        mKeyguardUpdateMonitor.removeCallback(mCallback);
-        Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(this);
-        Dependency.get(NetworkController.class).removeCallback(this);
-        Dependency.get(TunerService.class).removeTunable(this);
-    }
-
-    @Override
-    public void onDarkChanged(Rect area, float darkIntensity, int tint) {
-        setTextColor(DarkIconDispatcher.getTint(area, this, tint));
-    }
-
-    @Override
-    public void setIsAirplaneMode(IconState icon) {
-        update();
-    }
-
-    @Override
-    public void onTuningChanged(String key, String newValue) {
-        update();
-    }
-
-    @Override
-    public void dispatchDemoCommand(String command, Bundle args) {
-        setText(args.getString("name"));
-    }
-
-    @Override
-    public void onDemoModeStarted() {
-        mDemoMode = true;
-    }
-
-    @Override
-    public void onDemoModeFinished() {
-        mDemoMode = false;
-        update();
-    }
-
-    private void update() {
-        boolean showOperatorName = Dependency.get(TunerService.class)
-                .getValue(KEY_SHOW_OPERATOR_NAME, 1) != 0;
+    void update(boolean showOperatorName, boolean hasMobile,
+            List<OperatorNameViewController.SubInfo> subs) {
         setVisibility(showOperatorName ? VISIBLE : GONE);
 
-        boolean hasMobile = mContext.getSystemService(TelephonyManager.class).isDataCapable();
         boolean airplaneMode = WirelessUtils.isAirplaneModeOn(mContext);
         if (!hasMobile || airplaneMode) {
             setText(null);
@@ -131,22 +55,19 @@
         }
 
         if (!mDemoMode) {
-            updateText();
+            updateText(subs);
         }
     }
 
-    private void updateText() {
+    void updateText(List<OperatorNameViewController.SubInfo> subs) {
         CharSequence displayText = null;
-        List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(false);
         final int N = subs.size();
         for (int i = 0; i < N; i++) {
-            int subId = subs.get(i).getSubscriptionId();
-            int simState = mKeyguardUpdateMonitor.getSimState(subId);
+            OperatorNameViewController.SubInfo subInfo = subs.get(i);
             CharSequence carrierName = subs.get(i).getCarrierName();
-            if (!TextUtils.isEmpty(carrierName) && simState == TelephonyManager.SIM_STATE_READY) {
-                ServiceState ss = mKeyguardUpdateMonitor.getServiceState(subId);
-                if (ss != null && ss.getState() == ServiceState.STATE_IN_SERVICE) {
-                    displayText = carrierName;
+            if (!TextUtils.isEmpty(carrierName) && subInfo.simReady()) {
+                if (subInfo.stateInService()) {
+                    displayText = subInfo.getCarrierName();
                     break;
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java
new file mode 100644
index 0000000..bcba5cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.os.Bundle;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.TelephonyManager;
+import android.view.View;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.demomode.DemoModeCommandReceiver;
+import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.ViewController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/** Controller for {@link OperatorNameView}. */
+public class OperatorNameViewController extends ViewController<OperatorNameView> {
+    private static final String KEY_SHOW_OPERATOR_NAME = "show_operator_name";
+
+    private final DarkIconDispatcher mDarkIconDispatcher;
+    private final NetworkController mNetworkController;
+    private final TunerService mTunerService;
+    private final TelephonyManager mTelephonyManager;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+    private OperatorNameViewController(OperatorNameView view,
+            DarkIconDispatcher darkIconDispatcher,
+            NetworkController networkController,
+            TunerService tunerService,
+            TelephonyManager telephonyManager,
+            KeyguardUpdateMonitor keyguardUpdateMonitor) {
+        super(view);
+        mDarkIconDispatcher = darkIconDispatcher;
+        mNetworkController = networkController;
+        mTunerService = tunerService;
+        mTelephonyManager = telephonyManager;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+    }
+
+    @Override
+    protected void onViewAttached() {
+        mDarkIconDispatcher.addDarkReceiver(mDarkReceiver);
+        mNetworkController.addCallback(mSignalCallback);
+        mTunerService.addTunable(mTunable, KEY_SHOW_OPERATOR_NAME);
+        mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
+    }
+
+    @Override
+    protected void onViewDetached() {
+        mDarkIconDispatcher.removeDarkReceiver(mDarkReceiver);
+        mNetworkController.removeCallback(mSignalCallback);
+        mTunerService.removeTunable(mTunable);
+        mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
+    }
+
+    private void update() {
+        mView.update(mTunerService.getValue(KEY_SHOW_OPERATOR_NAME, 1) != 0,
+                mTelephonyManager.isDataCapable(), getSubInfos());
+    }
+
+    private List<SubInfo> getSubInfos() {
+        List<SubInfo> result = new ArrayList<>();
+        List<SubscriptionInfo> subscritionInfos =
+                mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(false);
+
+        for (SubscriptionInfo subscriptionInfo : subscritionInfos) {
+            int subId = subscriptionInfo.getSubscriptionId();
+            result.add(new SubInfo(
+                    subscriptionInfo.getCarrierName(),
+                    mKeyguardUpdateMonitor.getSimState(subId),
+                    mKeyguardUpdateMonitor.getServiceState(subId)));
+        }
+
+        return result;
+    }
+
+    /** Factory for constructing an {@link OperatorNameViewController}. */
+    public static class Factory {
+        private final DarkIconDispatcher mDarkIconDispatcher;
+        private final NetworkController mNetworkController;
+        private final TunerService mTunerService;
+        private final TelephonyManager mTelephonyManager;
+        private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+        @Inject
+        public Factory(DarkIconDispatcher darkIconDispatcher, NetworkController networkController,
+                TunerService tunerService, TelephonyManager telephonyManager,
+                KeyguardUpdateMonitor keyguardUpdateMonitor) {
+            mDarkIconDispatcher = darkIconDispatcher;
+            mNetworkController = networkController;
+            mTunerService = tunerService;
+            mTelephonyManager = telephonyManager;
+            mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        }
+
+        /** Create an {@link OperatorNameViewController}. */
+        public OperatorNameViewController create(OperatorNameView view) {
+            return new OperatorNameViewController(view, mDarkIconDispatcher, mNetworkController,
+                    mTunerService, mTelephonyManager, mKeyguardUpdateMonitor);
+        }
+    }
+
+    /**
+     * Needed because of how {@link CollapsedStatusBarFragment} works.
+     *
+     * Ideally this can be done internally.
+     **/
+    public View getView() {
+        return mView;
+    }
+
+    private final DarkIconDispatcher.DarkReceiver mDarkReceiver =
+            (area, darkIntensity, tint) ->
+                    mView.setTextColor(DarkIconDispatcher.getTint(area, mView, tint));
+
+    private final NetworkController.SignalCallback mSignalCallback =
+            new NetworkController.SignalCallback() {
+        @Override
+        public void setIsAirplaneMode(NetworkController.IconState icon) {
+            update();
+        }
+    };
+
+    private final TunerService.Tunable mTunable = (key, newValue) -> update();
+
+
+    private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+        @Override
+        public void onRefreshCarrierInfo() {
+            mView.updateText(getSubInfos());
+        }
+    };
+
+    // TODO: do we even register this anywhere?
+    private final DemoModeCommandReceiver mDemoModeCommandReceiver = new DemoModeCommandReceiver() {
+        @Override
+        public void onDemoModeStarted() {
+            mView.setDemoMode(true);
+        }
+
+        @Override
+        public void onDemoModeFinished() {
+            mView.setDemoMode(false);
+            update();
+        }
+
+        @Override
+        public void dispatchDemoCommand(String command, Bundle args) {
+            mView.setText(args.getString("name"));
+        }
+    };
+
+    static class SubInfo {
+        private final CharSequence mCarrierName;
+        private final int mSimState;
+        private final ServiceState mServiceState;
+
+        private SubInfo(CharSequence carrierName,
+                int simState, ServiceState serviceState) {
+            mCarrierName = carrierName;
+            mSimState = simState;
+            mServiceState = serviceState;
+        }
+
+        boolean simReady() {
+            return mSimState == TelephonyManager.SIM_STATE_READY;
+        }
+
+        CharSequence getCarrierName() {
+            return mCarrierName;
+        }
+
+        boolean stateInService() {
+            return mServiceState != null
+                    && mServiceState.getState() == ServiceState.STATE_IN_SERVICE;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
index 180f81c..83701a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
@@ -25,7 +25,6 @@
 import android.util.ArrayMap;
 import android.util.Pair;
 
-import com.android.internal.util.Preconditions;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.RemoteInputView;
@@ -245,6 +244,10 @@
         mCallbacks.add(callback);
     }
 
+    public void removeCallback(Callback callback) {
+        mCallbacks.remove(callback);
+    }
+
     public void remoteInputSent(NotificationEntry entry) {
         int N = mCallbacks.size();
         for (int i = 0; i < N; i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 0725bf9..6da981b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -16,6 +16,10 @@
 
 package com.android.systemui.statusbar;
 
+import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
+import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
+
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_TO_AOD;
 
@@ -23,10 +27,17 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
+import android.os.SystemProperties;
+import android.os.Trace;
 import android.text.format.DateFormat;
 import android.util.FloatProperty;
 import android.util.Log;
+import android.view.InsetsFlags;
+import android.view.InsetsVisibilities;
 import android.view.View;
+import android.view.ViewDebug;
+import android.view.WindowInsetsController.Appearance;
+import android.view.WindowInsetsController.Behavior;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
@@ -38,6 +49,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.policy.CallbackController;
@@ -53,9 +65,14 @@
  * Tracks and reports on {@link StatusBarState}.
  */
 @SysUISingleton
-public class StatusBarStateControllerImpl implements SysuiStatusBarStateController,
-        CallbackController<StateListener>, Dumpable {
+public class StatusBarStateControllerImpl implements
+        SysuiStatusBarStateController,
+        CallbackController<StateListener>,
+        Dumpable {
     private static final String TAG = "SbStateController";
+    private static final boolean DEBUG_IMMERSIVE_APPS =
+            SystemProperties.getBoolean("persist.debug.immersive_apps", false);
+
     // Must be a power of 2
     private static final int HISTORY_SIZE = 32;
 
@@ -133,11 +150,13 @@
     private Interpolator mDozeInterpolator = Interpolators.FAST_OUT_SLOW_IN;
 
     @Inject
-    public StatusBarStateControllerImpl(UiEventLogger uiEventLogger) {
+    public StatusBarStateControllerImpl(UiEventLogger uiEventLogger, DumpManager dumpManager) {
         mUiEventLogger = uiEventLogger;
         for (int i = 0; i < HISTORY_SIZE; i++) {
             mHistoricalRecords[i] = new HistoricalState();
         }
+
+        dumpManager.registerDumpable(this);
     }
 
     @Override
@@ -163,6 +182,7 @@
         }
 
         synchronized (mListeners) {
+            Trace.beginSection(TAG + "#setState(" + StatusBarState.toShortString(state) + ")");
             String tag = getClass().getSimpleName() + "#setState(" + state + ")";
             DejankUtils.startDetectingBlockingIpcs(tag);
             for (RankedListener rl : new ArrayList<>(mListeners)) {
@@ -180,6 +200,7 @@
                 rl.mListener.onStatePostChange();
             }
             DejankUtils.stopDetectingBlockingIpcs(tag);
+            Trace.endSection();
         }
 
         return true;
@@ -244,12 +265,14 @@
         mIsDozing = isDozing;
 
         synchronized (mListeners) {
+            Trace.beginSection(TAG + "#setDozing(" + isDozing + ")");
             String tag = getClass().getSimpleName() + "#setIsDozing";
             DejankUtils.startDetectingBlockingIpcs(tag);
             for (RankedListener rl : new ArrayList<>(mListeners)) {
                 rl.mListener.onDozingChanged(isDozing);
             }
             DejankUtils.stopDetectingBlockingIpcs(tag);
+            Trace.endSection();
         }
 
         return true;
@@ -315,12 +338,14 @@
         mDozeAmount = dozeAmount;
         float interpolatedAmount = mDozeInterpolator.getInterpolation(dozeAmount);
         synchronized (mListeners) {
+            Trace.beginSection(TAG + "#setDozeAmount");
             String tag = getClass().getSimpleName() + "#setDozeAmount";
             DejankUtils.startDetectingBlockingIpcs(tag);
             for (RankedListener rl : new ArrayList<>(mListeners)) {
                 rl.mListener.onDozeAmountChanged(mDozeAmount, interpolatedAmount);
             }
             DejankUtils.stopDetectingBlockingIpcs(tag);
+            Trace.endSection();
         }
     }
 
@@ -420,26 +445,44 @@
     }
 
     @Override
-    public void setFullscreenState(boolean isFullscreen) {
+    public void setSystemBarAttributes(@Appearance int appearance, @Behavior int behavior,
+            InsetsVisibilities requestedVisibilities, String packageName) {
+        boolean isFullscreen = !requestedVisibilities.getVisibility(ITYPE_STATUS_BAR)
+                || !requestedVisibilities.getVisibility(ITYPE_NAVIGATION_BAR);
         if (mIsFullscreen != isFullscreen) {
             mIsFullscreen = isFullscreen;
             synchronized (mListeners) {
                 for (RankedListener rl : new ArrayList<>(mListeners)) {
-                    rl.mListener.onFullscreenStateChanged(isFullscreen, true /* isImmersive */);
+                    rl.mListener.onFullscreenStateChanged(isFullscreen);
                 }
             }
         }
+
+        // TODO (b/190543382): Finish the logging logic.
+        // This section can be removed if we don't need to print it on logcat.
+        if (DEBUG_IMMERSIVE_APPS) {
+            boolean dim = (appearance & APPEARANCE_LOW_PROFILE_BARS) != 0;
+            String behaviorName = ViewDebug.flagsToString(InsetsFlags.class, "behavior", behavior);
+            String requestedVisibilityString = requestedVisibilities.toString();
+            if (requestedVisibilityString.isEmpty()) {
+                requestedVisibilityString = "none";
+            }
+            Log.d(TAG, packageName + " dim=" + dim + " behavior=" + behaviorName
+                    + " requested visibilities: " + requestedVisibilityString);
+        }
     }
 
     @Override
     public void setPulsing(boolean pulsing) {
         if (mPulsing != pulsing) {
             mPulsing = pulsing;
+            Trace.beginSection(TAG + "#setPulsing(" + pulsing + ")");
             synchronized (mListeners) {
                 for (RankedListener rl : new ArrayList<>(mListeners)) {
                     rl.mListener.onPulsingChanged(pulsing);
                 }
             }
+            Trace.endSection();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SuperStatusBarViewFactory.java b/packages/SystemUI/src/com/android/systemui/statusbar/SuperStatusBarViewFactory.java
deleted file mode 100644
index e4ae560..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SuperStatusBarViewFactory.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-
-import com.android.systemui.R;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
-import com.android.systemui.statusbar.phone.NotificationPanelView;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
-import com.android.systemui.statusbar.phone.StatusBarWindowView;
-import com.android.systemui.util.InjectionInflationController;
-
-import javax.inject.Inject;
-
-/**
- * Creates a single instance of super_status_bar and super_notification_shade that can be shared
- * across various system ui objects.
- */
-@SysUISingleton
-public class SuperStatusBarViewFactory {
-
-    private final Context mContext;
-    private final InjectionInflationController mInjectionInflationController;
-    private final NotificationShelfComponent.Builder mNotificationShelfComponentBuilder;
-
-    private NotificationShadeWindowView mNotificationShadeWindowView;
-    private StatusBarWindowView mStatusBarWindowView;
-    private NotificationShelfController mNotificationShelfController;
-
-    @Inject
-    public SuperStatusBarViewFactory(Context context,
-            InjectionInflationController injectionInflationController,
-            NotificationShelfComponent.Builder notificationShelfComponentBuilder) {
-        mContext = context;
-        mInjectionInflationController = injectionInflationController;
-        mNotificationShelfComponentBuilder = notificationShelfComponentBuilder;
-    }
-
-    /**
-     * Gets the inflated {@link NotificationShadeWindowView} from
-     * {@link R.layout#super_notification_shade}.
-     * Returns a cached instance, if it has already been inflated.
-     */
-    public NotificationShadeWindowView getNotificationShadeWindowView() {
-        if (mNotificationShadeWindowView != null) {
-            return mNotificationShadeWindowView;
-        }
-
-        mNotificationShadeWindowView = (NotificationShadeWindowView)
-                mInjectionInflationController.injectable(
-                LayoutInflater.from(mContext)).inflate(R.layout.super_notification_shade,
-                /* root= */ null);
-        if (mNotificationShadeWindowView == null) {
-            throw new IllegalStateException(
-                    "R.layout.super_notification_shade could not be properly inflated");
-        }
-
-        return mNotificationShadeWindowView;
-    }
-
-    /**
-     * Gets the inflated {@link StatusBarWindowView} from {@link R.layout#super_status_bar}.
-     * Returns a cached instance, if it has already been inflated.
-     */
-    public StatusBarWindowView getStatusBarWindowView() {
-        if (mStatusBarWindowView != null) {
-            return mStatusBarWindowView;
-        }
-
-        mStatusBarWindowView =
-                (StatusBarWindowView) mInjectionInflationController.injectable(
-                LayoutInflater.from(mContext)).inflate(R.layout.super_status_bar,
-                /* root= */ null);
-        if (mStatusBarWindowView == null) {
-            throw new IllegalStateException(
-                    "R.layout.super_status_bar could not be properly inflated");
-        }
-        return mStatusBarWindowView;
-    }
-
-    /**
-     * Gets the inflated {@link NotificationShelf} from
-     * {@link R.layout#status_bar_notification_shelf}.
-     * Returns a cached instance, if it has already been inflated.
-     *
-     * @param container the expected container to hold the {@link NotificationShelf}. The view
-     *                  isn't immediately attached, but the layout params of this view is used
-     *                  during inflation.
-     */
-    public NotificationShelfController getNotificationShelfController(ViewGroup container) {
-        if (mNotificationShelfController != null) {
-            return mNotificationShelfController;
-        }
-
-        NotificationShelf view = (NotificationShelf) LayoutInflater.from(mContext)
-                .inflate(R.layout.status_bar_notification_shelf, container, /* attachToRoot= */
-                        false);
-
-        if (view == null) {
-            throw new IllegalStateException(
-                    "R.layout.status_bar_notification_shelf could not be properly inflated");
-        }
-
-        NotificationShelfComponent component = mNotificationShelfComponentBuilder
-                .notificationShelf(view)
-                .build();
-        mNotificationShelfController = component.getNotificationShelfController();
-        mNotificationShelfController.init();
-
-        return mNotificationShelfController;
-    }
-
-    public NotificationPanelView getNotificationPanelView() {
-        NotificationShadeWindowView notificationShadeWindowView = getNotificationShadeWindowView();
-        if (notificationShadeWindowView == null) {
-            return null;
-        }
-
-        return mNotificationShadeWindowView.findViewById(R.id.notification_panel);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index 2520050..f0b2c2d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -19,7 +19,10 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.annotation.IntDef;
+import android.view.InsetsVisibilities;
 import android.view.View;
+import android.view.WindowInsetsController.Appearance;
+import android.view.WindowInsetsController.Behavior;
 
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -155,9 +158,10 @@
     boolean isKeyguardRequested();
 
     /**
-     * Set the fullscreen state
+     * Set the system bar attributes
      */
-    void setFullscreenState(boolean isFullscreen);
+    void setSystemBarAttributes(@Appearance int appearance, @Behavior int behavior,
+            InsetsVisibilities requestedVisibilities, String packageName);
 
     /**
      * Set pulsing
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
index 22bbb81b..04c60fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
@@ -29,7 +29,7 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.Utils
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.policy.BatteryController
@@ -114,9 +114,6 @@
             override fun onThemeChanged() {
                 updateRippleColor()
             }
-            override fun onOverlayChanged() {
-                updateRippleColor()
-            }
 
             override fun onConfigChanged(newConfig: Configuration?) {
                 normalizedPortPosX = context.resources.getFloat(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
index 6d6320e..a23d73d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import android.content.Context;
 import android.content.Intent;
@@ -55,6 +55,7 @@
 
 import javax.inject.Inject;
 
+/** */
 public class AccessPointControllerImpl
         implements NetworkController.AccessPointController,
         WifiPickerTracker.WifiPickerTrackerCallback, LifecycleOwner {
@@ -116,17 +117,19 @@
         super.finalize();
     }
 
+    /** */
     public boolean canConfigWifi() {
         return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI,
                 new UserHandle(mCurrentUser));
     }
 
+    /** */
     public boolean canConfigMobileData() {
         return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
                 UserHandle.of(mCurrentUser)) && mUserTracker.getUserInfo().isAdmin();
     }
 
-    public void onUserSwitched(int newUserId) {
+    void onUserSwitched(int newUserId) {
         mCurrentUser = newUserId;
     }
 
@@ -225,7 +228,7 @@
         }
     }
 
-    public void dump(PrintWriter pw) {
+    void dump(PrintWriter pw) {
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
         ipw.println("AccessPointControllerImpl:");
         ipw.increaseIndent();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/CallbackHandler.java
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/CallbackHandler.java
index 7ac6d63..052a789 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/CallbackHandler.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import android.os.Handler;
 import android.os.Looper;
@@ -22,11 +22,11 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.EmergencyListener;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
 
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
@@ -235,7 +235,7 @@
                 .append("icon=").append(icon)
                 .toString();
         recordLastCallback(log);
-        obtainMessage(MSG_ETHERNET_CHANGED, icon).sendToTarget();;
+        obtainMessage(MSG_ETHERNET_CHANGED, icon).sendToTarget();
     }
 
     @Override
@@ -252,14 +252,14 @@
                     .toString();
             recordLastCallback(log);
         }
-        obtainMessage(MSG_AIRPLANE_MODE_CHANGED, icon).sendToTarget();;
+        obtainMessage(MSG_AIRPLANE_MODE_CHANGED, icon).sendToTarget();
     }
 
-    public void setListening(EmergencyListener listener, boolean listening) {
+    void setListening(EmergencyListener listener, boolean listening) {
         obtainMessage(MSG_ADD_REMOVE_EMERGENCY, listening ? 1 : 0, 0, listener).sendToTarget();
     }
 
-    public void setListening(SignalCallback listener, boolean listening) {
+    void setListening(SignalCallback listener, boolean listening) {
         obtainMessage(MSG_ADD_REMOVE_SIGNAL, listening ? 1 : 0, 0, listener).sendToTarget();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/EthernetIcons.java
similarity index 93%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/EthernetIcons.java
index b391bd9..196aad9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/EthernetIcons.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import com.android.systemui.R;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/EthernetSignalController.java
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetSignalController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/EthernetSignalController.java
index 80b75a7..c9d40ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/EthernetSignalController.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import android.content.Context;
 import android.net.NetworkCapabilities;
@@ -21,12 +21,12 @@
 import com.android.settingslib.AccessibilityContentDescriptions;
 import com.android.settingslib.SignalIcon.IconGroup;
 import com.android.settingslib.SignalIcon.State;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
 
 import java.util.BitSet;
 
-
+/** */
 public class EthernetSignalController extends
         SignalController<State, IconGroup> {
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
index 43781f3..20ef4ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import static com.android.settingslib.mobile.MobileMappings.getDefaultIcons;
 import static com.android.settingslib.mobile.MobileMappings.getIconKey;
@@ -57,10 +57,10 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.SignalStrengthUtil;
 import com.android.systemui.R;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
 import com.android.systemui.util.CarrierConfigTracker;
 
 import java.io.PrintWriter;
@@ -91,8 +91,7 @@
     private int mImsType = IMS_TYPE_WWAN;
     // Save entire info for logging, we only use the id.
     final SubscriptionInfo mSubscriptionInfo;
-    // @VisibleForDemoMode
-    Map<String, MobileIconGroup> mNetworkToIconLookup;
+    private Map<String, MobileIconGroup> mNetworkToIconLookup;
 
     // Since some pieces of the phone state are interdependent we store it locally,
     // this could potentially become part of MobileState for simplification/complication
@@ -108,8 +107,6 @@
     private Config mConfig;
     @VisibleForTesting
     boolean mInflateSignalStrengths = false;
-    private MobileStatusTracker.Callback mCallback;
-    private RegistrationCallback mRegistrationCallback;
     private int mLastWwanLevel;
     private int mLastWlanLevel;
     private int mLastWlanCrossSimLevel;
@@ -121,6 +118,82 @@
     // Where to copy the next state into.
     private int mMobileStatusHistoryIndex;
 
+    private final MobileStatusTracker.Callback mMobileCallback =
+            new MobileStatusTracker.Callback() {
+                private String mLastStatus;
+
+                @Override
+                public void onMobileStatusChanged(boolean updateTelephony,
+                        MobileStatus mobileStatus) {
+                    if (Log.isLoggable(mTag, Log.DEBUG)) {
+                        Log.d(mTag, "onMobileStatusChanged="
+                                + " updateTelephony=" + updateTelephony
+                                + " mobileStatus=" + mobileStatus.toString());
+                    }
+                    String currentStatus = mobileStatus.toString();
+                    if (!currentStatus.equals(mLastStatus)) {
+                        mLastStatus = currentStatus;
+                        String status = new StringBuilder()
+                                .append(SSDF.format(System.currentTimeMillis())).append(",")
+                                .append(currentStatus)
+                                .toString();
+                        recordLastMobileStatus(status);
+                    }
+                    updateMobileStatus(mobileStatus);
+                    if (updateTelephony) {
+                        updateTelephony();
+                    } else {
+                        notifyListenersIfNecessary();
+                    }
+                }
+            };
+
+    private final RegistrationCallback mRegistrationCallback = new RegistrationCallback() {
+        @Override
+        public void onRegistered(ImsRegistrationAttributes attributes) {
+            Log.d(mTag, "onRegistered: " + "attributes=" + attributes);
+            int imsTransportType = attributes.getTransportType();
+            int registrationAttributes = attributes.getAttributeFlags();
+            if (imsTransportType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN) {
+                mImsType = IMS_TYPE_WWAN;
+                IconState statusIcon = new IconState(
+                        true,
+                        getCallStrengthIcon(mLastWwanLevel, /* isWifi= */false),
+                        getCallStrengthDescription(mLastWwanLevel, /* isWifi= */false));
+                notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
+            } else if (imsTransportType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
+                if (registrationAttributes == 0) {
+                    mImsType = IMS_TYPE_WLAN;
+                    IconState statusIcon = new IconState(
+                            true,
+                            getCallStrengthIcon(mLastWlanLevel, /* isWifi= */true),
+                            getCallStrengthDescription(mLastWlanLevel, /* isWifi= */true));
+                    notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
+                } else if (registrationAttributes
+                        == ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET) {
+                    mImsType = IMS_TYPE_WLAN_CROSS_SIM;
+                    IconState statusIcon = new IconState(
+                            true,
+                            getCallStrengthIcon(mLastWlanCrossSimLevel, /* isWifi= */false),
+                            getCallStrengthDescription(
+                                    mLastWlanCrossSimLevel, /* isWifi= */false));
+                    notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
+                }
+            }
+        }
+
+        @Override
+        public void onUnregistered(ImsReasonInfo info) {
+            Log.d(mTag, "onDeregistered: " + "info=" + info);
+            mImsType = IMS_TYPE_WWAN;
+            IconState statusIcon = new IconState(
+                    true,
+                    getCallStrengthIcon(mLastWwanLevel, /* isWifi= */false),
+                    getCallStrengthDescription(mLastWwanLevel, /* isWifi= */false));
+            notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
+        }
+    };
+
     // TODO: Reduce number of vars passed in, if we have the NetworkController, probably don't
     // need listener lists anymore.
     public MobileSignalController(
@@ -144,8 +217,8 @@
         mPhone = phone;
         mDefaults = defaults;
         mSubscriptionInfo = info;
-        mNetworkNameSeparator = getTextIfExists(R.string.status_bar_network_name_separator)
-                .toString();
+        mNetworkNameSeparator = getTextIfExists(
+                R.string.status_bar_network_name_separator).toString();
         mNetworkNameDefault = getTextIfExists(
                 com.android.internal.R.string.lockscreen_carrier_default).toString();
         mReceiverHandler = new Handler(receiverLooper);
@@ -165,88 +238,14 @@
                 updateTelephony();
             }
         };
-        mCallback = new MobileStatusTracker.Callback() {
-            private String mLastStatus;
-
-            @Override
-            public void onMobileStatusChanged(boolean updateTelephony,
-                    MobileStatus mobileStatus) {
-                if (Log.isLoggable(mTag, Log.DEBUG)) {
-                    Log.d(mTag, "onMobileStatusChanged="
-                            + " updateTelephony=" + updateTelephony
-                            + " mobileStatus=" + mobileStatus.toString());
-                }
-                String currentStatus = mobileStatus.toString();
-                if (!currentStatus.equals(mLastStatus)) {
-                    mLastStatus = currentStatus;
-                    String status = new StringBuilder()
-                            .append(SSDF.format(System.currentTimeMillis())).append(",")
-                            .append(currentStatus)
-                            .toString();
-                    recordLastMobileStatus(status);
-                }
-                updateMobileStatus(mobileStatus);
-                if (updateTelephony) {
-                    updateTelephony();
-                } else {
-                    notifyListenersIfNecessary();
-                }
-            }
-        };
-
-        mRegistrationCallback = new RegistrationCallback() {
-            @Override
-            public void onRegistered(ImsRegistrationAttributes attributes) {
-                Log.d(mTag, "onRegistered: " + "attributes=" + attributes);
-                int imsTransportType = attributes.getTransportType();
-                int registrationAttributes = attributes.getAttributeFlags();
-                if (imsTransportType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN) {
-                    mImsType = IMS_TYPE_WWAN;
-                    IconState statusIcon = new IconState(
-                            true,
-                            getCallStrengthIcon(mLastWwanLevel, /* isWifi= */false),
-                            getCallStrengthDescription(mLastWwanLevel, /* isWifi= */false));
-                    notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
-                } else if (imsTransportType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
-                    if (registrationAttributes == 0) {
-                        mImsType = IMS_TYPE_WLAN;
-                        IconState statusIcon = new IconState(
-                                true,
-                                getCallStrengthIcon(mLastWlanLevel, /* isWifi= */true),
-                                getCallStrengthDescription(mLastWlanLevel, /* isWifi= */true));
-                        notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
-                    } else if (registrationAttributes
-                            == ImsRegistrationAttributes.ATTR_EPDG_OVER_CELL_INTERNET) {
-                        mImsType = IMS_TYPE_WLAN_CROSS_SIM;
-                        IconState statusIcon = new IconState(
-                                true,
-                                getCallStrengthIcon(mLastWlanCrossSimLevel, /* isWifi= */false),
-                                getCallStrengthDescription(
-                                        mLastWlanCrossSimLevel, /* isWifi= */false));
-                        notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
-                    }
-                }
-            }
-
-            @Override
-            public void onUnregistered(ImsReasonInfo info) {
-                Log.d(mTag, "onDeregistered: " + "info=" + info);
-                mImsType = IMS_TYPE_WWAN;
-                IconState statusIcon = new IconState(
-                        true,
-                        getCallStrengthIcon(mLastWwanLevel, /* isWifi= */false),
-                        getCallStrengthDescription(mLastWwanLevel, /* isWifi= */false));
-                notifyCallStateChange(statusIcon, mSubscriptionInfo.getSubscriptionId());
-            }
-        };
         mImsMmTelManager = ImsMmTelManager.createForSubscriptionId(info.getSubscriptionId());
         mMobileStatusTracker = new MobileStatusTracker(mPhone, receiverLooper,
-                info, mDefaults, mCallback);
+                info, mDefaults, mMobileCallback);
         mProviderModelBehavior = featureFlags.isCombinedStatusBarSignalIconsEnabled();
         mProviderModelSetting = featureFlags.isProviderModelSettingEnabled();
     }
 
-    public void setConfiguration(Config config) {
+    void setConfiguration(Config config) {
         mConfig = config;
         updateInflateSignalStrength();
         mNetworkToIconLookup = mapIconSets(mConfig);
@@ -254,12 +253,12 @@
         updateTelephony();
     }
 
-    public void setAirplaneMode(boolean airplaneMode) {
+    void setAirplaneMode(boolean airplaneMode) {
         mCurrentState.airplaneMode = airplaneMode;
         notifyListenersIfNecessary();
     }
 
-    public void setUserSetupComplete(boolean userSetup) {
+    void setUserSetupComplete(boolean userSetup) {
         mCurrentState.userSetup = userSetup;
         notifyListenersIfNecessary();
     }
@@ -273,7 +272,7 @@
         notifyListenersIfNecessary();
     }
 
-    public void setCarrierNetworkChangeMode(boolean carrierNetworkChangeMode) {
+    void setCarrierNetworkChangeMode(boolean carrierNetworkChangeMode) {
         mCurrentState.carrierNetworkChangeMode = carrierNetworkChangeMode;
         updateTelephony();
     }
@@ -385,89 +384,83 @@
         if (mCurrentState.inetCondition == 0) {
             dataContentDescription = mContext.getString(R.string.data_connection_no_internet);
         }
-        final boolean dataDisabled = (mCurrentState.iconGroup == TelephonyIcons.DATA_DISABLED
-                || (mCurrentState.iconGroup == TelephonyIcons.NOT_DEFAULT_DATA))
-                && mCurrentState.userSetup;
+
+        final QsInfo qsInfo = getQsInfo(contentDescription, icons.dataType);
+        final SbInfo sbInfo = getSbInfo(contentDescription, icons.dataType);
+
+        MobileDataIndicators mobileDataIndicators = new MobileDataIndicators(
+                sbInfo.icon,
+                qsInfo.icon,
+                sbInfo.ratTypeIcon,
+                qsInfo.ratTypeIcon,
+                mCurrentState.hasActivityIn(),
+                mCurrentState.hasActivityOut(),
+                dataContentDescription,
+                dataContentDescriptionHtml,
+                qsInfo.description,
+                mSubscriptionInfo.getSubscriptionId(),
+                mCurrentState.roaming,
+                sbInfo.showTriangle);
+        callback.setMobileDataIndicators(mobileDataIndicators);
+    }
+
+    private QsInfo getQsInfo(String contentDescription, int dataTypeIcon) {
+        int qsTypeIcon = 0;
+        IconState qsIcon = null;
+        CharSequence qsDescription = null;
+
+        boolean pm = mProviderModelSetting || mProviderModelBehavior;
+        if (mCurrentState.dataSim) {
+            // If using provider model behavior, only show QS icons if the state is also default
+            if (pm && !mCurrentState.isDefault) {
+                return new QsInfo(qsTypeIcon, qsIcon, qsDescription);
+            }
+
+            if (mCurrentState.showQuickSettingsRatIcon() || mConfig.alwaysShowDataRatIcon) {
+                qsTypeIcon = dataTypeIcon;
+            }
+
+            boolean qsIconVisible = mCurrentState.enabled && !mCurrentState.isEmergency;
+            qsIcon = new IconState(qsIconVisible, getQsCurrentIconId(), contentDescription);
+
+            if (!mCurrentState.isEmergency) {
+                qsDescription = mCurrentState.networkName;
+            }
+        }
+
+        return new QsInfo(qsTypeIcon, qsIcon, qsDescription);
+    }
+
+    private SbInfo getSbInfo(String contentDescription, int dataTypeIcon) {
+        final boolean dataDisabled = mCurrentState.isDataDisabledOrNotDefault();
+        boolean showTriangle = false;
+        int typeIcon = 0;
+        IconState statusIcon = null;
 
         if (mProviderModelBehavior) {
-            // Show icon in QS when we are connected or data is disabled.
-            boolean showDataIcon = mCurrentState.dataConnected || dataDisabled;
-
-            int qsTypeIcon = 0;
-            IconState qsIcon = null;
-            CharSequence description = null;
-            // Only send data sim callbacks to QS.
-            if (mCurrentState.dataSim && mCurrentState.isDefault) {
-                qsTypeIcon =
-                        (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.qsDataType : 0;
-                qsIcon = new IconState(mCurrentState.enabled
-                        && !mCurrentState.isEmergency, getQsCurrentIconId(), contentDescription);
-                description = mCurrentState.isEmergency ? null : mCurrentState.networkName;
-            }
-            boolean activityIn = mCurrentState.dataConnected
-                    && !mCurrentState.carrierNetworkChangeMode
-                    && mCurrentState.activityIn;
-            boolean activityOut = mCurrentState.dataConnected
-                    && !mCurrentState.carrierNetworkChangeMode
-                    && mCurrentState.activityOut;
-            showDataIcon &= mCurrentState.dataSim && mCurrentState.isDefault;
-            boolean showTriangle = showDataIcon && !mCurrentState.airplaneMode;
-            int typeIcon = (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.dataType : 0;
-            showDataIcon |= mCurrentState.roaming;
-            IconState statusIcon = new IconState(showDataIcon && !mCurrentState.airplaneMode,
+            boolean showDataIconStatusBar = (mCurrentState.dataConnected || dataDisabled)
+                    && (mCurrentState.dataSim && mCurrentState.isDefault);
+            typeIcon =
+                    (showDataIconStatusBar || mConfig.alwaysShowDataRatIcon) ? dataTypeIcon : 0;
+            showDataIconStatusBar |= mCurrentState.roaming;
+            statusIcon = new IconState(
+                    showDataIconStatusBar && !mCurrentState.airplaneMode,
                     getCurrentIconId(), contentDescription);
-            MobileDataIndicators mobileDataIndicators = new MobileDataIndicators(
-                    statusIcon, qsIcon, typeIcon, qsTypeIcon,
-                    activityIn, activityOut, dataContentDescription, dataContentDescriptionHtml,
-                    description, icons.isWide, mSubscriptionInfo.getSubscriptionId(),
-                    mCurrentState.roaming, showTriangle);
-            callback.setMobileDataIndicators(mobileDataIndicators);
+
+            showTriangle = showDataIconStatusBar && !mCurrentState.airplaneMode;
         } else {
-            boolean showDataIcon = mCurrentState.dataConnected || dataDisabled;
-            IconState statusIcon = new IconState(
+            statusIcon = new IconState(
                     mCurrentState.enabled && !mCurrentState.airplaneMode,
                     getCurrentIconId(), contentDescription);
 
-            int qsTypeIcon = 0;
-            IconState qsIcon = null;
-            CharSequence description = null;
-            // Only send data sim callbacks to QS.
-            if (mProviderModelSetting) {
-                if (mCurrentState.dataSim && mCurrentState.isDefault) {
-                    qsTypeIcon =
-                            (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.qsDataType : 0;
-                    qsIcon = new IconState(
-                            mCurrentState.enabled && !mCurrentState.isEmergency,
-                            getQsCurrentIconId(), contentDescription);
-                    description = mCurrentState.isEmergency ? null : mCurrentState.networkName;
-                }
-            } else {
-                if (mCurrentState.dataSim) {
-                    qsTypeIcon =
-                            (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.qsDataType : 0;
-                    qsIcon = new IconState(
-                            mCurrentState.enabled && !mCurrentState.isEmergency,
-                            getQsCurrentIconId(), contentDescription);
-                    description = mCurrentState.isEmergency ? null : mCurrentState.networkName;
-                }
-            }
-
-            boolean activityIn = mCurrentState.dataConnected
-                    && !mCurrentState.carrierNetworkChangeMode
-                    && mCurrentState.activityIn;
-            boolean activityOut = mCurrentState.dataConnected
-                    && !mCurrentState.carrierNetworkChangeMode
-                    && mCurrentState.activityOut;
-            showDataIcon &= mCurrentState.isDefault || dataDisabled;
-            int typeIcon = (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.dataType : 0;
-            boolean showTriangle = mCurrentState.enabled && !mCurrentState.airplaneMode;
-            MobileDataIndicators mobileDataIndicators = new MobileDataIndicators(
-                    statusIcon, qsIcon, typeIcon, qsTypeIcon,
-                    activityIn, activityOut, dataContentDescription, dataContentDescriptionHtml,
-                    description, icons.isWide, mSubscriptionInfo.getSubscriptionId(),
-                    mCurrentState.roaming, showTriangle);
-            callback.setMobileDataIndicators(mobileDataIndicators);
+            boolean showDataIconInStatusBar =
+                    (mCurrentState.dataConnected && mCurrentState.isDefault) || dataDisabled;
+            typeIcon =
+                    (showDataIconInStatusBar || mConfig.alwaysShowDataRatIcon) ? dataTypeIcon : 0;
+            showTriangle = mCurrentState.enabled && !mCurrentState.airplaneMode;
         }
+
+        return new SbInfo(showTriangle, typeIcon, statusIcon);
     }
 
     @Override
@@ -508,7 +501,7 @@
         return mCurrentState.carrierNetworkChangeMode;
     }
 
-    public void handleBroadcast(Intent intent) {
+    void handleBroadcast(Intent intent) {
         String action = intent.getAction();
         if (action.equals(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)) {
             updateNetworkName(intent.getBooleanExtra(TelephonyManager.EXTRA_SHOW_SPN, false),
@@ -747,10 +740,10 @@
      * mTelephonyDisplayInfo, and mSimState.  It should be called any time one of these is updated.
      * This will call listeners if necessary.
      */
-    private final void updateTelephony() {
+    private void updateTelephony() {
         if (Log.isLoggable(mTag, Log.DEBUG)) {
-            Log.d(mTag, "updateTelephonySignalStrength: hasService=" +
-                    Utils.isInService(mServiceState) + " ss=" + mSignalStrength
+            Log.d(mTag, "updateTelephonySignalStrength: hasService="
+                    + Utils.isInService(mServiceState) + " ss=" + mSignalStrength
                     + " displayInfo=" + mTelephonyDisplayInfo);
         }
         checkDefaultData();
@@ -841,12 +834,15 @@
     public void dump(PrintWriter pw) {
         super.dump(pw);
         pw.println("  mSubscription=" + mSubscriptionInfo + ",");
+        pw.println("  mProviderModelSetting=" + mProviderModelSetting + ",");
+        pw.println("  mProviderModelBehavior=" + mProviderModelBehavior + ",");
         pw.println("  mServiceState=" + mServiceState + ",");
         pw.println("  mSignalStrength=" + mSignalStrength + ",");
         pw.println("  mTelephonyDisplayInfo=" + mTelephonyDisplayInfo + ",");
         pw.println("  mDataState=" + mDataState + ",");
         pw.println("  mInflateSignalStrengths=" + mInflateSignalStrengths + ",");
         pw.println("  isDataDisabled=" + isDataDisabled() + ",");
+        pw.println("  mNetworkToIconLookup=" + mNetworkToIconLookup + ",");
         pw.println("  MobileStatusHistory");
         int size = 0;
         for (int i = 0; i < STATUS_HISTORY_SIZE; i++) {
@@ -862,4 +858,31 @@
                     + mMobileStatusHistory[i & (STATUS_HISTORY_SIZE - 1)]);
         }
     }
+
+    /** Box for QS icon info */
+    private static final class QsInfo {
+        final int ratTypeIcon;
+        final IconState icon;
+        final CharSequence description;
+
+        QsInfo(int typeIcon, IconState iconState, CharSequence desc) {
+            ratTypeIcon = typeIcon;
+            icon = iconState;
+            description = desc;
+        }
+    }
+
+    /** Box for StatusBar icon info */
+    private static final class SbInfo {
+        final boolean showTriangle;
+        final int ratTypeIcon;
+        final IconState icon;
+
+        SbInfo(boolean show, int typeIcon, IconState iconState) {
+            showTriangle = show;
+            ratTypeIcon = typeIcon;
+            icon = iconState;
+        }
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
index eeea699..1433096 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import android.content.Context;
 import android.content.Intent;
@@ -22,29 +22,44 @@
 
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.demomode.DemoMode;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.policy.CallbackController;
+import com.android.systemui.statusbar.policy.DataSaverController;
 import com.android.wifitrackerlib.MergedCarrierEntry;
 import com.android.wifitrackerlib.WifiEntry;
 
 import java.util.List;
 
+/** */
 public interface NetworkController extends CallbackController<SignalCallback>, DemoMode {
-
+    /** */
     boolean hasMobileDataFeature();
+    /** */
     void setWifiEnabled(boolean enabled);
+    /** */
     AccessPointController getAccessPointController();
+    /** */
     DataUsageController getMobileDataController();
+    /** */
     DataSaverController getDataSaverController();
+    /** */
     String getMobileDataNetworkName();
+    /** */
     boolean isMobileDataNetworkInService();
+    /** */
     int getNumberSubscriptions();
 
+    /** */
     boolean hasVoiceCallingFeature();
 
+    /** */
     void addEmergencyListener(EmergencyListener listener);
+    /** */
     void removeEmergencyListener(EmergencyListener listener);
+    /** */
     boolean hasEmergencyCryptKeeperText();
 
+    /** */
     boolean isRadioOn();
 
     /**
@@ -81,7 +96,7 @@
                 .append(",qsIcon=").append(qsIcon == null ? "" : qsIcon.toString())
                 .append(",activityIn=").append(activityIn)
                 .append(",activityOut=").append(activityOut)
-                .append(",description=").append(description)
+                .append(",qsDescription=").append(description)
                 .append(",isTransient=").append(isTransient)
                 .append(",statusLabel=").append(statusLabel)
                 .append(']').toString();
@@ -100,8 +115,7 @@
         public boolean activityOut;
         public CharSequence typeContentDescription;
         public CharSequence typeContentDescriptionHtml;
-        public CharSequence description;
-        public boolean isWide;
+        public CharSequence qsDescription;
         public int subId;
         public boolean roaming;
         public boolean showTriangle;
@@ -109,7 +123,7 @@
         public MobileDataIndicators(IconState statusIcon, IconState qsIcon, int statusType,
                 int qsType, boolean activityIn, boolean activityOut,
                 CharSequence typeContentDescription, CharSequence typeContentDescriptionHtml,
-                CharSequence description, boolean isWide, int subId, boolean roaming,
+                CharSequence qsDescription, int subId, boolean roaming,
                 boolean showTriangle) {
             this.statusIcon = statusIcon;
             this.qsIcon = qsIcon;
@@ -119,8 +133,7 @@
             this.activityOut = activityOut;
             this.typeContentDescription = typeContentDescription;
             this.typeContentDescriptionHtml = typeContentDescriptionHtml;
-            this.description = description;
-            this.isWide = isWide;
+            this.qsDescription = qsDescription;
             this.subId = subId;
             this.roaming = roaming;
             this.showTriangle = showTriangle;
@@ -137,8 +150,7 @@
                 .append(",activityOut=").append(activityOut)
                 .append(",typeContentDescription=").append(typeContentDescription)
                 .append(",typeContentDescriptionHtml=").append(typeContentDescriptionHtml)
-                .append(",description=").append(description)
-                .append(",isWide=").append(isWide)
+                .append(",description=").append(qsDescription)
                 .append(",subId=").append(subId)
                 .append(",roaming=").append(roaming)
                 .append(",showTriangle=").append(showTriangle)
@@ -146,7 +158,8 @@
         }
     }
 
-    public interface SignalCallback {
+    /** */
+    interface SignalCallback {
         /**
          * Callback for listeners to be able to update the state of any UI tracking connectivity of
          * WiFi networks.
@@ -159,14 +172,19 @@
          */
         default void setMobileDataIndicators(MobileDataIndicators mobileDataIndicators) {}
 
+        /** */
         default void setSubs(List<SubscriptionInfo> subs) {}
 
+        /** */
         default void setNoSims(boolean show, boolean simDetected) {}
 
+        /** */
         default void setEthernetIndicators(IconState icon) {}
 
+        /** */
         default void setIsAirplaneMode(IconState icon) {}
 
+        /** */
         default void setMobileDataEnabled(boolean enabled) {}
 
         /**
@@ -186,11 +204,13 @@
         default void setCallIndicator(IconState statusIcon, int subId) {}
     }
 
-    public interface EmergencyListener {
+    /** */
+    interface EmergencyListener {
         void setEmergencyCallsOnly(boolean emergencyOnly);
     }
 
-    public static class IconState {
+    /** */
+    class IconState {
         public final boolean visible;
         public final int icon;
         public final String contentDescription;
@@ -220,7 +240,7 @@
      * Tracks changes in access points.  Allows listening for changes, scanning for new APs,
      * and connecting to new ones.
      */
-    public interface AccessPointController {
+    interface AccessPointController {
         void addAccessPointCallback(AccessPointCallback callback);
         void removeAccessPointCallback(AccessPointCallback callback);
         void scanForAccessPoints();
@@ -230,7 +250,7 @@
         boolean canConfigWifi();
         boolean canConfigMobileData();
 
-        public interface AccessPointCallback {
+        interface AccessPointCallback {
             void onAccessPointsChanged(List<WifiEntry> accessPoints);
             void onSettingsActivityTriggered(Intent settingsIntent);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index af0d413..d9b0ee7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -72,11 +72,16 @@
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.qs.tiles.dialog.InternetDialogUtil;
 import com.android.systemui.settings.CurrentUserTracker;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.DataSaverControllerImpl;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.statusbar.policy.EncryptionHelper;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.CarrierConfigTracker;
 
@@ -226,9 +231,9 @@
             DemoModeController demoModeController,
             CarrierConfigTracker carrierConfigTracker,
             FeatureFlags featureFlags,
-            DumpManager dumpManager,
             @Main Handler handler,
-            InternetDialogFactory internetDialogFactory) {
+            InternetDialogFactory internetDialogFactory,
+            DumpManager dumpManager) {
         this(context, connectivityManager,
                 telephonyManager,
                 telephonyListenerManager,
@@ -376,11 +381,10 @@
 
             @Override
             public void onCapabilitiesChanged(
-                Network network, NetworkCapabilities networkCapabilities) {
-                boolean lastValidated = (mLastNetworkCapabilities != null) &&
-                    mLastNetworkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED);
-                boolean validated =
-                    networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED);
+                    Network network, NetworkCapabilities networkCapabilities) {
+                boolean lastValidated = (mLastNetworkCapabilities != null)
+                        && mLastNetworkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED);
+                boolean validated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED);
 
                 // This callback is invoked a lot (i.e. when RSSI changes), so avoid updating
                 // icons when connectivity state has remained the same.
@@ -544,19 +548,23 @@
         return mDataUsageController;
     }
 
+    /** */
     public void addEmergencyListener(EmergencyListener listener) {
         mCallbackHandler.setListening(listener, true);
         mCallbackHandler.setEmergencyCallsOnly(isEmergencyOnly());
     }
 
+    /** */
     public void removeEmergencyListener(EmergencyListener listener) {
         mCallbackHandler.setListening(listener, false);
     }
 
+    /** */
     public boolean hasMobileDataFeature() {
         return mHasMobileDataFeature;
     }
 
+    /** */
     public boolean hasVoiceCallingFeature() {
         return mPhone.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE;
     }
@@ -661,7 +669,7 @@
         }
     }
 
-    public boolean isEmergencyOnly() {
+    boolean isEmergencyOnly() {
         if (mMobileSignalControllers.size() == 0) {
             // When there are no active subscriptions, determine emengency state from last
             // broadcast.
@@ -690,8 +698,10 @@
         if (mMobileSignalControllers.size() == 1) {
             mEmergencySource = EMERGENCY_ASSUMED_VOICE_CONTROLLER
                     + mMobileSignalControllers.keyAt(0);
-            if (DEBUG) Log.d(TAG, "Getting assumed emergency from "
-                    + mMobileSignalControllers.keyAt(0));
+            if (DEBUG)  {
+                Log.d(TAG, "Getting assumed emergency from "
+                        + mMobileSignalControllers.keyAt(0));
+            }
             return mMobileSignalControllers.valueAt(0).getState().isEmergency;
         }
         if (DEBUG) Log.e(TAG, "Cannot find controller for voice sub: " + voiceSubId);
@@ -915,7 +925,7 @@
 
     @GuardedBy("mLock")
     @VisibleForTesting
-    public void setCurrentSubscriptionsLocked(List<SubscriptionInfo> subscriptions) {
+    void setCurrentSubscriptionsLocked(List<SubscriptionInfo> subscriptions) {
         Collections.sort(subscriptions, new Comparator<SubscriptionInfo>() {
             @Override
             public int compare(SubscriptionInfo lhs, SubscriptionInfo rhs) {
@@ -1125,6 +1135,7 @@
         mEthernetSignalController.updateConnectivity(mConnectedTransports, mValidatedTransports);
     }
 
+    /** */
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("NetworkController state:");
 
@@ -1178,7 +1189,7 @@
         mCallbackHandler.dump(pw);
     }
 
-    private static final String emergencyToString(int emergencySource) {
+    private static String emergencyToString(int emergencySource) {
         if (emergencySource > EMERGENCY_NO_SUB) {
             return "ASSUMED_VOICE_CONTROLLER(" + (emergencySource - EMERGENCY_VOICE_CONTROLLER)
                     + ")";
@@ -1423,10 +1434,12 @@
         return info;
     }
 
+    /** */
     public boolean hasEmergencyCryptKeeperText() {
         return EncryptionHelper.IS_DATA_ENCRYPTED;
     }
 
+    /** */
     public boolean isRadioOn() {
         return !mAirplaneMode;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalController.java
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalController.java
index 4b6722c..d23dba5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalController.java
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
-import static com.android.systemui.statusbar.policy.NetworkControllerImpl.TAG;
+import static com.android.systemui.statusbar.connectivity.NetworkControllerImpl.TAG;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -23,8 +23,8 @@
 
 import com.android.settingslib.SignalIcon.IconGroup;
 import com.android.settingslib.SignalIcon.State;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
 
 import java.io.PrintWriter;
 import java.util.BitSet;
@@ -83,7 +83,7 @@
         return mCurrentState;
     }
 
-    public void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
+    void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) {
         mCurrentState.inetCondition = validatedTransports.get(mTransportType) ? 1 : 0;
         notifyListenersIfNecessary();
     }
@@ -114,7 +114,7 @@
         return false;
     }
 
-    public void saveLastState() {
+    void saveLastState() {
         if (RECORD_HISTORY) {
             recordLastState();
         }
@@ -161,7 +161,7 @@
         }
     }
 
-    public void notifyListenersIfNecessary() {
+    void notifyListenersIfNecessary() {
         if (isDirty()) {
             saveLastState();
             notifyListeners();
@@ -192,7 +192,7 @@
         mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE;
     }
 
-    public void dump(PrintWriter pw) {
+    void dump(PrintWriter pw) {
         pw.println("  - " + mTag + " -----");
         pw.println("  Current State: " + mCurrentState);
         if (RECORD_HISTORY) {
@@ -210,7 +210,7 @@
         }
     }
 
-    public final void notifyListeners() {
+    final void notifyListeners() {
         notifyListeners(mCallbackHandler);
     }
 
@@ -219,7 +219,7 @@
      * based on current state, and only need to be called in the scenario where
      * mCurrentState != mLastState.
      */
-    public abstract void notifyListeners(SignalCallback callback);
+    abstract void notifyListeners(SignalCallback callback);
 
     /**
      * Generate a blank T.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiIcons.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
index 577cc4f..3c449ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
@@ -14,12 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import com.android.settingslib.AccessibilityContentDescriptions;
 import com.android.settingslib.R;
 import com.android.settingslib.SignalIcon.IconGroup;
 
+/** */
 public class WifiIcons {
 
     static final int[] WIFI_FULL_ICONS = {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java
index f8e3647..3622a66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT;
@@ -36,15 +36,16 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.wifi.WifiStatusTracker;
 import com.android.systemui.R;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
 
 import java.io.PrintWriter;
 import java.util.Objects;
 
+/** */
 public class WifiSignalController extends
         SignalController<WifiSignalController.WifiState, IconGroup> {
     private final boolean mHasMobileDataFeature;
@@ -163,7 +164,7 @@
         int qsTypeIcon = 0;
         IconState qsIcon = null;
         if (sbVisible) {
-            qsTypeIcon = icons.qsDataType;
+            qsTypeIcon = icons.dataType;
             qsIcon = new IconState(mCurrentState.connected, getQsCurrentIconIdForCarrierWifi(),
                     contentDescription);
         }
@@ -172,7 +173,7 @@
         MobileDataIndicators mobileDataIndicators = new MobileDataIndicators(
                 statusIcon, qsIcon, typeIcon, qsTypeIcon,
                 mCurrentState.activityIn, mCurrentState.activityOut, dataContentDescription,
-                dataContentDescriptionHtml, description, icons.isWide,
+                dataContentDescriptionHtml, description,
                 mCurrentState.subId, /* roaming= */ false, /* showTriangle= */ true
         );
         callback.setMobileDataIndicators(mobileDataIndicators);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 4919593..d297d95 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -22,14 +22,19 @@
 import android.os.Handler;
 
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.qs.carrier.QSCarrierGroupController;
 import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.MediaArtworkProcessor;
 import com.android.systemui.statusbar.NotificationClickNotifier;
 import com.android.systemui.statusbar.NotificationListener;
@@ -42,6 +47,7 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
 import com.android.systemui.statusbar.notification.DynamicChildBindController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
@@ -60,11 +66,12 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
+import com.android.systemui.statusbar.phone.StatusBarWindowController;
+import com.android.systemui.statusbar.phone.SystemUIHostDialogProvider;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.tracing.ProtoTracer;
-import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -92,23 +99,25 @@
             NotificationLockscreenUserManager lockscreenUserManager,
             SmartReplyController smartReplyController,
             NotificationEntryManager notificationEntryManager,
-            Lazy<StatusBar> statusBarLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             StatusBarStateController statusBarStateController,
             Handler mainHandler,
             RemoteInputUriController remoteInputUriController,
             NotificationClickNotifier clickNotifier,
-            ActionClickLogger actionClickLogger) {
+            ActionClickLogger actionClickLogger,
+            DumpManager dumpManager) {
         return new NotificationRemoteInputManager(
                 context,
                 lockscreenUserManager,
                 smartReplyController,
                 notificationEntryManager,
-                statusBarLazy,
+                statusBarOptionalLazy,
                 statusBarStateController,
                 mainHandler,
                 remoteInputUriController,
                 clickNotifier,
-                actionClickLogger);
+                actionClickLogger,
+                dumpManager);
     }
 
     /** */
@@ -116,7 +125,7 @@
     @Provides
     static NotificationMediaManager provideNotificationMediaManager(
             Context context,
-            Lazy<StatusBar> statusBarLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             Lazy<NotificationShadeWindowController> notificationShadeWindowController,
             NotificationEntryManager notificationEntryManager,
             MediaArtworkProcessor mediaArtworkProcessor,
@@ -125,11 +134,11 @@
             NotifCollection notifCollection,
             FeatureFlags featureFlags,
             @Main DelayableExecutor mainExecutor,
-            DeviceConfigProxy deviceConfigProxy,
-            MediaDataManager mediaDataManager) {
+            MediaDataManager mediaDataManager,
+            DumpManager dumpManager) {
         return new NotificationMediaManager(
                 context,
-                statusBarLazy,
+                statusBarOptionalLazy,
                 notificationShadeWindowController,
                 notificationEntryManager,
                 mediaArtworkProcessor,
@@ -138,8 +147,8 @@
                 notifCollection,
                 featureFlags,
                 mainExecutor,
-                deviceConfigProxy,
-                mediaDataManager);
+                mediaDataManager,
+                dumpManager);
     }
 
     /** */
@@ -245,12 +254,63 @@
             ActivityStarter activityStarter,
             @Main Executor mainExecutor,
             IActivityManager iActivityManager,
-            OngoingCallLogger logger) {
+            OngoingCallLogger logger,
+            DumpManager dumpManager,
+            StatusBarWindowController statusBarWindowController,
+            SwipeStatusBarAwayGestureHandler swipeStatusBarAwayGestureHandler,
+            StatusBarStateController statusBarStateController) {
+        Optional<StatusBarWindowController> windowController =
+                featureFlags.isOngoingCallInImmersiveEnabled()
+                        ? Optional.of(statusBarWindowController)
+                        : Optional.empty();
+        Optional<SwipeStatusBarAwayGestureHandler> gestureHandler =
+                featureFlags.isOngoingCallInImmersiveEnabled()
+                        ? Optional.of(swipeStatusBarAwayGestureHandler)
+                        : Optional.empty();
         OngoingCallController ongoingCallController =
                 new OngoingCallController(
-                        notifCollection, featureFlags, systemClock, activityStarter, mainExecutor,
-                        iActivityManager, logger);
+                        notifCollection,
+                        featureFlags,
+                        systemClock,
+                        activityStarter,
+                        mainExecutor,
+                        iActivityManager,
+                        logger,
+                        dumpManager,
+                        windowController,
+                        gestureHandler,
+                        statusBarStateController);
         ongoingCallController.init();
         return ongoingCallController;
     }
+
+    /** */
+    @Binds
+    QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
+            QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl);
+
+    /**
+     */
+    @Provides
+    @SysUISingleton
+    static LaunchAnimator provideLaunchAnimator(Context context) {
+        return new LaunchAnimator(context);
+    }
+
+    /**
+     */
+    @Provides
+    @SysUISingleton
+    static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) {
+        return new ActivityLaunchAnimator(launchAnimator);
+    }
+
+    /**
+     */
+    @Provides
+    @SysUISingleton
+    static DialogLaunchAnimator provideDialogLaunchAnimator(Context context,
+            LaunchAnimator launchAnimator) {
+        return new DialogLaunchAnimator(context, launchAnimator, new SystemUIHostDialogProvider());
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index 1037e57..b97bac2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -24,7 +24,6 @@
 import android.view.Gravity
 import android.view.View
 import android.widget.FrameLayout
-
 import com.android.internal.annotations.GuardedBy
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.R
@@ -44,7 +43,6 @@
 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
 import com.android.systemui.util.leak.RotationUtils.Rotation
 
-import java.lang.IllegalStateException
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -71,9 +69,6 @@
     private val contentInsetsProvider: StatusBarContentInsetsProvider,
     private val animationScheduler: SystemStatusAnimationScheduler
 ) {
-    private var sbHeightPortrait = 0
-    private var sbHeightLandscape = 0
-
     private lateinit var tl: View
     private lateinit var tr: View
     private lateinit var bl: View
@@ -156,16 +151,12 @@
 
         val newCorner = selectDesignatedCorner(rot, isRtl)
         val index = newCorner.cornerIndex()
+        val paddingTop = contentInsetsProvider.getStatusBarPaddingTop(rot)
 
-        val h = when (rot) {
-            0, 2 -> sbHeightPortrait
-            1, 3 -> sbHeightLandscape
-            else -> 0
-        }
         synchronized(lock) {
             nextViewState = nextViewState.copy(
                     rotation = rot,
-                    height = h,
+                    paddingTop = paddingTop,
                     designatedCorner = newCorner,
                     cornerIndex = index)
         }
@@ -203,26 +194,17 @@
         }
     }
 
-    @UiThread
-    private fun updateHeights(rot: Int) {
-        val height = when (rot) {
-            0, 2 -> sbHeightPortrait
-            1, 3 -> sbHeightLandscape
-            else -> 0
-        }
-
-        views.forEach { it.layoutParams.height = height }
-    }
-
     // Update the gravity and margins of the privacy views
     @UiThread
-    private fun updateRotations(rotation: Int) {
+    private fun updateRotations(rotation: Int, paddingTop: Int) {
         // To keep a view in the corner, its gravity is always the description of its current corner
         // Therefore, just figure out which view is in which corner. This turns out to be something
         // like (myCorner - rot) mod 4, where topLeft = 0, topRight = 1, etc. and portrait = 0, and
         // rotating the device counter-clockwise increments rotation by 1
 
         views.forEach { corner ->
+            corner.setPadding(0, paddingTop, 0, 0)
+
             val rotatedCorner = rotatedCorner(cornerForView(corner), rotation)
             (corner.layoutParams as FrameLayout.LayoutParams).apply {
                 gravity = rotatedCorner.toGravity()
@@ -265,6 +247,7 @@
 
         var rot = activeRotationForCorner(tl, rtl)
         var contentInsets = state.contentRectForRotation(rot)
+        tl.setPadding(0, state.paddingTop, 0, 0)
         (tl.layoutParams as FrameLayout.LayoutParams).apply {
             height = contentInsets.height()
             if (rtl) {
@@ -276,6 +259,7 @@
 
         rot = activeRotationForCorner(tr, rtl)
         contentInsets = state.contentRectForRotation(rot)
+        tr.setPadding(0, state.paddingTop, 0, 0)
         (tr.layoutParams as FrameLayout.LayoutParams).apply {
             height = contentInsets.height()
             if (rtl) {
@@ -287,6 +271,7 @@
 
         rot = activeRotationForCorner(br, rtl)
         contentInsets = state.contentRectForRotation(rot)
+        br.setPadding(0, state.paddingTop, 0, 0)
         (br.layoutParams as FrameLayout.LayoutParams).apply {
             height = contentInsets.height()
             if (rtl) {
@@ -298,6 +283,7 @@
 
         rot = activeRotationForCorner(bl, rtl)
         contentInsets = state.contentRectForRotation(rot)
+        bl.setPadding(0, state.paddingTop, 0, 0)
         (bl.layoutParams as FrameLayout.LayoutParams).apply {
             height = contentInsets.height()
             if (rtl) {
@@ -412,6 +398,7 @@
         val right = contentInsetsProvider.getStatusBarContentInsetsForRotation(ROTATION_LANDSCAPE)
         val bottom = contentInsetsProvider
                 .getStatusBarContentInsetsForRotation(ROTATION_UPSIDE_DOWN)
+        val paddingTop = contentInsetsProvider.getStatusBarPaddingTop()
 
         synchronized(lock) {
             nextViewState = nextViewState.copy(
@@ -422,20 +409,12 @@
                     portraitRect = top,
                     landscapeRect = right,
                     upsideDownRect = bottom,
+                    paddingTop = paddingTop,
                     layoutRtl = rtl
             )
         }
     }
 
-    /**
-     * Set the status bar height in portrait and landscape, in pixels. If they are the same you can
-     * pass the same value twice
-     */
-    fun setStatusBarHeights(portrait: Int, landscape: Int) {
-        sbHeightPortrait = portrait
-        sbHeightLandscape = landscape
-    }
-
     private fun updateStatusBarState() {
         synchronized(lock) {
             nextViewState = nextViewState.copy(shadeExpanded = isShadeInQs())
@@ -488,7 +467,7 @@
 
         if (state.rotation != currentViewState.rotation) {
             // A rotation has started, hide the views to avoid flicker
-            updateRotations(state.rotation)
+            updateRotations(state.rotation, state.paddingTop)
         }
 
         if (state.needsLayout(currentViewState)) {
@@ -627,7 +606,7 @@
     val layoutRtl: Boolean = false,
 
     val rotation: Int = 0,
-    val height: Int = 0,
+    val paddingTop: Int = 0,
     val cornerIndex: Int = -1,
     val designatedCorner: View? = null,
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 7291b5a..589446f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -24,13 +24,10 @@
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
-
 import com.android.systemui.R
-import com.android.systemui.statusbar.SuperStatusBarViewFactory
 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher
 import com.android.systemui.statusbar.phone.StatusBarWindowController
 import com.android.systemui.statusbar.phone.StatusBarWindowView
-
 import javax.inject.Inject
 
 /**
@@ -38,7 +35,7 @@
  */
 class SystemEventChipAnimationController @Inject constructor(
     private val context: Context,
-    private val statusBarViewFactory: SuperStatusBarViewFactory,
+    private val statusBarWindowView: StatusBarWindowView,
     private val statusBarWindowController: StatusBarWindowController,
     private val locationPublisher: StatusBarLocationPublisher
 ) : SystemStatusChipAnimationCallback {
@@ -51,7 +48,6 @@
 
     private lateinit var animationWindowView: FrameLayout
     private lateinit var animationDotView: View
-    private lateinit var statusBarWindowView: StatusBarWindowView
     private var currentAnimatedView: View? = null
 
     // TODO: move to dagger
@@ -125,7 +121,6 @@
 
     private fun init() {
         initialized = true
-        statusBarWindowView = statusBarViewFactory.statusBarWindowView
         animationWindowView = LayoutInflater.from(context)
                 .inflate(R.layout.system_event_animation_window, null) as FrameLayout
         animationDotView = animationWindowView.findViewById(R.id.dot_view)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
new file mode 100644
index 0000000..80577ee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.gesture
+
+import android.content.Context
+import android.os.Looper
+import android.view.Choreographer
+import android.view.Display
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.*
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shared.system.InputChannelCompat
+import com.android.systemui.shared.system.InputMonitorCompat
+import com.android.systemui.statusbar.phone.StatusBarWindowController
+import javax.inject.Inject
+
+/**
+ * A class to detect when a user swipes away the status bar. To be notified when the swipe away
+ * gesture is detected, add a callback via [addOnGestureDetectedCallback].
+ */
+@SysUISingleton
+open class SwipeStatusBarAwayGestureHandler @Inject constructor(
+    context: Context,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val logger: SwipeStatusBarAwayGestureLogger
+) {
+
+    /**
+     * Active callbacks, each associated with a tag. Gestures will only be monitored if
+     * [callbacks.size] > 0.
+     */
+    private val callbacks: MutableMap<String, () -> Unit> = mutableMapOf()
+
+    private var startY: Float = 0f
+    private var startTime: Long = 0L
+    private var monitoringCurrentTouch: Boolean = false
+
+    private var inputMonitor: InputMonitorCompat? = null
+    private var inputReceiver: InputChannelCompat.InputEventReceiver? = null
+
+    private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
+        com.android.internal.R.dimen.system_gestures_start_threshold
+    )
+
+    /** Adds a callback that will be triggered when the swipe away gesture is detected. */
+    fun addOnGestureDetectedCallback(tag: String, callback: () -> Unit) {
+        val callbacksWasEmpty = callbacks.isEmpty()
+        callbacks[tag] = callback
+        if (callbacksWasEmpty) {
+            startGestureListening()
+        }
+    }
+
+    /** Removes the callback. */
+    fun removeOnGestureDetectedCallback(tag: String) {
+        callbacks.remove(tag)
+        if (callbacks.isEmpty()) {
+             stopGestureListening()
+        }
+    }
+
+    private fun onInputEvent(ev: InputEvent) {
+        if (ev !is MotionEvent) {
+            return
+        }
+
+        when (ev.actionMasked) {
+            ACTION_DOWN -> {
+                if (
+                    // Gesture starts just below the status bar
+                    ev.y >= statusBarWindowController.statusBarHeight
+                    && ev.y <= 3 * statusBarWindowController.statusBarHeight
+                ) {
+                    logger.logGestureDetectionStarted(ev.y.toInt())
+                    startY = ev.y
+                    startTime = ev.eventTime
+                    monitoringCurrentTouch = true
+                } else {
+                    monitoringCurrentTouch = false
+                }
+            }
+            ACTION_MOVE -> {
+                if (!monitoringCurrentTouch) {
+                    return
+                }
+                if (
+                    // Gesture is up
+                    ev.y < startY
+                    // Gesture went far enough
+                    && (startY - ev.y) >= swipeDistanceThreshold
+                    // Gesture completed quickly enough
+                    && (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
+                ) {
+                    monitoringCurrentTouch = false
+                    logger.logGestureDetected(ev.y.toInt())
+                    callbacks.values.forEach { it.invoke() }
+                }
+            }
+            ACTION_CANCEL, ACTION_UP -> {
+                if (monitoringCurrentTouch) {
+                    logger.logGestureDetectionEndedWithoutTriggering(ev.y.toInt())
+                }
+                monitoringCurrentTouch = false
+            }
+        }
+    }
+
+    /** Start listening for the swipe gesture. */
+    private fun startGestureListening() {
+        stopGestureListening()
+
+        logger.logInputListeningStarted()
+        inputMonitor = InputMonitorCompat(TAG, Display.DEFAULT_DISPLAY).also {
+            inputReceiver = it.getInputReceiver(
+                Looper.getMainLooper(),
+                Choreographer.getInstance(),
+                this::onInputEvent
+            )
+        }
+    }
+
+    /** Stop listening for the swipe gesture. */
+    private fun stopGestureListening() {
+        inputMonitor?.let {
+            logger.logInputListeningStopped()
+            inputMonitor = null
+            it.dispose()
+        }
+        inputReceiver?.let {
+            inputReceiver = null
+            it.dispose()
+        }
+    }
+}
+
+private const val SWIPE_TIMEOUT_MS: Long = 500
+private val TAG = SwipeStatusBarAwayGestureHandler::class.simpleName
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
new file mode 100644
index 0000000..17feaa8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.gesture
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import javax.inject.Inject
+
+/** Log messages for [SwipeStatusBarAwayGestureHandler]. */
+class SwipeStatusBarAwayGestureLogger @Inject constructor(
+    @SwipeStatusBarAwayLog private val buffer: LogBuffer
+) {
+    fun logGestureDetectionStarted(y: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = y },
+            { "Beginning gesture detection. y=$int1" }
+        )
+    }
+
+    fun logGestureDetectionEndedWithoutTriggering(y: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = y },
+            { "Gesture finished; no swipe up gesture detected. Final y=$int1" }
+        )
+    }
+
+    fun logGestureDetected(y: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { int1 = y },
+            { "Gesture detected; notifying callbacks. y=$int1" }
+        )
+    }
+
+    fun logInputListeningStarted() {
+        buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening started "})
+    }
+
+    fun logInputListeningStopped() {
+        buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening stopped "})
+    }
+}
+
+private const val TAG = "SwipeStatusBarAwayGestureHandler"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 0773460..bacb85a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -29,6 +29,7 @@
 import android.os.Handler
 import android.os.UserHandle
 import android.provider.Settings
+import android.util.Log
 import android.view.View
 import android.view.ViewGroup
 import com.android.settingslib.Utils
@@ -42,7 +43,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.util.concurrency.Execution
@@ -73,27 +74,37 @@
     @Main private val handler: Handler,
     optionalPlugin: Optional<BcSmartspaceDataPlugin>
 ) {
+    companion object {
+        private const val TAG = "LockscreenSmartspaceController"
+    }
+
     private var session: SmartspaceSession? = null
     private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
-    private lateinit var smartspaceView: SmartspaceView
 
-    lateinit var view: View
-        private set
+    // Smartspace can be used on multiple displays, such as when the user casts their screen
+    private var smartspaceViews = mutableSetOf<SmartspaceView>()
 
     private var showSensitiveContentForCurrentUser = false
     private var showSensitiveContentForManagedUser = false
     private var managedUserHandle: UserHandle? = null
 
-    private val deviceProvisionedListener =
-        object : DeviceProvisionedController.DeviceProvisionedListener {
-            override fun onDeviceProvisionedChanged() {
-                connectSession()
-            }
+    var stateChangeListener = object : View.OnAttachStateChangeListener {
+        override fun onViewAttachedToWindow(v: View) {
+            smartspaceViews.add(v as SmartspaceView)
+            connectSession()
 
-            override fun onUserSetupChanged() {
-                connectSession()
+            updateTextColorFromWallpaper()
+            statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount)
+        }
+
+        override fun onViewDetachedFromWindow(v: View) {
+            smartspaceViews.remove(v as SmartspaceView)
+
+            if (smartspaceViews.isEmpty()) {
+                disconnect()
             }
         }
+    }
 
     private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
         execution.assertIsMainThread()
@@ -125,10 +136,21 @@
     private val statusBarStateListener = object : StatusBarStateController.StateListener {
         override fun onDozeAmountChanged(linear: Float, eased: Float) {
             execution.assertIsMainThread()
-            smartspaceView.setDozeAmount(eased)
+            smartspaceViews.forEach { it.setDozeAmount(eased) }
         }
     }
 
+    private val deviceProvisionedListener =
+        object : DeviceProvisionedController.DeviceProvisionedListener {
+            override fun onDeviceProvisionedChanged() {
+                connectSession()
+            }
+
+            override fun onUserSetupChanged() {
+                connectSession()
+            }
+        }
+
     init {
         deviceProvisionedController.addCallback(deviceProvisionedListener)
     }
@@ -140,17 +162,16 @@
     }
 
     /**
-     * Constructs the smartspace view and connects it to the smartspace service. Subsequent calls
-     * are idempotent until [disconnect] is called.
+     * Constructs the smartspace view and connects it to the smartspace service.
      */
-    fun buildAndConnectView(parent: ViewGroup): View {
+    fun buildAndConnectView(parent: ViewGroup): View? {
         execution.assertIsMainThread()
 
         if (!isEnabled()) {
             throw RuntimeException("Cannot build view when not enabled")
         }
 
-        buildView(parent)
+        val view = buildView(parent)
         connectSession()
 
         return view
@@ -160,14 +181,9 @@
         session?.requestSmartspaceUpdate()
     }
 
-    private fun buildView(parent: ViewGroup) {
+    private fun buildView(parent: ViewGroup): View? {
         if (plugin == null) {
-            return
-        }
-        if (this::view.isInitialized) {
-            // Due to some oddities with a singleton smartspace view, allow reparenting
-            (view.getParent() as ViewGroup?)?.removeView(view)
-            return
+            return null
         }
 
         val ssView = plugin.getView(parent)
@@ -182,16 +198,11 @@
             }
         })
         ssView.setFalsingManager(falsingManager)
-
-        this.smartspaceView = ssView
-        this.view = ssView as View
-
-        updateTextColorFromWallpaper()
-        statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount)
+        return (ssView as View).apply { addOnAttachStateChangeListener(stateChangeListener) }
     }
 
     private fun connectSession() {
-        if (plugin == null || session != null || !this::smartspaceView.isInitialized) {
+        if (plugin == null || session != null || smartspaceViews.isEmpty()) {
             return
         }
 
@@ -204,6 +215,7 @@
 
         val newSession = smartspaceManager.createSmartspaceSession(
                 SmartspaceConfig.Builder(context, "lockscreen").build())
+        Log.d(TAG, "Starting smartspace session for lockscreen")
         newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
         this.session = newSession
 
@@ -223,10 +235,10 @@
 
     /**
      * Disconnects the smartspace view from the smartspace service and cleans up any resources.
-     * Calling [buildAndConnectView] again will cause the same view to be reconnected to the
-     * service.
      */
     fun disconnect() {
+        if (!smartspaceViews.isEmpty()) return
+
         execution.assertIsMainThread()
 
         if (session == null) {
@@ -244,6 +256,7 @@
         session = null
 
         plugin?.onTargetsAvailable(emptyList())
+        Log.d(TAG, "Ending smartspace session for lockscreen")
     }
 
     fun addListener(listener: SmartspaceTargetListener) {
@@ -277,7 +290,7 @@
 
     private fun updateTextColorFromWallpaper() {
         val wallpaperTextColor = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)
-        smartspaceView.setPrimaryTextColor(wallpaperTextColor)
+        smartspaceViews.forEach { it.setPrimaryTextColor(wallpaperTextColor) }
     }
 
     private fun reloadSmartspace() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
index f19cf5d..64a7305 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
@@ -2,8 +2,8 @@
 
 import android.util.MathUtils
 import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.LaunchAnimator
 import kotlin.math.min
 
 /** Parameters for the notifications expand animations. */
@@ -15,7 +15,7 @@
 
     topCornerRadius: Float = 0f,
     bottomCornerRadius: Float = 0f
-) : ActivityLaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
+) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
     @VisibleForTesting
     constructor() : this(
         top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f
@@ -55,6 +55,6 @@
         }
 
     fun getProgress(delay: Long, duration: Long): Float {
-        return ActivityLaunchAnimator.getProgress(linearProgress, delay, duration)
+        return LaunchAnimator.getProgress(linearProgress, delay, duration)
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
index 2537b19..129fa5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
@@ -20,6 +20,7 @@
 import android.service.notification.StatusBarNotification;
 import android.view.View;
 
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 
 /**
@@ -37,6 +38,9 @@
     /** Called when the user clicks "Manage" or "History" in the Shade. */
     void startHistoryIntent(View view, boolean showHistory);
 
+    /** Called when the user succeed to drop notification to proper target view. */
+    void onDragSuccess(NotificationEntry entry);
+
     default boolean isCollapsingToShowActivityOverLockscreen() {
         return false;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index 0fb1c54..da70621 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -43,6 +43,14 @@
     private final Optional<Bubbles> mBubblesOptional;
     private final NotificationActivityStarter mNotificationActivityStarter;
 
+    private ExpandableNotificationRow.OnDragSuccessListener mOnDragSuccessListener =
+            new ExpandableNotificationRow.OnDragSuccessListener() {
+                @Override
+                public void onDragSuccess(NotificationEntry entry) {
+                    mNotificationActivityStarter.onDragSuccess(entry);
+                }
+            };
+
     private NotificationClicker(
             NotificationClickerLogger logger,
             Optional<StatusBar> statusBarOptional,
@@ -111,8 +119,10 @@
         if (notification.contentIntent != null || notification.fullScreenIntent != null
                 || row.getEntry().isBubble()) {
             row.setOnClickListener(this);
+            row.setOnDragSuccessListener(mOnDragSuccessListener);
         } else {
             row.setOnClickListener(null);
+            row.setOnDragSuccessListener(null);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 1ab2a9e..60f44a0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -37,7 +37,8 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.systemui.Dumpable;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLifetimeExtender;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
@@ -98,15 +99,16 @@
         CommonNotifCollection,
         Dumpable,
         VisualStabilityManager.Callback {
-    private static final String TAG = "NotificationEntryMgr";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    /**
-     * Used when a notification is removed and it doesn't have a reason that maps to one of the
-     * reasons defined in NotificationListenerService
-     * (e.g. {@link NotificationListenerService#REASON_CANCEL})
-     */
-    public static final int UNDEFINED_DISMISS_REASON = 0;
+    private final NotificationEntryManagerLogger mLogger;
+    private final NotificationGroupManagerLegacy mGroupManager;
+    private final FeatureFlags mFeatureFlags;
+    private final Lazy<NotificationRowBinder> mNotificationRowBinderLazy;
+    private final Lazy<NotificationRemoteInputManager> mRemoteInputManagerLazy;
+    private final LeakDetector mLeakDetector;
+    private final ForegroundServiceDismissalFeatureController mFgsFeatureController;
+    private final IStatusBarService mStatusBarService;
+    private final DumpManager mDumpManager;
 
     private final Set<NotificationEntry> mAllNotifications = new ArraySet<>();
     private final Set<NotificationEntry> mReadOnlyAllNotifications =
@@ -129,20 +131,8 @@
     private final Map<NotificationEntry, NotificationLifetimeExtender> mRetainedNotifications =
             new ArrayMap<>();
 
-    private final NotificationEntryManagerLogger mLogger;
-
-    private final IStatusBarService mStatusBarService;
-
-    // Lazily retrieved dependencies
-    private final Lazy<NotificationRowBinder> mNotificationRowBinderLazy;
-    private final Lazy<NotificationRemoteInputManager> mRemoteInputManagerLazy;
-    private final LeakDetector mLeakDetector;
     private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
 
-    private final NotificationGroupManagerLegacy mGroupManager;
-    private final FeatureFlags mFeatureFlags;
-    private final ForegroundServiceDismissalFeatureController mFgsFeatureController;
-
     private LegacyNotificationRanker mRanker = new LegacyNotificationRankerStub();
     private NotificationPresenter mPresenter;
     private RankingMap mLatestRankingMap;
@@ -153,6 +143,40 @@
     private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>();
     private final List<NotificationRemoveInterceptor> mRemoveInterceptors = new ArrayList<>();
 
+    /**
+     * Injected constructor. See {@link NotificationsModule}.
+     */
+    public NotificationEntryManager(
+            NotificationEntryManagerLogger logger,
+            NotificationGroupManagerLegacy groupManager,
+            FeatureFlags featureFlags,
+            Lazy<NotificationRowBinder> notificationRowBinderLazy,
+            Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
+            LeakDetector leakDetector,
+            ForegroundServiceDismissalFeatureController fgsFeatureController,
+            IStatusBarService statusBarService,
+            DumpManager dumpManager
+    ) {
+        mLogger = logger;
+        mGroupManager = groupManager;
+        mFeatureFlags = featureFlags;
+        mNotificationRowBinderLazy = notificationRowBinderLazy;
+        mRemoteInputManagerLazy = notificationRemoteInputManagerLazy;
+        mLeakDetector = leakDetector;
+        mFgsFeatureController = fgsFeatureController;
+        mStatusBarService = statusBarService;
+        mDumpManager = dumpManager;
+    }
+
+    /** Once called, the NEM will start processing notification events from system server. */
+    public void initialize(
+            NotificationListener notificationListener,
+            LegacyNotificationRanker ranker) {
+        mRanker = ranker;
+        notificationListener.addNotificationHandler(mNotifListener);
+        mDumpManager.registerDumpable(this);
+    }
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("NotificationEntryManager state:");
@@ -194,38 +218,6 @@
         }
     }
 
-    /**
-     * Injected constructor. See {@link NotificationsModule}.
-     */
-    public NotificationEntryManager(
-            NotificationEntryManagerLogger logger,
-            NotificationGroupManagerLegacy groupManager,
-            FeatureFlags featureFlags,
-            Lazy<NotificationRowBinder> notificationRowBinderLazy,
-            Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
-            LeakDetector leakDetector,
-            ForegroundServiceDismissalFeatureController fgsFeatureController,
-            IStatusBarService statusBarService
-    ) {
-        mLogger = logger;
-        mGroupManager = groupManager;
-        mFeatureFlags = featureFlags;
-        mNotificationRowBinderLazy = notificationRowBinderLazy;
-        mRemoteInputManagerLazy = notificationRemoteInputManagerLazy;
-        mLeakDetector = leakDetector;
-        mFgsFeatureController = fgsFeatureController;
-        mStatusBarService = statusBarService;
-    }
-
-    /** Once called, the NEM will start processing notification events from system server. */
-    public void attach(NotificationListener notificationListener) {
-        notificationListener.addNotificationHandler(mNotifListener);
-    }
-
-    public void setRanker(LegacyNotificationRanker ranker) {
-        mRanker = ranker;
-    }
-
     /** Adds a {@link NotificationEntryListener}. */
     public void addNotificationEntryListener(NotificationEntryListener listener) {
         mNotificationEntryListeners.add(listener);
@@ -313,9 +305,6 @@
             NotificationEntry entry = mPendingNotifications.get(key);
             entry.abortTask();
             mPendingNotifications.remove(key);
-            for (NotifCollectionListener listener : mNotifCollectionListeners) {
-                listener.onEntryCleanUp(entry);
-            }
             mLogger.logInflationAborted(key, "pending", reason);
         }
         NotificationEntry addedEntry = getActiveNotificationUnfiltered(key);
@@ -485,6 +474,18 @@
                 if (!lifetimeExtended) {
                     // At this point, we are guaranteed the notification will be removed
                     abortExistingInflation(key, "removeNotification");
+                    // Fix for b/201097913: NotifCollectionListener#onEntryRemoved specifies that
+                    //   #onEntryRemoved should be called when a notification is cancelled,
+                    //   regardless of whether the notification was pending or active.
+                    // Note that mNotificationEntryListeners are NOT notified of #onEntryRemoved
+                    //   because for that interface, #onEntryRemoved should only be called for
+                    //   active entries, NOT pending ones.
+                    for (NotifCollectionListener listener : mNotifCollectionListeners) {
+                        listener.onEntryRemoved(pendingEntry, REASON_UNKNOWN);
+                    }
+                    for (NotifCollectionListener listener : mNotifCollectionListeners) {
+                        listener.onEntryCleanUp(pendingEntry);
+                    }
                     mAllNotifications.remove(pendingEntry);
                     mLeakDetector.trackGarbage(pendingEntry);
                 }
@@ -976,4 +977,14 @@
         /** true if the notification is for the current profiles */
         boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
     }
+
+    private static final String TAG = "NotificationEntryMgr";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    /**
+     * Used when a notification is removed and it doesn't have a reason that maps to one of the
+     * reasons defined in NotificationListenerService
+     * (e.g. {@link NotificationListenerService#REASON_CANCEL})
+     */
+    public static final int UNDEFINED_DISMISS_REASON = 0;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 1bbef25..22c3eda 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -3,6 +3,7 @@
 import android.view.ViewGroup
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.LaunchAnimator
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
@@ -54,7 +55,7 @@
             // Do nothing. Notifications are always animated inside their rootView.
         }
 
-    override fun createAnimatorState(): ActivityLaunchAnimator.State {
+    override fun createAnimatorState(): LaunchAnimator.State {
         // If the notification panel is collapsed, the clip may be larger than the height.
         val height = max(0, notification.actualHeight - notification.clipBottomAmount)
         val location = notification.locationOnScreen
@@ -72,12 +73,12 @@
             notification.currentBackgroundRadiusTop
         }
         val params = ExpandAnimationParameters(
-                top = windowTop,
-                bottom = location[1] + height,
-                left = location[0],
-                right = location[0] + notification.width,
-                topCornerRadius = topCornerRadius,
-                bottomCornerRadius = notification.currentBackgroundRadiusBottom
+            top = windowTop,
+            bottom = location[1] + height,
+            left = location[0],
+            right = location[0] + notification.width,
+            topCornerRadius = topCornerRadius,
+            bottomCornerRadius = notification.currentBackgroundRadiusBottom
         )
 
         params.startTranslationZ = notification.translationZ
@@ -86,8 +87,8 @@
         params.startClipTopAmount = notification.clipTopAmount
         if (notification.isChildInGroup) {
             params.startNotificationTop += notification.notificationParent.translationY
-            val parentRoundedClip = Math.max(clipStartLocation
-                - notification.notificationParent.locationOnScreen[1], 0)
+            val parentRoundedClip = Math.max(
+                clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
             params.parentStartRoundedTopClipping = parentRoundedClip
 
             val parentClip = notification.notificationParent.clipTopAmount
@@ -157,7 +158,7 @@
     }
 
     override fun onLaunchAnimationProgress(
-        state: ActivityLaunchAnimator.State,
+        state: LaunchAnimator.State,
         progress: Float,
         linearProgress: Float
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 8ae31cb..277b4ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -63,7 +63,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.LogBufferEulogizer;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index a0ef1b6..e26fa04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -28,8 +28,6 @@
 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_SORTING;
 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_TRANSFORMING;
 
-import static java.util.Objects.requireNonNull;
-
 import android.annotation.MainThread;
 import android.annotation.Nullable;
 import android.util.ArrayMap;
@@ -344,14 +342,8 @@
         mPipelineState.incrementTo(STATE_GROUP_STABILIZING);
         stabilizeGroupingNotifs(mNotifList);
 
-        // Step 5: Sort
-        // Assign each top-level entry a section, then sort the list by section and then within
-        // section by our list of custom comparators
-        dispatchOnBeforeSort(mReadOnlyNotifList);
-        mPipelineState.incrementTo(STATE_SORTING);
-        sortList();
 
-        // Step 6: Filter out entries after pre-group filtering, grouping, promoting and sorting
+        // Step 5: Filter out entries after pre-group filtering, grouping and promoting
         // Now filters can see grouping information to determine whether to filter or not.
         dispatchOnBeforeFinalizeFilter(mReadOnlyNotifList);
         mPipelineState.incrementTo(STATE_FINALIZE_FILTERING);
@@ -359,6 +351,13 @@
         applyNewNotifList();
         pruneIncompleteGroups(mNotifList);
 
+        // Step 6: Sort
+        // Assign each top-level entry a section, then sort the list by section and then within
+        // section by our list of custom comparators
+        dispatchOnBeforeSort(mReadOnlyNotifList);
+        mPipelineState.incrementTo(STATE_SORTING);
+        sortList();
+
         // Step 7: Lock in our group structure and log anything that's changed since the last run
         mPipelineState.incrementTo(STATE_FINALIZING);
         logChanges();
@@ -837,8 +836,8 @@
     private final Comparator<ListEntry> mTopLevelComparator = (o1, o2) -> {
 
         int cmp = Integer.compare(
-                requireNonNull(o1.getSection()).getIndex(),
-                requireNonNull(o2.getSection()).getIndex());
+                o1.getSectionIndex(),
+                o2.getSectionIndex());
 
         if (cmp == 0) {
             for (int i = 0; i < mNotifComparators.size(); i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java
index be1383f..6e98c27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java
@@ -149,7 +149,7 @@
             final String entryKey = entry.getKey();
             if (mHeadsUpManager.isAlerting(entryKey)) {
                 boolean removeImmediatelyForRemoteInput =
-                        mRemoteInputManager.getController().isSpinning(entryKey)
+                        mRemoteInputManager.isSpinning(entryKey)
                                 && !FORCE_REMOTE_INPUT_HISTORY;
                 mHeadsUpManager.removeNotification(entry.getKey(), removeImmediatelyForRemoteInput);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
index d80cc08..25b2019 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
@@ -19,7 +19,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/LowPriorityInflationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/LowPriorityInflationHelper.java
index aec2647..518c3f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/LowPriorityInflationHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/LowPriorityInflationHelper.java
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.notification.collection.inflation;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
index db49e44..168e086 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
@@ -21,7 +21,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java
index f40f24a..5993f1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java
@@ -25,6 +25,7 @@
 
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.StatusBarState;
@@ -60,8 +61,12 @@
  * 2. Tracking group expansion states
  */
 @SysUISingleton
-public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, StateListener,
-        GroupMembershipManager, GroupExpansionManager, Dumpable {
+public class NotificationGroupManagerLegacy implements
+        OnHeadsUpChangedListener,
+        StateListener,
+        GroupMembershipManager,
+        GroupExpansionManager,
+        Dumpable {
 
     private static final String TAG = "NotifGroupManager";
     private static final boolean DEBUG = StatusBar.DEBUG;
@@ -87,10 +92,13 @@
     public NotificationGroupManagerLegacy(
             StatusBarStateController statusBarStateController,
             Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier,
-            Optional<Bubbles> bubblesOptional) {
+            Optional<Bubbles> bubblesOptional,
+            DumpManager dumpManager) {
         statusBarStateController.addCallback(this);
         mPeopleNotificationIdentifier = peopleNotificationIdentifier;
         mBubblesOptional = bubblesOptional;
+
+        dumpManager.registerDumpable(this);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/VisualStabilityManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/VisualStabilityManager.java
index 165df30..6e47c7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/VisualStabilityManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/VisualStabilityManager.java
@@ -24,6 +24,7 @@
 
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -71,9 +72,11 @@
             NotificationEntryManager notificationEntryManager,
             @Main Handler handler,
             StatusBarStateController statusBarStateController,
-            WakefulnessLifecycle wakefulnessLifecycle) {
+            WakefulnessLifecycle wakefulnessLifecycle,
+            DumpManager dumpManager) {
 
         mHandler = handler;
+        dumpManager.registerDumpable(this);
 
         if (notificationEntryManager != null) {
             notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java
index 798bfe7..027ac0f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/PipelineState.java
@@ -82,8 +82,8 @@
     public static final int STATE_GROUPING = 4;
     public static final int STATE_TRANSFORMING = 5;
     public static final int STATE_GROUP_STABILIZING = 6;
-    public static final int STATE_SORTING = 7;
-    public static final int STATE_FINALIZE_FILTERING = 8;
+    public static final int STATE_FINALIZE_FILTERING = 7;
+    public static final int STATE_SORTING = 8;
     public static final int STATE_FINALIZING = 9;
 
     @IntDef(prefix = { "STATE_" }, value = {
@@ -94,8 +94,8 @@
             STATE_GROUPING,
             STATE_TRANSFORMING,
             STATE_GROUP_STABILIZING,
-            STATE_SORTING,
             STATE_FINALIZE_FILTERING,
+            STATE_SORTING,
             STATE_FINALIZING,
     })
     @Retention(RetentionPolicy.SOURCE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt
index 727ce20..289dacb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt
@@ -78,7 +78,7 @@
 }
 
 private fun treeSpecToStrHelper(tree: NodeSpec, sb: StringBuilder, indent: String) {
-    sb.append("${indent}ns{${tree.controller.nodeLabel}")
+    sb.append("${indent}{${tree.controller.nodeLabel}}\n")
     if (tree.children.isNotEmpty()) {
         val childIndent = "$indent  "
         for (child in tree.children) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
new file mode 100644
index 0000000..9b8ac72
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.render
+
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
+
+/**
+ * Converts a notif list (the output of the ShadeListBuilder) into a NodeSpec, an abstract
+ * representation of which views should be present in the shade. This spec will later be consumed
+ * by the ViewDiffer, which will add and remove views until the shade matches the spec. Up until
+ * this point, the pipeline has dealt with pure data representations of notifications (in the
+ * form of NotificationEntries). In this step, NotificationEntries finally become associated with
+ * the views that will represent them. In addition, we add in any non-notification views that also
+ * need to present in the shade, notably the section headers.
+ */
+class NodeSpecBuilder(
+    private val viewBarn: NotifViewBarn
+) {
+    fun buildNodeSpec(
+        rootController: NodeController,
+        notifList: List<ListEntry>
+    ): NodeSpec {
+        val root = NodeSpecImpl(null, rootController)
+        var currentSection: NotifSection? = null
+        val prevSections = mutableSetOf<NotifSection?>()
+
+        for (entry in notifList) {
+            val section = entry.section!!
+
+            if (prevSections.contains(section)) {
+                throw java.lang.RuntimeException("Section ${section.label} has been duplicated")
+            }
+
+            // If this notif begins a new section, first add the section's header view
+            if (section != currentSection) {
+                section.headerController?.let { headerController ->
+                    root.children.add(NodeSpecImpl(root, headerController))
+                }
+                prevSections.add(currentSection)
+                currentSection = section
+            }
+
+            // Finally, add the actual notif node!
+            root.children.add(buildNotifNode(root, entry))
+        }
+
+        return root
+    }
+
+    private fun buildNotifNode(parent: NodeSpec, entry: ListEntry): NodeSpec = when (entry) {
+        is NotificationEntry -> NodeSpecImpl(parent, viewBarn.requireView(entry))
+        is GroupEntry -> NodeSpecImpl(parent, viewBarn.requireView(checkNotNull(entry.summary)))
+                .apply { entry.children.forEach { children.add(buildNotifNode(this, it)) } }
+        else -> throw RuntimeException("Unexpected entry: $entry")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt
index 79bc3d7..c79f59b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt
@@ -19,18 +19,16 @@
 import android.view.textclassifier.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.ListEntry
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController
 import javax.inject.Inject
 
 /**
- * The ViewBarn is just a map from [ListEntry] to an instance of an
- * [ExpandableNotificationRowController].
+ * The ViewBarn is just a map from [ListEntry] to an instance of a [NodeController].
  */
 @SysUISingleton
 class NotifViewBarn @Inject constructor() {
-    private val rowMap = mutableMapOf<String, ExpandableNotificationRowController>()
+    private val rowMap = mutableMapOf<String, NodeController>()
 
-    fun requireView(forEntry: ListEntry): ExpandableNotificationRowController {
+    fun requireView(forEntry: ListEntry): NodeController {
         if (DEBUG) {
             Log.d(TAG, "requireView: $forEntry.key")
         }
@@ -42,7 +40,7 @@
         return li
     }
 
-    fun registerViewForEntry(entry: ListEntry, controller: ExpandableNotificationRowController) {
+    fun registerViewForEntry(entry: ListEntry, controller: NodeController) {
         if (DEBUG) {
             Log.d(TAG, "registerViewForEntry: $entry.key")
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
index c1a63e9..a2c7aa5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
@@ -18,9 +18,7 @@
 
 import android.content.Context
 import android.view.View
-import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.ShadeListBuilder
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
@@ -34,42 +32,21 @@
     context: Context,
     listContainer: NotificationListContainer,
     logger: ShadeViewDifferLogger,
-    private val viewBarn: NotifViewBarn,
+    viewBarn: NotifViewBarn,
     private val notificationIconAreaController: NotificationIconAreaController
 ) {
     // We pass a shim view here because the listContainer may not actually have a view associated
     // with it and the differ never actually cares about the root node's view.
     private val rootController = RootNodeController(listContainer, View(context))
+    private val specBuilder = NodeSpecBuilder(viewBarn)
     private val viewDiffer = ShadeViewDiffer(rootController, logger)
 
     fun attach(listBuilder: ShadeListBuilder) =
             listBuilder.setOnRenderListListener(::onNewNotifTree)
 
-    private fun onNewNotifTree(tree: List<ListEntry>) = viewDiffer.applySpec(buildTree(tree))
-
-    private fun buildTree(notifList: List<ListEntry>): NodeSpec {
-        val root = NodeSpecImpl(null, rootController).apply {
-            // Insert first section header, if present
-            notifList.firstOrNull()?.section?.headerController?.let {
-                children.add(NodeSpecImpl(this, it))
-            }
-            notifList.asSequence().zipWithNext().forEach { (prev, entry) ->
-                // Insert new header if the section has changed between two entries
-                entry.section.takeIf { it != prev.section }?.headerController?.let {
-                    children.add(NodeSpecImpl(this, it))
-                }
-                children.add(buildNotifNode(entry, this))
-            }
-        }
+    private fun onNewNotifTree(notifList: List<ListEntry>) {
+        viewDiffer.applySpec(specBuilder.buildNodeSpec(rootController, notifList))
         notificationIconAreaController.updateNotificationIcons(notifList)
-        return root
-    }
-
-    private fun buildNotifNode(entry: ListEntry, parent: NodeSpec): NodeSpec = when (entry) {
-        is NotificationEntry -> NodeSpecImpl(parent, viewBarn.requireView(entry))
-        is GroupEntry -> NodeSpecImpl(parent, viewBarn.requireView(checkNotNull(entry.summary)))
-                .apply { entry.children.forEach { children.add(buildNotifNode(it, this)) } }
-        else -> throw RuntimeException("Unexpected entry: $entry")
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 6964838..94f5c44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -30,11 +30,12 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserContextProvider;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
@@ -109,7 +110,8 @@
             Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
             LeakDetector leakDetector,
             ForegroundServiceDismissalFeatureController fgsFeatureController,
-            IStatusBarService statusBarService) {
+            IStatusBarService statusBarService,
+            DumpManager dumpManager) {
         return new NotificationEntryManager(
                 logger,
                 groupManager,
@@ -118,7 +120,8 @@
                 notificationRemoteInputManagerLazy,
                 leakDetector,
                 fgsFeatureController,
-                statusBarService);
+                statusBarService,
+                dumpManager);
     }
 
     /** Provides an instance of {@link NotificationGutsManager} */
@@ -126,7 +129,7 @@
     @Provides
     static NotificationGutsManager provideNotificationGutsManager(
             Context context,
-            Lazy<StatusBar> statusBarLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             @Main Handler mainHandler,
             @Background Handler bgHandler,
             AccessibilityManager accessibilityManager,
@@ -142,10 +145,11 @@
             Optional<BubblesManager> bubblesManagerOptional,
             UiEventLogger uiEventLogger,
             OnUserInteractionCallback onUserInteractionCallback,
-            ShadeController shadeController) {
+            ShadeController shadeController,
+            DumpManager dumpManager) {
         return new NotificationGutsManager(
                 context,
-                statusBarLazy,
+                statusBarOptionalLazy,
                 mainHandler,
                 bgHandler,
                 accessibilityManager,
@@ -161,23 +165,25 @@
                 bubblesManagerOptional,
                 uiEventLogger,
                 onUserInteractionCallback,
-                shadeController);
+                shadeController,
+                dumpManager);
     }
 
     /** Provides an instance of {@link VisualStabilityManager} */
     @SysUISingleton
     @Provides
     static VisualStabilityManager provideVisualStabilityManager(
-            FeatureFlags featureFlags,
             NotificationEntryManager notificationEntryManager,
             Handler handler,
             StatusBarStateController statusBarStateController,
-            WakefulnessLifecycle wakefulnessLifecycle) {
+            WakefulnessLifecycle wakefulnessLifecycle,
+            DumpManager dumpManager) {
         return new VisualStabilityManager(
                 notificationEntryManager,
                 handler,
                 statusBarStateController,
-                wakefulnessLifecycle);
+                wakefulnessLifecycle,
+                dumpManager);
     }
 
     /** Provides an instance of {@link NotificationLogger} */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 32d90d3..11b0429 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.statusbar.NotificationListener
 import com.android.systemui.statusbar.NotificationPresenter
 import com.android.systemui.statusbar.notification.AnimatedImageNotificationManager
@@ -128,8 +128,7 @@
             groupManagerLegacy.get().setHeadsUpManager(headsUpManager)
             groupAlertTransferHelper.setHeadsUpManager(headsUpManager)
 
-            entryManager.setRanker(legacyRanker)
-            entryManager.attach(notificationListener)
+            entryManager.initialize(notificationListener, legacyRanker)
         }
 
         peopleSpaceWidgetManager.attach(notificationListener)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
index 4d56e60..b1c69e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
@@ -152,7 +152,7 @@
             // Also we should not defer the removal if reordering isn't allowed since otherwise
             // some notifications can't disappear before the panel is closed.
             boolean ignoreEarliestRemovalTime =
-                    mRemoteInputManager.getController().isSpinning(key)
+                    mRemoteInputManager.isSpinning(key)
                             && !FORCE_REMOTE_INPUT_HISTORY
                             || !mVisualStabilityManager.isReorderingAllowed();
             mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 4f3406c..2eb2065 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -21,7 +21,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
-import android.graphics.RectF;
+import android.graphics.Point;
 import android.util.AttributeSet;
 import android.util.MathUtils;
 import android.view.MotionEvent;
@@ -110,7 +110,6 @@
     private Interpolator mCurrentAppearInterpolator;
 
     NotificationBackgroundView mBackgroundNormal;
-    private RectF mAppearAnimationRect = new RectF();
     private float mAnimationTranslationY;
     private boolean mDrawingAppearAnimation;
     private ValueAnimator mAppearAnimator;
@@ -122,13 +121,6 @@
     private long mLastActionUpTime;
 
     private float mNormalBackgroundVisibilityAmount;
-    private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater
-            = new ValueAnimator.AnimatorUpdateListener() {
-        @Override
-        public void onAnimationUpdate(ValueAnimator animation) {
-            setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha());
-        }
-    };
     private FakeShadowView mFakeShadow;
     private int mCurrentBackgroundTint;
     private int mTargetTint;
@@ -137,9 +129,8 @@
     private float mOverrideAmount;
     private boolean mShadowHidden;
     private boolean mIsHeadsUpAnimation;
-    private int mHeadsUpAddStartLocation;
-    private float mHeadsUpLocation;
-    private boolean mIsAppearing;
+    /* In order to track headsup longpress coorindate. */
+    protected Point mTargetPoint;
     private boolean mDismissed;
     private boolean mRefocusOnDismiss;
     private AccessibilityManager mAccessibilityManager;
@@ -151,7 +142,6 @@
         setClipChildren(false);
         setClipToPadding(false);
         updateColors();
-        initDimens();
     }
 
     private void updateColors() {
@@ -163,17 +153,6 @@
                 R.color.notification_ripple_untinted_color);
     }
 
-    private void initDimens() {
-        mHeadsUpAddStartLocation = getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.notification_content_margin_start);
-    }
-
-    @Override
-    public void onDensityOrFontScaleChanged() {
-        super.onDensityOrFontScaleChanged();
-        initDimens();
-    }
-
     /**
      * Reload background colors from resources and invalidate views.
      */
@@ -435,7 +414,6 @@
             Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) {
         enableAppearDrawing(true);
         mIsHeadsUpAnimation = isHeadsUpAnimation;
-        mHeadsUpLocation = endLocation;
         if (mDrawingAppearAnimation) {
             startAppearAnimation(false /* isAppearing */, translationDirection,
                     delay, duration, onFinishedRunnable, animationListener);
@@ -449,7 +427,6 @@
     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
         enableAppearDrawing(true);
         mIsHeadsUpAnimation = isHeadsUpAppear;
-        mHeadsUpLocation = mHeadsUpAddStartLocation;
         if (mDrawingAppearAnimation) {
             startAppearAnimation(true /* isAppearing */, isHeadsUpAppear ? 0.0f : -1.0f, delay,
                     duration, null, null);
@@ -471,7 +448,6 @@
                 mAppearAnimationTranslation = 0;
             }
         }
-        mIsAppearing = isAppearing;
 
         float targetValue;
         if (isAppearing) {
@@ -521,8 +497,8 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 mWasCancelled = false;
-                Configuration.Builder builder = new Configuration.Builder(getCujType(isAppearing))
-                        .setView(ActivatableNotificationView.this);
+                Configuration.Builder builder = Configuration.Builder
+                        .withView(getCujType(isAppearing), ActivatableNotificationView.this);
                 InteractionJankMonitor.getInstance().begin(builder);
             }
 
@@ -568,8 +544,19 @@
         final int actualHeight = getActualHeight();
         float bottom = actualHeight * interpolatedFraction;
 
-        setOutlineRect(0, mAppearAnimationTranslation, getWidth(),
-                bottom + mAppearAnimationTranslation);
+        if (mTargetPoint != null) {
+            int width = getWidth();
+            float fraction = 1 - mAppearAnimationFraction;
+
+            setOutlineRect(mTargetPoint.x * fraction,
+                    mAnimationTranslationY
+                            + (mAnimationTranslationY - mTargetPoint.y) * fraction,
+                    width - (width - mTargetPoint.x) * fraction,
+                    actualHeight - (actualHeight - mTargetPoint.y) * fraction);
+        } else {
+            setOutlineRect(0, mAppearAnimationTranslation, getWidth(),
+                    bottom + mAppearAnimationTranslation);
+        }
     }
 
     private float getInterpolatedAppearAnimationFraction() {
@@ -768,8 +755,4 @@
         void onActivated(ActivatableNotificationView view);
         void onActivationReset(ActivatableNotificationView view);
     }
-
-    interface OnDimmedListener {
-        void onSetDimmed(boolean dimmed);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 73bb6cd..0d8e850 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -37,6 +37,7 @@
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Path;
+import android.graphics.Point;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.AnimationDrawable;
 import android.graphics.drawable.ColorDrawable;
@@ -260,6 +261,8 @@
     // Use #setLongPressPosition to optionally assign positional data with the long press.
     private LongPressListener mLongPressListener;
 
+    private ExpandableNotificationRowDragController mDragController;
+
     private boolean mGroupExpansionChanging;
 
     /**
@@ -331,6 +334,7 @@
                 }
             };
     private OnClickListener mOnClickListener;
+    private OnDragSuccessListener mOnDragSuccessListener;
     private boolean mHeadsupDisappearRunning;
     private View mChildAfterViewWhenDismissed;
     private View mGroupParentWhenDismissed;
@@ -1083,6 +1087,10 @@
         mLongPressListener = longPressListener;
     }
 
+    public void setDragController(ExpandableNotificationRowDragController dragController) {
+        mDragController = dragController;
+    }
+
     @Override
     public void setOnClickListener(@Nullable OnClickListener l) {
         super.setOnClickListener(l);
@@ -1329,6 +1337,7 @@
     public void dismiss(boolean refocusOnDismiss) {
         super.dismiss(refocusOnDismiss);
         setLongPressListener(null);
+        setDragController(null);
         mGroupParentWhenDismissed = mNotificationParent;
         mChildAfterViewWhenDismissed = null;
         mEntry.getIcons().getStatusBarIcon().setDismissed();
@@ -1637,6 +1646,8 @@
         }
         onHeightReset();
         requestLayout();
+
+        setTargetPoint(null);
     }
 
     public void showFeedbackIcon(boolean show, Pair<Integer, Integer> resIds) {
@@ -1727,6 +1738,29 @@
         mTranslateableViews.remove(mGutsStub);
     }
 
+    /**
+     * Called once when starting drag motion after opening notification guts,
+     * in case of notification that has {@link android.app.Notification#contentIntent}
+     * and it is to start an activity.
+     */
+    public void doDragCallback(float x, float y) {
+        if (mDragController != null) {
+            setTargetPoint(new Point((int) x, (int) y));
+            mDragController.startDragAndDrop(this);
+        }
+    }
+
+    public void setOnDragSuccessListener(OnDragSuccessListener listener) {
+        mOnDragSuccessListener = listener;
+    }
+
+    /**
+     * Called when a notification is dropped on proper target window.
+     */
+    public void dragAndDropSuccess() {
+        mOnDragSuccessListener.onDragSuccess(getEntry());
+    }
+
     private void doLongClickCallback() {
         doLongClickCallback(getWidth() / 2, getHeight() / 2);
     }
@@ -3255,6 +3289,16 @@
     }
 
     /**
+     * Called when notification drag and drop is finished successfully.
+     */
+    public interface OnDragSuccessListener {
+        /**
+         * @param entry NotificationEntry that succeed to drop on proper target window.
+         */
+        void onDragSuccess(NotificationEntry entry);
+    }
+
+    /**
      * Equivalent to View.OnClickListener with coordinates
      */
     public interface CoordinateOnClickListener {
@@ -3321,4 +3365,11 @@
             }
         }
     }
+
+    private void setTargetPoint(Point p) {
+        mTargetPoint = p;
+    }
+    public Point getTargetPoint() {
+        return mTargetPoint;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index c9fcdac8..0662a1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -25,6 +25,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -85,6 +86,8 @@
     private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
     private final Optional<BubblesManager> mBubblesManagerOptional;
 
+    private final ExpandableNotificationRowDragController mDragController;
+
     @Inject
     public ExpandableNotificationRowController(
             ExpandableNotificationRow view,
@@ -109,7 +112,8 @@
             FalsingManager falsingManager,
             FalsingCollector falsingCollector,
             PeopleNotificationIdentifier peopleNotificationIdentifier,
-            Optional<BubblesManager> bubblesManagerOptional) {
+            Optional<BubblesManager> bubblesManagerOptional,
+            ExpandableNotificationRowDragController dragController) {
         mView = view;
         mListContainer = listContainer;
         mActivatableNotificationViewController = activatableNotificationViewController;
@@ -134,6 +138,7 @@
         mFalsingCollector = falsingCollector;
         mPeopleNotificationIdentifier = peopleNotificationIdentifier;
         mBubblesManagerOptional = bubblesManagerOptional;
+        mDragController = dragController;
     }
 
     /**
@@ -164,6 +169,10 @@
         );
         mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
         if (mAllowLongPress) {
+            if (mView.getResources().getBoolean(R.bool.config_notificationToContents)) {
+                mView.setDragController(mDragController);
+            }
+
             mView.setLongPressListener((v, x, y, item) -> {
                 if (mView.isSummaryWithChildren()) {
                     mView.expandNotification();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
new file mode 100644
index 0000000..06b739b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.HapticFeedbackConstants;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.ShadeController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import javax.inject.Inject;
+
+/**
+ * Controller for Notification to window.
+ */
+public class ExpandableNotificationRowDragController {
+    private static final String TAG = ExpandableNotificationRowDragController.class.getSimpleName();
+    private int mIconSize;
+
+    private final Context mContext;
+    private final HeadsUpManager mHeadsUpManager;
+
+    @Inject
+    public ExpandableNotificationRowDragController(Context context,
+            HeadsUpManager headsUpManager) {
+        mContext = context;
+        mHeadsUpManager = headsUpManager;
+
+        init();
+    }
+
+    private void init() {
+        mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.drag_and_drop_icon_size);
+    }
+
+    /**
+     * Called when drag event beyond the touchslop,
+     * and start drag and drop.
+     *
+     * @param view notification that was long pressed and started to drag and drop.
+     */
+    @VisibleForTesting
+    public void startDragAndDrop(View view) {
+        ExpandableNotificationRow enr = null;
+        if (view instanceof ExpandableNotificationRow) {
+            enr = (ExpandableNotificationRow) view;
+        }
+
+        StatusBarNotification sn = enr.getEntry().getSbn();
+        Notification notification = sn.getNotification();
+        final PendingIntent contentIntent = notification.contentIntent != null
+                ? notification.contentIntent
+                : notification.fullScreenIntent;
+        Bitmap iconBitmap = getBitmapFromDrawable(
+                getPkgIcon(enr.getEntry().getSbn().getPackageName()));
+
+        final ImageView snapshot = new ImageView(mContext);
+        snapshot.setImageBitmap(iconBitmap);
+        snapshot.layout(0, 0, mIconSize, mIconSize);
+
+        ClipDescription clipDescription = new ClipDescription("Drag And Drop",
+                new String[]{ClipDescription.MIMETYPE_APPLICATION_ACTIVITY});
+        Intent dragIntent = new Intent();
+        dragIntent.putExtra("android.intent.extra.PENDING_INTENT", contentIntent);
+        dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
+        ClipData.Item item = new ClipData.Item(dragIntent);
+        ClipData dragData = new ClipData(clipDescription, item);
+        View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
+        view.setOnDragListener(getDraggedViewDragListener());
+        view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL);
+    }
+
+
+    private Drawable getPkgIcon(String pkgName) {
+        Drawable pkgicon = null;
+        PackageManager pm = mContext.getPackageManager();
+        ApplicationInfo info;
+        try {
+            info = pm.getApplicationInfo(
+                    pkgName,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES
+                            | PackageManager.MATCH_DISABLED_COMPONENTS
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+            if (info != null) {
+                pkgicon = pm.getApplicationIcon(info);
+            } else {
+                Log.d(TAG, " application info is null ");
+                pkgicon = pm.getDefaultActivityIcon();
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "can not find package with : " + pkgName);
+            pkgicon = pm.getDefaultActivityIcon();
+        }
+
+        return pkgicon;
+    }
+
+    private Bitmap getBitmapFromDrawable(@NonNull Drawable drawable) {
+        final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(bmp);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bmp;
+    }
+
+    private View.OnDragListener getDraggedViewDragListener() {
+        return (view, dragEvent) -> {
+            switch (dragEvent.getAction()) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                    if (view instanceof ExpandableNotificationRow) {
+                        ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
+                        if (enr.isPinned()) {
+                            mHeadsUpManager.releaseAllImmediately();
+                        } else {
+                            Dependency.get(ShadeController.class).animateCollapsePanels(
+                                    CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
+                        }
+                    }
+                    return true;
+                case DragEvent.ACTION_DRAG_ENDED:
+                    if (dragEvent.getResult()) {
+                        if (view instanceof ExpandableNotificationRow) {
+                            ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
+                            enr.dragAndDropSuccess();
+                        }
+                    }
+                    return true;
+            }
+            return false;
+        };
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 73c4b05..1530e523 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -874,7 +874,7 @@
             mRow.getImageResolver().purgeCache();
         }
 
-        private class RtlEnabledContext extends ContextWrapper {
+        private static class RtlEnabledContext extends ContextWrapper {
             private RtlEnabledContext(Context packageContext) {
                 super(packageContext);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 4319e29..7eec95a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -50,6 +50,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -116,7 +117,7 @@
     @VisibleForTesting
     protected String mKeyToRemoveOnGutsClosed;
 
-    private final Lazy<StatusBar> mStatusBarLazy;
+    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private final Handler mMainHandler;
     private final Handler mBgHandler;
     private final Optional<BubblesManager> mBubblesManagerOptional;
@@ -135,7 +136,7 @@
      * Injected constructor. See {@link NotificationsModule}.
      */
     public NotificationGutsManager(Context context,
-            Lazy<StatusBar> statusBarLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             @Main Handler mainHandler,
             @Background Handler bgHandler,
             AccessibilityManager accessibilityManager,
@@ -151,9 +152,10 @@
             Optional<BubblesManager> bubblesManagerOptional,
             UiEventLogger uiEventLogger,
             OnUserInteractionCallback onUserInteractionCallback,
-            ShadeController shadeController) {
+            ShadeController shadeController,
+            DumpManager dumpManager) {
         mContext = context;
-        mStatusBarLazy = statusBarLazy;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
         mMainHandler = mainHandler;
         mBgHandler = bgHandler;
         mAccessibilityManager = accessibilityManager;
@@ -171,6 +173,8 @@
         mOnUserInteractionCallback = onUserInteractionCallback;
         mShadeController = shadeController;
         mAppWidgetManager = AppWidgetManager.getInstance(context);
+
+        dumpManager.registerDumpable(this);
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter,
@@ -561,17 +565,22 @@
                             .setLeaveOpenOnKeyguardHide(true);
                 }
 
-                Runnable r = () -> mMainHandler.post(
-                        () -> openGutsInternal(view, x, y, menuItem));
-
-                mStatusBarLazy.get().executeRunnableDismissingKeyguard(
-                        r,
-                        null /* cancelAction */,
-                        false /* dismissShade */,
-                        true /* afterKeyguardGone */,
-                        true /* deferred */);
-
-                return true;
+                Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
+                if (statusBarOptional.isPresent()) {
+                    Runnable r = () -> mMainHandler.post(
+                            () -> openGutsInternal(view, x, y, menuItem));
+                    statusBarOptional.get().executeRunnableDismissingKeyguard(
+                            r,
+                            null /* cancelAction */,
+                            false /* dismissShade */,
+                            true /* afterKeyguardGone */,
+                            true /* deferred */);
+                    return true;
+                }
+                /**
+                 * When {@link StatusBar} doesn't exist, falling through to call
+                 * {@link #openGutsInternal(View,int,int,NotificationMenuRowPlugin.MenuItem)}.
+                 */
             }
         }
         return openGutsInternal(view, x, y, menuItem);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index c3dc700..d59318e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -41,8 +41,11 @@
 import android.widget.FrameLayout.LayoutParams;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.statusbar.AlphaOptimizedImageView;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -306,7 +309,7 @@
         final int showDismissSetting =  Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.SHOW_NEW_NOTIF_DISMISS, -1);
         final boolean newFlowHideShelf = showDismissSetting == -1
-                ? mContext.getResources().getBoolean(R.bool.flag_notif_updates)
+                ? Dependency.get(FeatureFlags.class).isEnabled(Flags.NOTIFICATION_UPDATES)
                 : showDismissSetting == 1;
         if (newFlowHideShelf) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 41c88cc..a9cc3237 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -72,8 +72,6 @@
     private boolean mPanelFullWidth;
     private boolean mPulsing;
     private boolean mUnlockHintRunning;
-    private boolean mQsCustomizerShowing;
-    private int mIntrinsicPadding;
     private float mHideAmount;
     private boolean mAppearing;
     private float mPulseHeight = MAX_PULSE_HEIGHT;
@@ -83,6 +81,7 @@
     private float mAppearFraction;
     private boolean mIsShadeOpening;
     private float mOverExpansion;
+    private int mStackTopMargin;
 
     /** Distance of top of notifications panel from top of screen. */
     private float mStackY = 0;
@@ -95,7 +94,6 @@
 
     /** Height of the notifications panel without top padding when expansion completes. */
     private float mStackEndHeight;
-    private float mTransitionToFullShadeAmount;
 
     /**
      * @return Height of the notifications panel without top padding when expansion completes.
@@ -494,22 +492,6 @@
         return mUnlockHintRunning;
     }
 
-    public boolean isQsCustomizerShowing() {
-        return mQsCustomizerShowing;
-    }
-
-    public void setQsCustomizerShowing(boolean qsCustomizerShowing) {
-        mQsCustomizerShowing = qsCustomizerShowing;
-    }
-
-    public void setIntrinsicPadding(int intrinsicPadding) {
-        mIntrinsicPadding = intrinsicPadding;
-    }
-
-    public int getIntrinsicPadding() {
-        return mIntrinsicPadding;
-    }
-
     /**
      * @return whether a view is dozing and not pulsing right now
      */
@@ -586,30 +568,11 @@
         mOnPulseHeightChangedListener = onPulseHeightChangedListener;
     }
 
-    public Runnable getOnPulseHeightChangedListener() {
-        return mOnPulseHeightChangedListener;
-    }
-
     public void setTrackedHeadsUpRow(ExpandableNotificationRow row) {
         mTrackedHeadsUpRow = row;
     }
 
     /**
-     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
-     * shade. 0.0f means we're not transitioning yet.
-     */
-    public void setTransitionToFullShadeAmount(float transitionToFullShadeAmount) {
-        mTransitionToFullShadeAmount = transitionToFullShadeAmount;
-    }
-
-    /**
-     * get
-     */
-    public float getTransitionToFullShadeAmount() {
-        return mTransitionToFullShadeAmount;
-    }
-
-    /**
      * Returns the currently tracked heads up row, if there is one and it is currently above the
      * shelf (still appearing).
      */
@@ -631,4 +594,12 @@
     public void setHasAlertEntries(boolean hasAlertEntries) {
         mHasAlertEntries = hasAlertEntries;
     }
+
+    public void setStackTopMargin(int stackTopMargin) {
+        mStackTopMargin = stackTopMargin;
+    }
+
+    public int getStackTopMargin() {
+        return mStackTopMargin;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index 594afce..faf0fdf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -21,6 +21,8 @@
 
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -41,6 +43,7 @@
     private final ExpandableView[] mTmpFirstInSectionViews;
     private final ExpandableView[] mTmpLastInSectionViews;
     private final KeyguardBypassController mBypassController;
+    private final FeatureFlags mFeatureFlags;
     private boolean mExpanded;
     private HashSet<ExpandableView> mAnimatedChildren;
     private Runnable mRoundingChangedCallback;
@@ -55,7 +58,9 @@
     @Inject
     NotificationRoundnessManager(
             KeyguardBypassController keyguardBypassController,
-            NotificationSectionsFeatureManager sectionsFeatureManager) {
+            NotificationSectionsFeatureManager sectionsFeatureManager,
+            FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
         int numberOfSections = sectionsFeatureManager.getNumberOfBuckets();
         mFirstInSectionViews = new ExpandableView[numberOfSections];
         mLastInSectionViews = new ExpandableView[numberOfSections];
@@ -122,9 +127,8 @@
     void setViewsAffectedBySwipe(
             ExpandableView viewBefore,
             ExpandableView viewSwiped,
-            ExpandableView viewAfter,
-            boolean cornerAnimationsEnabled) {
-        if (!cornerAnimationsEnabled) {
+            ExpandableView viewAfter) {
+        if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_UPDATES)) {
             return;
         }
         final boolean animate = true;
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 733c0a9..3e67f7e 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
@@ -81,15 +81,11 @@
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.EmptyShadeView;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.NotificationShelfController;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 import com.android.systemui.statusbar.notification.FakeShadowView;
-import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.ShadeViewRefactor;
@@ -112,7 +108,6 @@
 import com.android.systemui.statusbar.policy.HeadsUpUtil;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 import com.android.systemui.util.Assert;
-import com.android.systemui.util.leak.RotationUtils;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -160,7 +155,7 @@
      * gap is drawn between them). In this case we don't want to round their corners.
      */
     private static final int DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX = 1;
-    private KeyguardBypassEnabledProvider mKeyguardBypassEnabledProvider;
+    private boolean mKeyguardBypassEnabled;
 
     private ExpandHelper mExpandHelper;
     private NotificationSwipeHelper mSwipeHelper;
@@ -217,7 +212,6 @@
 
     private GroupMembershipManager mGroupMembershipManager;
     private GroupExpansionManager mGroupExpansionManager;
-    private NotificationActivityStarter mNotificationActivityStarter;
     private HashSet<ExpandableView> mChildrenToAddAnimated = new HashSet<>();
     private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>();
     private ArrayList<ExpandableView> mChildrenToRemoveAnimated = new ArrayList<>();
@@ -417,6 +411,7 @@
     private boolean mBackwardScrollable;
     private NotificationShelf mShelf;
     private int mMaxDisplayedNotifications = -1;
+    private float mKeyguardBottomPadding = -1;
     private int mStatusBarHeight;
     private int mMinInteractionHeight;
     private final Rect mClipRect = new Rect();
@@ -448,7 +443,6 @@
     private final Rect mTmpRect = new Rect();
     private DismissListener mDismissListener;
     private DismissAllAnimationListener mDismissAllAnimationListener;
-    private NotificationRemoteInputManager mRemoteInputManager;
     private ShadeController mShadeController;
     private Consumer<Boolean> mOnStackYChanged;
 
@@ -463,6 +457,7 @@
     private float mLastSentExpandedHeight;
     private boolean mWillExpand;
     private int mGapHeight;
+    private boolean mIsRemoteInputActive;
 
     /**
      * The extra inset during the full shade transition
@@ -534,7 +529,6 @@
     private NotificationEntry mTopHeadsUpEntry;
     private long mNumHeadsUp;
     private NotificationStackScrollLayoutController.TouchHandler mTouchHandler;
-    private final FeatureFlags mFeatureFlags;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private boolean mShouldUseSplitNotificationShade;
 
@@ -569,6 +563,9 @@
         }
     };
 
+    @Nullable
+    private OnClickListener mManageButtonClickListener;
+
     @Inject
     public NotificationStackScrollLayout(
             @Named(VIEW_CONTEXT) Context context,
@@ -577,12 +574,10 @@
             GroupMembershipManager groupMembershipManager,
             GroupExpansionManager groupExpansionManager,
             AmbientState ambientState,
-            FeatureFlags featureFlags,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         super(context, attrs, 0, 0);
         Resources res = getResources();
         mSectionsManager = notificationSectionsManager;
-        mFeatureFlags = featureFlags;
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
         updateSplitNotificationShade();
         mSectionsManager.initialize(this, LayoutInflater.from(context));
@@ -656,12 +651,20 @@
     }
 
     /**
+     * Sets whether keyguard bypass is enabled. If true, this layout will be rendered in bypass
+     * mode when it is on the keyguard.
+     */
+    public void setKeyguardBypassEnabled(boolean isEnabled) {
+        mKeyguardBypassEnabled = isEnabled;
+    }
+
+    /**
      * @return the height at which we will wake up when pulsing
      */
     public float getWakeUpHeight() {
         ExpandableView firstChild = getFirstChildWithBackground();
         if (firstChild != null) {
-            if (mKeyguardBypassEnabledProvider.getBypassEnabled()) {
+            if (mKeyguardBypassEnabled) {
                 return firstChild.getHeadsUpHeightWithoutHeader();
             } else {
                 return firstChild.getCollapsedHeight();
@@ -677,6 +680,10 @@
         mSectionsManager.reinflateViews(LayoutInflater.from(mContext));
     }
 
+    public void setIsRemoteInputActive(boolean isActive) {
+        mIsRemoteInputActive = isActive;
+    }
+
     @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void updateFooter() {
@@ -686,12 +693,11 @@
         // TODO: move this logic to controller, which will invoke updateFooterView directly
         boolean showDismissView = mClearAllEnabled &&
                 mController.hasActiveClearableNotifications(ROWS_ALL);
-        RemoteInputController remoteInputController = mRemoteInputManager.getController();
         boolean showFooterView = (showDismissView || getVisibleNotificationCount() > 0)
                 && mIsCurrentUserSetup  // see: b/193149550
                 && mStatusBarState != StatusBarState.KEYGUARD
                 && !mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()
-                && (remoteInputController == null || !remoteInputController.isRemoteInputActive());
+                && !mIsRemoteInputActive;
         boolean showHistory = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                 Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, UserHandle.USER_CURRENT) == 1;
 
@@ -743,6 +749,16 @@
             mDebugPaint.setColor(Color.YELLOW);
             canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
 
+            y = (int) mMaxLayoutHeight;
+            mDebugPaint.setColor(Color.MAGENTA);
+            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+
+            if (mKeyguardBottomPadding >= 0) {
+                y = getHeight() - (int) mKeyguardBottomPadding;
+                mDebugPaint.setColor(Color.GRAY);
+                canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+            }
+
             y = getHeight() - getEmptyBottomMargin();
             mDebugPaint.setColor(Color.GREEN);
             canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
@@ -790,7 +806,7 @@
             }
         }
         boolean shouldDrawBackground;
-        if (mKeyguardBypassEnabledProvider.getBypassEnabled() && onKeyguard()) {
+        if (mKeyguardBypassEnabled && onKeyguard()) {
             shouldDrawBackground = isPulseExpanding();
         } else {
             shouldDrawBackground = !mAmbientState.isDozing() || anySectionHasVisibleChild;
@@ -905,15 +921,12 @@
     }
 
     private void reinitView() {
-        initView(getContext(), mKeyguardBypassEnabledProvider, mSwipeHelper);
+        initView(getContext(), mSwipeHelper);
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    void initView(Context context,
-            KeyguardBypassEnabledProvider keyguardBypassEnabledProvider,
-            NotificationSwipeHelper swipeHelper) {
+    void initView(Context context, NotificationSwipeHelper swipeHelper) {
         mScroller = new OverScroller(getContext());
-        mKeyguardBypassEnabledProvider = keyguardBypassEnabledProvider;
         mSwipeHelper = swipeHelper;
 
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
@@ -955,7 +968,7 @@
             return;
         }
         // Portrait is easy, just use the dimen for paddings
-        if (RotationUtils.getRotation(mContext) == RotationUtils.ROTATION_NONE) {
+        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
             mSidePaddings = mMinimumPaddings;
             return;
         }
@@ -1353,7 +1366,7 @@
     private void notifyAppearChangedListeners() {
         float appear;
         float expandAmount;
-        if (mKeyguardBypassEnabledProvider.getBypassEnabled() && onKeyguard()) {
+        if (mKeyguardBypassEnabled && onKeyguard()) {
             appear = calculateAppearFractionBypass();
             expandAmount = getPulseHeight();
         } else {
@@ -2396,8 +2409,7 @@
             minTopPosition = firstVisibleSection.getBounds().top;
         }
         boolean shiftPulsingWithFirst = mNumHeadsUp <= 1
-                && (mAmbientState.isDozing()
-                        || (mKeyguardBypassEnabledProvider.getBypassEnabled() && onKeyguard));
+                && (mAmbientState.isDozing() || (mKeyguardBypassEnabled && onKeyguard));
         for (NotificationSection section : mSections) {
             int minBottomPosition = minTopPosition;
             if (section == lastSection) {
@@ -2540,7 +2552,7 @@
         } else {
             mTopPaddingOverflow = 0;
         }
-        setTopPadding(topPadding, animate && !mKeyguardBypassEnabledProvider.getBypassEnabled());
+        setTopPadding(topPadding, animate && !mKeyguardBypassEnabled);
         setExpandedHeight(mExpandedHeight);
     }
 
@@ -3104,7 +3116,7 @@
             boolean performDisappearAnimation = !mIsExpanded
                     // Only animate if we still have pinned heads up, otherwise we just have the
                     // regular collapse animation of the lock screen
-                    || (mKeyguardBypassEnabledProvider.getBypassEnabled() && onKeyguard()
+                    || (mKeyguardBypassEnabled && onKeyguard()
                             && mInHeadsUpPinnedMode);
             if (performDisappearAnimation && !isHeadsUp) {
                 type = row.wasJustClicked()
@@ -4275,7 +4287,6 @@
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     void setIntrinsicPadding(int intrinsicPadding) {
         mIntrinsicPadding = intrinsicPadding;
-        mAmbientState.setIntrinsicPadding(intrinsicPadding);
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
@@ -4340,7 +4351,7 @@
         // Since we are clipping to the outline we need to make sure that the shadows aren't
         // clipped when pulsing
         float ownTranslationZ = 0;
-        if (mKeyguardBypassEnabledProvider.getBypassEnabled() && mAmbientState.isHiddenAtAll()) {
+        if (mKeyguardBypassEnabled && mAmbientState.isHiddenAtAll()) {
             ExpandableView firstChildNotGone = getFirstChildNotGone();
             if (firstChildNotGone != null && firstChildNotGone.showingPulsing()) {
                 ownTranslationZ = firstChildNotGone.getTranslationZ();
@@ -4382,6 +4393,14 @@
         return -1;
     }
 
+    /**
+     * Returns whether or not a History button is shown in the footer. If there is no footer, then
+     * this will return false.
+     **/
+    public boolean isHistoryShown() {
+        return mFooterView != null && mFooterView.isHistoryShown();
+    }
+
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     void setFooterView(@NonNull FooterView footerView) {
         int index = -1;
@@ -4391,6 +4410,9 @@
         }
         mFooterView = footerView;
         addView(mFooterView, index);
+        if (mManageButtonClickListener != null) {
+            mFooterView.setManageButtonClickListener(mManageButtonClickListener);
+        }
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -4797,6 +4819,16 @@
         }
     }
 
+    /**
+     * This is used for debugging only; it will be used to draw the otherwise invisible line which
+     * NotificationPanelViewController treats as the bottom when calculating how many notifications
+     * appear on the keyguard.
+     * Setting a negative number will disable rendering this line.
+     */
+    public void setKeyguardBottomPadding(float keyguardBottomPadding) {
+        mKeyguardBottomPadding = keyguardBottomPadding;
+    }
+
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setShouldShowShelfOnly(boolean shouldShowShelfOnly) {
         mShouldShowShelfOnly = shouldShowShelfOnly;
@@ -4879,25 +4911,18 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void setQsCustomizerShowing(boolean isShowing) {
-        mAmbientState.setQsCustomizerShowing(isShowing);
-        requestChildrenUpdate();
-    }
-
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) {
         mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed;
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.println(String.format("[%s: pulsing=%s qsCustomizerShowing=%s visibility=%s"
+        pw.println(String.format("[%s: pulsing=%s visibility=%s"
                         + " alpha=%f scrollY:%d maxTopPadding=%d showShelfOnly=%s"
                         + " qsExpandFraction=%f"
                         + " hideAmount=%f]",
                 this.getClass().getSimpleName(),
                 mPulsing ? "T" : "f",
-                mAmbientState.isQsCustomizerShowing() ? "T" : "f",
                 getVisibility() == View.VISIBLE ? "visible"
                         : getVisibility() == View.GONE ? "gone"
                                 : "invisible",
@@ -5103,9 +5128,12 @@
         return canChildBeDismissed(row) && matchesSelection(row, selection);
     }
 
-    public void setNotificationActivityStarter(
-            NotificationActivityStarter notificationActivityStarter) {
-        mNotificationActivityStarter = notificationActivityStarter;
+    /** Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. */
+    public void setManageButtonClickListener(@Nullable OnClickListener listener) {
+        mManageButtonClickListener = listener;
+        if (mFooterView != null) {
+            mFooterView.setManageButtonClickListener(mManageButtonClickListener);
+        }
     }
 
     @VisibleForTesting
@@ -5120,9 +5148,6 @@
             clearNotifications(ROWS_ALL, true /* closeShade */);
             footerView.setSecondaryVisible(false /* visible */, true /* animate */);
         });
-        footerView.setManageButtonClickListener(v -> {
-            mNotificationActivityStarter.startHistoryIntent(v, mFooterView.isHistoryShown());
-        });
         setFooterView(footerView);
     }
 
@@ -5174,7 +5199,7 @@
     public float setPulseHeight(float height) {
         float overflow;
         mAmbientState.setPulseHeight(height);
-        if (mKeyguardBypassEnabledProvider.getBypassEnabled()) {
+        if (mKeyguardBypassEnabled) {
             notifyAppearChangedListeners();
             overflow = Math.max(0, height - getIntrinsicPadding());
         } else {
@@ -5286,10 +5311,10 @@
             }
         }
         mController.getNoticationRoundessManager()
-                .setViewsAffectedBySwipe((ExpandableView) viewBefore,
+                .setViewsAffectedBySwipe(
+                        (ExpandableView) viewBefore,
                         (ExpandableView) viewSwiped,
-                        (ExpandableView) viewAfter,
-                        getResources().getBoolean(R.bool.flag_notif_updates));
+                        (ExpandableView) viewAfter);
 
         updateFirstAndLastBackgroundViews();
         requestDisallowInterceptTouchEvent(true);
@@ -5301,8 +5326,7 @@
     void onSwipeEnd() {
         updateFirstAndLastBackgroundViews();
         mController.getNoticationRoundessManager()
-                .setViewsAffectedBySwipe(null, null, null,
-                        getResources().getBoolean(R.bool.flag_notif_updates));
+                .setViewsAffectedBySwipe(null, null, null);
         // Round bottom corners for notification right before shelf.
         mShelf.updateAppearance();
     }
@@ -5376,10 +5400,6 @@
         mFooterDismissListener = listener;
     }
 
-    public void setRemoteInputManager(NotificationRemoteInputManager remoteInputManager) {
-        mRemoteInputManager = remoteInputManager;
-    }
-
     void setShadeController(ShadeController shadeController) {
         mShadeController = shadeController;
     }
@@ -5431,7 +5451,7 @@
     }
 
     private void updateSplitNotificationShade() {
-        boolean split = shouldUseSplitNotificationShade(mFeatureFlags, getResources());
+        boolean split = shouldUseSplitNotificationShade(getResources());
         if (split != mShouldUseSplitNotificationShade) {
             mShouldUseSplitNotificationShade = split;
             updateDismissBehavior();
@@ -5688,6 +5708,10 @@
         mSwipeHelper.resetExposedMenuView(animate, force);
     }
 
+    boolean isUsingSplitNotificationShade() {
+        return mShouldUseSplitNotificationShade;
+    }
+
     static boolean matchesSelection(
             ExpandableNotificationRow row,
             @SelectedRows int selection) {
@@ -6112,10 +6136,6 @@
     /** Only rows where entry.isHighPriority() is false. */
     public static final int ROWS_GENTLE = 2;
 
-    interface KeyguardBypassEnabledProvider {
-        boolean getBypassEnabled();
-    }
-
     interface DismissListener {
         void onDismiss(@SelectedRows int selectedRows);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 1e92ca9..b226aec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -48,6 +48,8 @@
 import android.view.ViewGroup;
 import android.view.WindowInsets;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.jank.InteractionJankMonitor;
@@ -66,13 +68,13 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
@@ -185,9 +187,14 @@
     private int mBarState;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
 
+    private View mLongPressedView;
+
     private final NotificationListContainerImpl mNotificationListContainer =
             new NotificationListContainerImpl();
 
+    @Nullable
+    private NotificationActivityStarter mNotificationActivityStarter;
+
     private ColorExtractor.OnColorsChangedListener mOnColorsChangedListener;
 
     /**
@@ -265,15 +272,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            updateShowEmptyShadeView();
-            mView.updateCornerRadius();
-            mView.updateBgColor();
-            mView.updateDecorViews();
-            mView.reinflateViews();
-        }
-
-        @Override
         public void onUiModeChanged() {
             mView.updateBgColor();
             mView.updateDecorViews();
@@ -281,6 +279,11 @@
 
         @Override
         public void onThemeChanged() {
+            updateShowEmptyShadeView();
+            mView.updateCornerRadius();
+            mView.updateBgColor();
+            mView.updateDecorViews();
+            mView.reinflateViews();
             updateFooter();
         }
 
@@ -513,6 +516,11 @@
                 }
 
                 @Override
+                public void onLongPressSent(View v) {
+                    mLongPressedView = v;
+                }
+
+                @Override
                 public void onBeginDrag(View v) {
                     mFalsingCollector.onNotificationStartDismissing();
                     mView.onSwipeBegin(v);
@@ -707,7 +715,13 @@
                 NotificationPanelEvent.fromSelection(selection)));
         mView.setFooterDismissListener(() ->
                 mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
-        mView.setRemoteInputManager(mRemoteInputManager);
+        mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
+        mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
+            @Override
+            public void onRemoteInputActive(boolean active) {
+                mView.setIsRemoteInputActive(active);
+            }
+        });
         mView.setShadeController(mShadeController);
 
         if (mFgFeatureController.isForegroundServiceDismissalEnabled()) {
@@ -738,8 +752,15 @@
             });
         }
 
-        mView.initView(mView.getContext(), mKeyguardBypassController::getBypassEnabled,
-                mSwipeHelper);
+        mView.initView(mView.getContext(), mSwipeHelper);
+        mView.setKeyguardBypassEnabled(mKeyguardBypassController.getBypassEnabled());
+        mKeyguardBypassController
+                .registerOnBypassStateChangedListener(mView::setKeyguardBypassEnabled);
+        mView.setManageButtonClickListener(v -> {
+            if (mNotificationActivityStarter != null) {
+                mNotificationActivityStarter.startHistoryIntent(v, mView.isHistoryShown());
+            }
+        });
 
         mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
         mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
@@ -1145,11 +1166,16 @@
     /**
      * Update whether we should show the empty shade view (no notifications in the shade).
      * If so, send the update to our view.
+     *
+     * When in split mode, notifications are always visible regardless of the state of the
+     * QuickSettings panel. That being the case, empty view is always shown if the other conditions
+     * are true.
      */
     public void updateShowEmptyShadeView() {
         mShowEmptyShadeView = mBarState != KEYGUARD
-                && !mView.isQsExpanded()
+                && (!mView.isQsExpanded() || mView.isUsingSplitNotificationShade())
                 && mView.getVisibleNotificationCount() == 0;
+
         mView.updateEmptyShadeView(
                 mShowEmptyShadeView,
                 mZenModeController.areNotificationsHiddenInShade());
@@ -1243,6 +1269,16 @@
         mNotificationListContainer.setMaxDisplayedNotifications(maxNotifications);
     }
 
+    /**
+     * This is used for debugging only; it will be used to draw the otherwise invisible line which
+     * NotificationPanelViewController treats as the bottom when calculating how many notifications
+     * appear on the keyguard.
+     * Setting a negative number will disable rendering this line.
+     */
+    public void setKeyguardBottomPadding(float keyguardBottomPadding) {
+        mView.setKeyguardBottomPadding(keyguardBottomPadding);
+    }
+
     public RemoteInputController.Delegate createDelegate() {
         return new RemoteInputController.Delegate() {
             public void setRemoteInputActive(NotificationEntry entry,
@@ -1450,6 +1486,10 @@
         return mDynamicPrivacyController.isInLockedDownShade();
     }
 
+    public boolean isLongPressInProgress() {
+        return mLongPressedView != null;
+    }
+
     /**
      * Set the dimmed state for all of the notification views.
      */
@@ -1487,6 +1527,11 @@
         mView.setExtraTopInsetForFullShadeTransition(extraTopInset);
     }
 
+    /** */
+    public void setWillExpand(boolean willExpand) {
+        mView.setWillExpand(willExpand);
+    }
+
     /**
      * Set a listener to when scrolling changes.
      */
@@ -1509,6 +1554,10 @@
         mView.animateNextTopPaddingChange();
     }
 
+    public void setNotificationActivityStarter(NotificationActivityStarter activityStarter) {
+        mNotificationActivityStarter = activityStarter;
+    }
+
     /**
      * Enum for UiEvent logged from this class
      */
@@ -1580,7 +1629,8 @@
         @Override
         public void setNotificationActivityStarter(
                 NotificationActivityStarter notificationActivityStarter) {
-            mView.setNotificationActivityStarter(notificationActivityStarter);
+            NotificationStackScrollLayoutController.this
+                    .setNotificationActivityStarter(notificationActivityStarter);
         }
 
         @Override
@@ -1694,17 +1744,23 @@
             mView.handleEmptySpaceClick(ev);
 
             NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
+
+            boolean longPressWantsIt = false;
+            if (mLongPressedView != null) {
+                longPressWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
+            }
             boolean expandWantsIt = false;
-            if (!mSwipeHelper.isSwiping()
+            if (mLongPressedView == null && !mSwipeHelper.isSwiping()
                     && !mView.getOnlyScrollingInThisMotion() && guts == null) {
                 expandWantsIt = mView.getExpandHelper().onInterceptTouchEvent(ev);
             }
             boolean scrollWantsIt = false;
-            if (!mSwipeHelper.isSwiping() && !mView.isExpandingNotification()) {
+            if (mLongPressedView == null && !mSwipeHelper.isSwiping()
+                    && !mView.isExpandingNotification()) {
                 scrollWantsIt = mView.onInterceptTouchEventScroll(ev);
             }
             boolean swipeWantsIt = false;
-            if (!mView.isBeingDragged()
+            if (mLongPressedView == null && !mView.isBeingDragged()
                     && !mView.isExpandingNotification()
                     && !mView.getExpandedInThisMotion()
                     && !mView.getOnlyScrollingInThisMotion()
@@ -1732,7 +1788,7 @@
                 InteractionJankMonitor.getInstance().begin(mView,
                         CUJ_NOTIFICATION_SHADE_SCROLL_FLING);
             }
-            return swipeWantsIt || scrollWantsIt || expandWantsIt;
+            return swipeWantsIt || scrollWantsIt || expandWantsIt || longPressWantsIt;
         }
 
         @Override
@@ -1741,11 +1797,15 @@
             boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
                     || ev.getActionMasked() == MotionEvent.ACTION_UP;
             mView.handleEmptySpaceClick(ev);
+            boolean longPressWantsIt = false;
+            if (guts != null && mLongPressedView != null) {
+                longPressWantsIt = mSwipeHelper.onTouchEvent(ev);
+            }
             boolean expandWantsIt = false;
             boolean onlyScrollingInThisMotion = mView.getOnlyScrollingInThisMotion();
             boolean expandingNotification = mView.isExpandingNotification();
-            if (mView.getIsExpanded() && !mSwipeHelper.isSwiping() && !onlyScrollingInThisMotion
-                    && guts == null) {
+            if (mLongPressedView == null && mView.getIsExpanded()
+                    && !mSwipeHelper.isSwiping() && !onlyScrollingInThisMotion && guts == null) {
                 ExpandHelper expandHelper = mView.getExpandHelper();
                 if (isCancelOrUp) {
                     expandHelper.onlyObserveMovements(false);
@@ -1759,12 +1819,12 @@
                 }
             }
             boolean scrollerWantsIt = false;
-            if (mView.isExpanded() && !mSwipeHelper.isSwiping() && !expandingNotification
-                    && !mView.getDisallowScrollingInThisMotion()) {
+            if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
+                    && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
                 scrollerWantsIt = mView.onScrollTouch(ev);
             }
             boolean horizontalSwipeWantsIt = false;
-            if (!mView.isBeingDragged()
+            if (mLongPressedView == null && !mView.isBeingDragged()
                     && !expandingNotification
                     && !mView.getExpandedInThisMotion()
                     && !onlyScrollingInThisMotion
@@ -1790,7 +1850,7 @@
                 mView.setCheckForLeaveBehind(true);
             }
             traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt);
-            return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt;
+            return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt;
         }
 
         private void traceJankOnTouchEvent(int action, boolean scrollerWantsIt) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 8be5de7..0accce8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -24,8 +24,10 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
+import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -54,8 +56,7 @@
     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
     private boolean mIsExpanded;
     private boolean mClipNotificationScrollToTop;
-    private int mStatusBarHeight;
-    private float mHeadsUpInset;
+    @VisibleForTesting float mHeadsUpInset;
     private int mPinnedZTranslationExtra;
     private float mNotificationScrimPadding;
 
@@ -75,9 +76,9 @@
         mPaddingBetweenElements = res.getDimensionPixelSize(
                 R.dimen.notification_divider_height);
         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
-        mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
         mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
-        mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize(
+        int statusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
+        mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize(
                 R.dimen.heads_up_status_bar_padding);
         mPinnedZTranslationExtra = res.getDimensionPixelSize(
                 R.dimen.heads_up_pinned_elevation);
@@ -407,8 +408,8 @@
             viewState.alpha = 1f - ambientState.getHideAmount();
         } else if (ambientState.isExpansionChanging()) {
             // Adjust alpha for shade open & close.
-            viewState.alpha = Interpolators.getNotificationScrimAlpha(
-                    ambientState.getExpansionFraction(), true /* notification */);
+            float expansion = ambientState.getExpansionFraction();
+            viewState.alpha = ShadeInterpolation.getContentAlpha(expansion);
         }
 
         if (ambientState.isShadeExpanded() && view.mustStayOnScreen()
@@ -562,13 +563,14 @@
 
         // Move the tracked heads up into position during the appear animation, by interpolating
         // between the HUN inset (where it will appear as a HUN) and the end position in the shade
+        float headsUpTranslation = mHeadsUpInset - ambientState.getStackTopMargin();
         ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
         if (trackedHeadsUpRow != null) {
             ExpandableViewState childState = trackedHeadsUpRow.getViewState();
             if (childState != null) {
                 float endPosition = childState.yTranslation - ambientState.getStackTranslation();
                 childState.yTranslation = MathUtils.lerp(
-                        mHeadsUpInset, endPosition, ambientState.getAppearFraction());
+                        headsUpTranslation, endPosition, ambientState.getAppearFraction());
             }
         }
 
@@ -602,7 +604,7 @@
                 }
             }
             if (row.isPinned()) {
-                childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset);
+                childState.yTranslation = Math.max(childState.yTranslation, headsUpTranslation);
                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
                 childState.hidden = false;
                 ExpandableViewState topState =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index 4466cfe..e3a4bf0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -433,6 +433,7 @@
                     if (row.isDismissed()) {
                         needsAnimation = false;
                     }
+
                     NotificationEntry entry = row.getEntry();
                     StatusBarIconView icon = entry.getIcons().getStatusBarIcon();
                     final StatusBarIconView centeredIcon = entry.getIcons().getCenteredIcon();
@@ -442,7 +443,8 @@
                     if (icon.getParent() != null) {
                         icon.getLocationOnScreen(mTmpLocation);
                         float iconPosition = mTmpLocation[0] - icon.getTranslationX()
-                                + ViewState.getFinalTranslationX(icon) + icon.getWidth() * 0.25f;
+                                + ViewState.getFinalTranslationX(icon)
+                                + icon.getWidth() * 0.25f;
                         mHostLayout.getLocationOnScreen(mTmpLocation);
                         targetLocation = iconPosition - mTmpLocation[0];
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
index aeb2efd..111cbbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
@@ -37,6 +37,7 @@
     private final Handler mHandler;
 
     private AutoHideUiElement mStatusBar;
+    /** For tablets, this will represent the Taskbar */
     private AutoHideUiElement mNavigationBar;
     private int mDisplayId;
 
@@ -89,7 +90,7 @@
         }
     }
 
-    void resumeSuspendedAutoHide() {
+    public void resumeSuspendedAutoHide() {
         if (mAutoHideSuspended) {
             scheduleAutoHide();
             Runnable checkBarModesRunnable = getCheckBarModesRunnable();
@@ -99,7 +100,7 @@
         }
     }
 
-    void suspendAutoHide() {
+    public void suspendAutoHide() {
         mHandler.removeCallbacks(mAutoHide);
         Runnable checkBarModesRunnable = getCheckBarModesRunnable();
         if (checkBarModesRunnable != null) {
@@ -171,4 +172,23 @@
 
         return false;
     }
+
+    /**
+     * Injectable factory for creating a {@link AutoHideController}.
+     */
+    public static class Factory {
+        private final Handler mHandler;
+        private final IWindowManager mIWindowManager;
+
+        @Inject
+        public Factory(@Main Handler handler, IWindowManager iWindowManager) {
+            mHandler = handler;
+            mIWindowManager = iWindowManager;
+        }
+
+        /** Create an {@link AutoHideController} */
+        public AutoHideController create(Context context) {
+            return new AutoHideController(context, mHandler, mIWindowManager);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
index 6d12a1c..989f6b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -39,10 +39,15 @@
 
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.DisableFlagsLogger.DisableState;
+import com.android.systemui.statusbar.OperatorNameView;
+import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
@@ -50,16 +55,17 @@
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
 import com.android.systemui.statusbar.policy.EncryptionHelper;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
 
 import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /**
  * Contains the collapsed status bar and handles hiding/showing based on disable flags
  * and keyguard state. Also manages lifecycle to make sure the views it contains are being
@@ -85,10 +91,11 @@
     private View mCenteredIconArea;
     private int mDisabled1;
     private int mDisabled2;
-    private final StatusBar mStatusBarComponent;
+    private Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private DarkIconManager mDarkIconManager;
-    private View mOperatorNameFrame;
     private final CommandQueue mCommandQueue;
+    private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger;
+    private final OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
     private final OngoingCallController mOngoingCallController;
     private final SystemStatusAnimationScheduler mAnimationScheduler;
     private final StatusBarLocationPublisher mLocationPublisher;
@@ -111,6 +118,7 @@
             disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate);
         }
     };
+    private OperatorNameViewController mOperatorNameViewController;
 
     @Inject
     public CollapsedStatusBarFragment(
@@ -119,24 +127,28 @@
             StatusBarLocationPublisher locationPublisher,
             NotificationIconAreaController notificationIconAreaController,
             FeatureFlags featureFlags,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             StatusBarIconController statusBarIconController,
             KeyguardStateController keyguardStateController,
             NetworkController networkController,
             StatusBarStateController statusBarStateController,
-            StatusBar statusBarComponent,
-            CommandQueue commandQueue
+            CommandQueue commandQueue,
+            CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger,
+            OperatorNameViewController.Factory operatorNameViewControllerFactory
     ) {
         mOngoingCallController = ongoingCallController;
         mAnimationScheduler = animationScheduler;
         mLocationPublisher = locationPublisher;
         mNotificationIconAreaController = notificationIconAreaController;
         mFeatureFlags = featureFlags;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
         mStatusBarIconController = statusBarIconController;
         mKeyguardStateController = keyguardStateController;
         mNetworkController = networkController;
         mStatusBarStateController = statusBarStateController;
-        mStatusBarComponent = statusBarComponent;
         mCommandQueue = commandQueue;
+        mCollapsedStatusBarFragmentLogger = collapsedStatusBarFragmentLogger;
+        mOperatorNameViewControllerFactory = operatorNameViewControllerFactory;
     }
 
     @Override
@@ -236,7 +248,14 @@
         if (displayId != getContext().getDisplayId()) {
             return;
         }
+
+        int state1BeforeAdjustment = state1;
         state1 = adjustDisableFlags(state1);
+
+        mCollapsedStatusBarFragmentLogger.logDisableFlagChange(
+                new DisableState(state1BeforeAdjustment, state2),
+                new DisableState(state1, state2));
+
         final int old1 = mDisabled1;
         final int diff1 = state1 ^ old1;
         final int old2 = mDisabled2;
@@ -272,7 +291,8 @@
     }
 
     protected int adjustDisableFlags(int state) {
-        boolean headsUpVisible = mStatusBarComponent.headsUpShouldBeVisible();
+        boolean headsUpVisible = mStatusBarOptionalLazy.get()
+                .map(StatusBar::headsUpShouldBeVisible).orElse(false);
         if (headsUpVisible) {
             state |= DISABLE_CLOCK;
         }
@@ -300,7 +320,8 @@
         // The shelf will be hidden when dozing with a custom clock, we must show notification
         // icons in this occasion.
         if (mStatusBarStateController.isDozing()
-                && mStatusBarComponent.getPanelController().hasCustomClock()) {
+                && mStatusBarOptionalLazy.get().map(
+                        sb -> sb.getPanelController().hasCustomClock()).orElse(false)) {
             state |= DISABLE_CLOCK | DISABLE_SYSTEM_INFO;
         }
 
@@ -341,10 +362,13 @@
     }
 
     private boolean shouldHideNotificationIcons() {
-        if (!mStatusBar.isClosed() && mStatusBarComponent.hideStatusBarIconsWhenExpanded()) {
+        final Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
+        if (!mStatusBar.isClosed()
+                && statusBarOptional.map(
+                        StatusBar::hideStatusBarIconsWhenExpanded).orElse(false)) {
             return true;
         }
-        if (mStatusBarComponent.hideStatusBarIconsForBouncer()) {
+        if (statusBarOptional.map(StatusBar::hideStatusBarIconsForBouncer).orElse(false)) {
             return true;
         }
         return false;
@@ -403,14 +427,14 @@
     }
 
     public void hideOperatorName(boolean animate) {
-        if (mOperatorNameFrame != null) {
-            animateHide(mOperatorNameFrame, animate);
+        if (mOperatorNameViewController != null) {
+            animateHide(mOperatorNameViewController.getView(), animate);
         }
     }
 
     public void showOperatorName(boolean animate) {
-        if (mOperatorNameFrame != null) {
-            animateShow(mOperatorNameFrame, animate);
+        if (mOperatorNameViewController != null) {
+            animateShow(mOperatorNameViewController.getView(), animate);
         }
     }
 
@@ -487,7 +511,9 @@
     private void initOperatorName() {
         if (getResources().getBoolean(R.bool.config_showOperatorNameInStatusBar)) {
             ViewStub stub = mStatusBar.findViewById(R.id.operator_name);
-            mOperatorNameFrame = stub.inflate();
+            mOperatorNameViewController =
+                    mOperatorNameViewControllerFactory.create((OperatorNameView) stub.inflate());
+            mOperatorNameViewController.init();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLogger.kt
new file mode 100644
index 0000000..3c2b555
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLogger.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.CollapsedSbFragmentLog
+import com.android.systemui.statusbar.DisableFlagsLogger
+import javax.inject.Inject
+
+/** Used by [CollapsedStatusBarFragment] to log messages to a [LogBuffer]. */
+class CollapsedStatusBarFragmentLogger @Inject constructor(
+        @CollapsedSbFragmentLog private val buffer: LogBuffer,
+        private val disableFlagsLogger: DisableFlagsLogger,
+) {
+
+    /** Logs a string representing the old and new disable flag states to [buffer]. */
+    fun logDisableFlagChange(
+            oldState: DisableFlagsLogger.DisableState,
+            newState: DisableFlagsLogger.DisableState) {
+        buffer.log(
+                TAG,
+                LogLevel.INFO,
+                {
+                    int1 = oldState.disable1
+                    int2 = oldState.disable2
+                    long1 = newState.disable1.toLong()
+                    long2 = newState.disable2.toLong()
+                },
+                {
+                    disableFlagsLogger.getDisableFlagsString(
+                            DisableFlagsLogger.DisableState(int1, int2),
+                            DisableFlagsLogger.DisableState(long1.toInt(), long2.toInt())
+                    )
+                }
+        )
+    }
+}
+
+private const val TAG = "CollapsedSbFragment"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
index 07618da..96fa8a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
@@ -17,6 +17,7 @@
 import android.content.Context
 import android.content.pm.ActivityInfo
 import android.content.res.Configuration
+import android.graphics.Rect
 import android.os.LocaleList
 import android.view.View.LAYOUT_DIRECTION_RTL
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -29,6 +30,7 @@
     private val lastConfig = Configuration()
     private var density: Int = 0
     private var smallestScreenWidth: Int = 0
+    private var maxBounds: Rect? = null
     private var fontScale: Float = 0.toFloat()
     private val inCarMode: Boolean
     private var uiMode: Int = 0
@@ -85,6 +87,14 @@
             }
         }
 
+        val maxBounds = newConfig.windowConfiguration.maxBounds
+        if (maxBounds != this.maxBounds) {
+            this.maxBounds = maxBounds
+            listeners.filterForEach({ this.listeners.contains(it) }) {
+                it.onMaxBoundsChanged()
+            }
+        }
+
         val localeList = newConfig.locales
         if (localeList != this.localeList) {
             this.localeList = localeList
@@ -113,7 +123,7 @@
 
         if (lastConfig.updateFrom(newConfig) and ActivityInfo.CONFIG_ASSETS_PATHS != 0) {
             listeners.filterForEach({ this.listeners.contains(it) }) {
-                it.onOverlayChanged()
+                it.onThemeChanged()
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
index f25359e..d06de75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java
@@ -25,6 +25,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.CommandQueue;
 
 import java.io.FileDescriptor;
@@ -50,11 +51,16 @@
     /**
      */
     @Inject
-    public DarkIconDispatcherImpl(Context context, CommandQueue commandQueue) {
+    public DarkIconDispatcherImpl(
+            Context context,
+            CommandQueue commandQueue,
+            DumpManager dumpManager) {
         mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone);
         mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone);
 
         mTransitionsController = new LightBarTransitionsController(context, this, commandQueue);
+
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     public LightBarTransitionsController getTransitionsController() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index b4f8126..908cd34 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -29,9 +29,9 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.systemui.R;
 import com.android.systemui.demomode.DemoMode;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarMobileView;
 import com.android.systemui.statusbar.StatusBarWifiView;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index e67c6ac..49e3fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -22,6 +22,7 @@
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.Log;
 import android.util.MathUtils;
 
 import androidx.annotation.NonNull;
@@ -33,8 +34,9 @@
 import com.android.systemui.doze.AlwaysOnDisplayPolicy;
 import com.android.systemui.doze.DozeScreenState;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.tuner.TunerService;
 
 import java.io.FileDescriptor;
@@ -48,8 +50,10 @@
  * Retrieve doze information
  */
 @SysUISingleton
-public class DozeParameters implements TunerService.Tunable,
-        com.android.systemui.plugins.statusbar.DozeParameters, Dumpable {
+public class DozeParameters implements
+        TunerService.Tunable,
+        com.android.systemui.plugins.statusbar.DozeParameters,
+        Dumpable {
     private static final int MAX_DURATION = 60 * 1000;
     public static final boolean FORCE_NO_BLANKING =
             SystemProperties.getBoolean("debug.force_no_blanking", false);
@@ -254,9 +258,20 @@
     }
 
     /**
+     * Whether the single tap sensor uses the proximity sensor for this device posture.
+     */
+    public boolean singleTapUsesProx(@DevicePostureController.DevicePostureInt int devicePosture) {
+        return getPostureSpecificBool(
+                mResources.getIntArray(R.array.doze_single_tap_uses_prox_posture_mapping),
+                singleTapUsesProx(),
+                devicePosture
+        );
+    }
+
+    /**
      * Whether the single tap sensor uses the proximity sensor.
      */
-    public boolean singleTapUsesProx() {
+    private boolean singleTapUsesProx() {
         return mResources.getBoolean(R.bool.doze_single_tap_uses_prox);
     }
 
@@ -268,6 +283,15 @@
     }
 
     /**
+     * Gets the brightness string array per posture. Brightness names along with
+     * doze_brightness_sensor_type is used to determine the brightness sensor to use for
+     * the current posture.
+     */
+    public String[] brightnessNames() {
+        return mResources.getStringArray(R.array.doze_brightness_sensor_name_posture_mapping);
+    }
+
+    /**
      * Callback to listen for DozeParameter changes.
      */
     public void addCallback(Callback callback) {
@@ -306,6 +330,20 @@
         pw.println(getSelectivelyRegisterSensorsUsingProx());
     }
 
+    private boolean getPostureSpecificBool(
+            int[] postureMapping,
+            boolean defaultSensorBool,
+            int posture) {
+        boolean bool = defaultSensorBool;
+        if (posture < postureMapping.length) {
+            bool = postureMapping[posture] != 0;
+        } else {
+            Log.e("DozeParameters", "Unsupported doze posture " + posture);
+        }
+
+        return bool;
+    }
+
     interface Callback {
         /**
          * Invoked when the value of getAlwaysOn may have changed.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index f289b9f..57b9c03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -226,11 +226,11 @@
             return;
         }
 
-        if (reason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
+        if (reason == DozeLog.PULSE_REASON_SENSOR_WAKE_REACH) {
             mScrimController.setWakeLockScreenSensorActive(true);
         }
 
-        boolean passiveAuthInterrupt = reason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN
+        boolean passiveAuthInterrupt = reason == DozeLog.PULSE_REASON_SENSOR_WAKE_REACH
                         && mWakeLockScreenPerformsAuth;
         // Set the state to pulsing, so ScrimController will know what to do once we ask it to
         // execute the transition. The pulse callback will then be invoked when the scrims
@@ -329,7 +329,7 @@
 
     @Override
     public void extendPulse(int reason) {
-        if (reason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
+        if (reason == DozeLog.PULSE_REASON_SENSOR_WAKE_REACH) {
             mScrimController.setWakeLockScreenSensorActive(true);
         }
         if (mDozeScrimController.isPulsing() && mHeadsUpManagerPhone.hasNotifications()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 878fbbf..927b4c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -163,7 +163,6 @@
         mHeadsUpStatusBarView.setOnDrawingRectChangedListener(null);
         mWakeUpCoordinator.removeListener(this);
         mNotificationPanelViewController.removeTrackingHeadsUpListener(mSetTrackingHeadsUp);
-        mNotificationPanelViewController.setVerticalTranslationListener(null);
         mNotificationPanelViewController.setHeadsUpAppearanceController(null);
         mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight);
         mDarkIconDispatcher.removeDarkReceiver(this);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 4b545eb..5f402d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -121,7 +121,7 @@
             }
 
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 updateResources();
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 4701d8b..c639eec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -80,6 +80,7 @@
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.camera.CameraIntents;
@@ -1046,11 +1047,13 @@
             return;
         }
 
+        ActivityLaunchAnimator.Controller animationController = createLaunchAnimationController(v);
         if (mHasCard) {
             Intent intent = new Intent(mContext, WalletActivity.class)
                     .setAction(Intent.ACTION_VIEW)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
-            mContext.startActivity(intent);
+            mActivityStarter.startActivity(intent, true /* dismissShade */, animationController,
+                    true /* showOverLockscreenWhenLocked */);
         } else {
             if (mQuickAccessWalletController.getWalletClient().createWalletIntent() == null) {
                 Log.w(TAG, "Could not get intent of the wallet app.");
@@ -1058,10 +1061,14 @@
             }
             mActivityStarter.postStartActivityDismissingKeyguard(
                     mQuickAccessWalletController.getWalletClient().createWalletIntent(),
-                    /* delay= */ 0);
+                    /* delay= */ 0, animationController);
         }
     }
 
+    protected ActivityLaunchAnimator.Controller createLaunchAnimationController(View view) {
+        return ActivityLaunchAnimator.Controller.fromView(view, null);
+    }
+
     private void onControlsClick(View v) {
         if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index 8c0dfc5..a090ac3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -126,6 +126,19 @@
         mExpansionCallbacks.add(expansionCallback);
     }
 
+    /**
+     * Enable/disable only the back button
+     */
+    public void setBackButtonEnabled(boolean enabled) {
+        int vis = mContainer.getSystemUiVisibility();
+        if (enabled) {
+            vis &= ~View.STATUS_BAR_DISABLE_BACK;
+        } else {
+            vis |= View.STATUS_BAR_DISABLE_BACK;
+        }
+        mContainer.setSystemUiVisibility(vis);
+    }
+
     public void show(boolean resetSecuritySelection) {
         show(resetSecuritySelection, true /* scrimmed */);
     }
@@ -252,8 +265,6 @@
                 mKeyguardViewController.resetSecurityContainer();
                 showPromptReason(mBouncerPromptReason);
             }
-            SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
-                    SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN);
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index 3a4a819..3d5f755 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -43,6 +43,11 @@
     @BypassOverride private val bypassOverride: Int
     private var hasFaceFeature: Boolean
     private var pendingUnlock: PendingUnlock? = null
+    private val listeners = mutableListOf<OnBypassStateChangedListener>()
+
+    private val faceAuthEnabledChangedCallback = object : KeyguardStateController.Callback {
+        override fun onFaceAuthEnabledChanged() = notifyListeners()
+    }
     var userHasDeviceEntryIntent: Boolean = false // ie: attempted udfps auth
 
     @IntDef(
@@ -84,7 +89,10 @@
             }
             return enabled && mKeyguardStateController.isFaceAuthEnabled
         }
-        private set
+        private set(value) {
+            field = value
+            notifyListeners()
+        }
 
     var bouncerShowing: Boolean = false
     var altBouncerShowing: Boolean = false
@@ -141,6 +149,8 @@
                 })
     }
 
+    private fun notifyListeners() = listeners.forEach { it.onBypassStateChanged(bypassEnabled) }
+
     /**
      * Notify that the biometric unlock has happened.
      *
@@ -225,6 +235,32 @@
         pw.println("  userHasDeviceEntryIntent: $userHasDeviceEntryIntent")
     }
 
+    /** Registers a listener for bypass state changes. */
+    fun registerOnBypassStateChangedListener(listener: OnBypassStateChangedListener) {
+        val start = listeners.isEmpty()
+        listeners.add(listener)
+        if (start) {
+            mKeyguardStateController.addCallback(faceAuthEnabledChangedCallback)
+        }
+    }
+
+    /**
+     * Unregisters a listener for bypass state changes, previous registered with
+     * [registerOnBypassStateChangedListener]
+     */
+    fun unregisterOnBypassStateChangedListener(listener: OnBypassStateChangedListener) {
+        listeners.remove(listener)
+        if (listeners.isEmpty()) {
+            mKeyguardStateController.removeCallback(faceAuthEnabledChangedCallback)
+        }
+    }
+
+    /** Listener for bypass state change events.  */
+    interface OnBypassStateChangedListener {
+        /** Invoked when bypass becomes enabled or disabled. */
+        fun onBypassStateChanged(isEnabled: Boolean)
+    }
+
     companion object {
         const val BYPASS_FADE_DURATION = 67
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index b58cab4..e368aad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -32,12 +32,6 @@
  * Utility class to calculate the clock position and top padding of notifications on Keyguard.
  */
 public class KeyguardClockPositionAlgorithm {
-    /**
-     * How much the clock height influences the shade position.
-     * 0 means nothing, 1 means move the shade up by the height of the clock
-     * 0.5f means move the shade up by half of the size of the clock.
-     */
-    private static float CLOCK_HEIGHT_WEIGHT = 0.7f;
 
     /**
      * Margin between the bottom of the status view and the notification shade.
@@ -45,11 +39,6 @@
     private int mStatusViewBottomMargin;
 
     /**
-     * Height of the parent view - display size in px.
-     */
-    private int mHeight;
-
-    /**
      * Height of {@link KeyguardStatusView}.
      */
     private int mKeyguardStatusHeight;
@@ -83,7 +72,8 @@
     private int mNotificationStackHeight;
 
     /**
-     * Minimum top margin to avoid overlap with status bar, or multi-user switcher avatar.
+     * Minimum top margin to avoid overlap with status bar, lock icon, or multi-user switcher
+     * avatar.
      */
     private int mMinTopMargin;
 
@@ -93,12 +83,6 @@
     private int mCutoutTopInset = 0;
 
     /**
-     * Maximum bottom padding to avoid overlap with {@link KeyguardBottomAreaView} or
-     * the ambient indication.
-     */
-    private int mMaxShadeBottom;
-
-    /**
      * Recommended distance from the status bar.
      */
     private int mContainerTopPadding;
@@ -114,14 +98,9 @@
     private int mBurnInPreventionOffsetX;
 
     /**
-     * Burn-in prevention y translation.
+     * Burn-in prevention y translation for clock layouts.
      */
-    private int mBurnInPreventionOffsetY;
-
-    /**
-     * Burn-in prevention y translation for large clock layouts.
-     */
-    private int mBurnInPreventionOffsetYLargeClock;
+    private int mBurnInPreventionOffsetYClock;
 
     /**
      * Doze/AOD transition amount.
@@ -178,35 +157,26 @@
                 res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
         mBurnInPreventionOffsetX = res.getDimensionPixelSize(
                 R.dimen.burn_in_prevention_offset_x);
-        mBurnInPreventionOffsetY = res.getDimensionPixelSize(
-                R.dimen.burn_in_prevention_offset_y);
-        mBurnInPreventionOffsetYLargeClock = res.getDimensionPixelSize(
-                R.dimen.burn_in_prevention_offset_y_large_clock);
+        mBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
+                R.dimen.burn_in_prevention_offset_y_clock);
     }
 
     /**
      * Sets up algorithm values.
      */
-    public void setup(int keyguardStatusBarHeaderHeight, int maxShadeBottom,
-            int notificationStackHeight, float panelExpansion, int parentHeight,
+    public void setup(int keyguardStatusBarHeaderHeight, float panelExpansion,
             int keyguardStatusHeight, int userSwitchHeight, int userSwitchPreferredY,
-            boolean hasCustomClock, boolean hasVisibleNotifs, float dark,
-            float overStrechAmount, boolean bypassEnabled, int unlockedStackScrollerPadding,
-            float qsExpansion, int cutoutTopInset, boolean isSplitShade, float udfpsTop,
-            float clockBottom, boolean isClockTopAligned) {
+            float dark, float overStretchAmount, boolean bypassEnabled,
+            int unlockedStackScrollerPadding, float qsExpansion, int cutoutTopInset,
+            boolean isSplitShade, float udfpsTop, float clockBottom, boolean isClockTopAligned) {
         mMinTopMargin = keyguardStatusBarHeaderHeight + Math.max(mContainerTopPadding,
                 userSwitchHeight);
-        mMaxShadeBottom = maxShadeBottom;
-        mNotificationStackHeight = notificationStackHeight;
         mPanelExpansion = panelExpansion;
-        mHeight = parentHeight;
         mKeyguardStatusHeight = keyguardStatusHeight + mStatusViewBottomMargin;
         mUserSwitchHeight = userSwitchHeight;
         mUserSwitchPreferredY = userSwitchPreferredY;
-        mHasCustomClock = hasCustomClock;
-        mHasVisibleNotifs = hasVisibleNotifs;
         mDarkAmount = dark;
-        mOverStretchAmount = overStrechAmount;
+        mOverStretchAmount = overStretchAmount;
         mBypassEnabled = bypassEnabled;
         mUnlockedStackScrollerPadding = unlockedStackScrollerPadding;
         mQsExpansion = qsExpansion;
@@ -266,11 +236,11 @@
 
         // This will keep the clock at the top but out of the cutout area
         float shift = 0;
-        if (clockY - mBurnInPreventionOffsetYLargeClock < mCutoutTopInset) {
-            shift = mCutoutTopInset - (clockY - mBurnInPreventionOffsetYLargeClock);
+        if (clockY - mBurnInPreventionOffsetYClock < mCutoutTopInset) {
+            shift = mCutoutTopInset - (clockY - mBurnInPreventionOffsetYClock);
         }
 
-        int burnInPreventionOffsetY = mBurnInPreventionOffsetYLargeClock; // requested offset
+        int burnInPreventionOffsetY = mBurnInPreventionOffsetYClock; // requested offset
         final boolean hasUdfps = mUdfpsTop > -1;
         if (hasUdfps && !mIsClockTopAligned) {
             // ensure clock doesn't overlap with the udfps icon
@@ -278,8 +248,8 @@
                 // sometimes the clock textView extends beyond udfps, so let's just use the
                 // space above the KeyguardStatusView/clock as our burn-in offset
                 burnInPreventionOffsetY = (int) (clockY - mCutoutTopInset) / 2;
-                if (mBurnInPreventionOffsetYLargeClock < burnInPreventionOffsetY) {
-                    burnInPreventionOffsetY = mBurnInPreventionOffsetYLargeClock;
+                if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+                    burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
                 }
                 shift = -burnInPreventionOffsetY;
             } else {
@@ -287,8 +257,8 @@
                 float lowerSpace = mUdfpsTop - mClockBottom;
                 // center the burn-in offset within the upper + lower space
                 burnInPreventionOffsetY = (int) (lowerSpace + upperSpace) / 2;
-                if (mBurnInPreventionOffsetYLargeClock < burnInPreventionOffsetY) {
-                    burnInPreventionOffsetY = mBurnInPreventionOffsetYLargeClock;
+                if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+                    burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
                 }
                 shift = (lowerSpace - upperSpace) / 2;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
index a5b5f1c..3a68b9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
@@ -95,44 +95,85 @@
     }
 
     /**
-     * Changes the text with an animation and makes sure a single indication is shown long enough.
+     * Changes the text with an animation. Makes sure a single indication is shown long enough.
+     */
+    public void switchIndication(CharSequence text, KeyguardIndication indication) {
+        switchIndication(text, indication, true, null);
+    }
+
+    /**
+     * Changes the text with an optional animation. For animating text, makes sure a single
+     * indication is shown long enough.
      *
      * @param text The text to show.
      * @param indication optional display information for the text
+     * @param animate whether to animate this indication in - we may not want this on AOD
+     * @param onAnimationEndCallback runnable called after this indication is animated in
      */
-    public void switchIndication(CharSequence text, KeyguardIndication indication) {
+    public void switchIndication(CharSequence text, KeyguardIndication indication,
+            boolean animate, Runnable onAnimationEndCallback) {
         if (text == null) text = "";
 
         CharSequence lastPendingMessage = mMessages.peekLast();
         if (TextUtils.equals(lastPendingMessage, text)
                 || (lastPendingMessage == null && TextUtils.equals(text, getText()))) {
+            if (onAnimationEndCallback != null) {
+                onAnimationEndCallback.run();
+            }
             return;
         }
         mMessages.add(text);
         mKeyguardIndicationInfo.add(indication);
 
-        final boolean hasIcon = indication != null && indication.getIcon() != null;
-        final AnimatorSet animSet = new AnimatorSet();
-        final AnimatorSet.Builder animSetBuilder = animSet.play(getOutAnimator());
+        if (animate) {
+            final boolean hasIcon = indication != null && indication.getIcon() != null;
+            final AnimatorSet animator = new AnimatorSet();
+            // Make sure each animation is visible for a minimum amount of time, while not worrying
+            // about fading in blank text
+            long timeInMillis = System.currentTimeMillis();
+            long delay = Math.max(0, mNextAnimationTime - timeInMillis);
+            setNextAnimationTime(timeInMillis + delay + getFadeOutDuration());
+            final long minDurationMillis =
+                    (indication != null && indication.getMinVisibilityMillis() != null)
+                            ? indication.getMinVisibilityMillis()
+                            : MSG_MIN_DURATION_MILLIS_DEFAULT;
+            if (!text.equals("") || hasIcon) {
+                setNextAnimationTime(mNextAnimationTime + minDurationMillis);
+                Animator inAnimator = getInAnimator();
+                inAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        super.onAnimationEnd(animation);
+                        if (onAnimationEndCallback != null) {
+                            onAnimationEndCallback.run();
+                        }
+                    }
+                });
+                animator.playSequentially(getOutAnimator(), inAnimator);
+            } else {
+                Animator outAnimator = getOutAnimator();
+                outAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        super.onAnimationEnd(animation);
+                        if (onAnimationEndCallback != null) {
+                            onAnimationEndCallback.run();
+                        }
+                    }
+                });
+                animator.play(outAnimator);
+            }
 
-        // Make sure each animation is visible for a minimum amount of time, while not worrying
-        // about fading in blank text
-        long timeInMillis = System.currentTimeMillis();
-        long delay = Math.max(0, mNextAnimationTime - timeInMillis);
-        setNextAnimationTime(timeInMillis + delay + getFadeOutDuration());
-
-        final long minDurationMillis =
-                (indication != null && indication.getMinVisibilityMillis() != null)
-                    ? indication.getMinVisibilityMillis()
-                    : MSG_MIN_DURATION_MILLIS_DEFAULT;
-
-        if (!text.equals("") || hasIcon) {
-            setNextAnimationTime(mNextAnimationTime + minDurationMillis);
-            animSetBuilder.before(getInAnimator());
+            animator.setStartDelay(delay);
+            animator.start();
+        } else {
+            setAlpha(1f);
+            setTranslationY(0f);
+            setNextIndication();
+            if (onAnimationEndCallback != null) {
+                onAnimationEndCallback.run();
+            }
         }
-
-        animSet.setStartDelay(delay);
-        animSet.start();
     }
 
     private AnimatorSet getOutAnimator() {
@@ -143,29 +184,8 @@
         fadeOut.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animator) {
-                KeyguardIndication info = mKeyguardIndicationInfo.poll();
-                if (info != null) {
-                    // First, update the style.
-                    // If a background is set on the text, we don't want shadow on the text
-                    if (info.getBackground() != null) {
-                        setTextAppearance(sButtonStyleId);
-                    } else {
-                        setTextAppearance(sStyleId);
-                    }
-                    setBackground(info.getBackground());
-                    setTextColor(info.getTextColor());
-                    setOnClickListener(info.getClickListener());
-                    setClickable(info.getClickListener() != null);
-                    final Drawable icon = info.getIcon();
-                    if (icon != null) {
-                        icon.setTint(getCurrentTextColor());
-                        if (icon instanceof AnimatedVectorDrawable) {
-                            ((AnimatedVectorDrawable) icon).start();
-                        }
-                    }
-                    setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
-                }
-                setText(mMessages.poll());
+                super.onAnimationEnd(animator);
+                setNextIndication();
             }
         });
 
@@ -177,6 +197,32 @@
         return animatorSet;
     }
 
+    private void setNextIndication() {
+        KeyguardIndication info = mKeyguardIndicationInfo.poll();
+        if (info != null) {
+            // First, update the style.
+            // If a background is set on the text, we don't want shadow on the text
+            if (info.getBackground() != null) {
+                setTextAppearance(sButtonStyleId);
+            } else {
+                setTextAppearance(sStyleId);
+            }
+            setBackground(info.getBackground());
+            setTextColor(info.getTextColor());
+            setOnClickListener(info.getClickListener());
+            setClickable(info.getClickListener() != null);
+            final Drawable icon = info.getIcon();
+            if (icon != null) {
+                icon.setTint(getCurrentTextColor());
+                if (icon instanceof AnimatedVectorDrawable) {
+                    ((AnimatedVectorDrawable) icon).start();
+                }
+            }
+            setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
+        }
+        setText(mMessages.poll());
+    }
+
     private AnimatorSet getInAnimator() {
         AnimatorSet animatorSet = new AnimatorSet();
         ObjectAnimator fadeIn = ObjectAnimator.ofFloat(this, View.ALPHA, 1f);
@@ -190,6 +236,7 @@
         yTranslate.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationCancel(Animator animation) {
+                super.onAnimationCancel(animation);
                 setTranslationY(0);
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 55e0c9b..fe154d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -18,10 +18,7 @@
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection;
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN;
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT;
 
-import android.animation.ValueAnimator;
 import android.annotation.ColorInt;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -45,36 +42,18 @@
 import android.widget.TextView;
 
 import com.android.settingslib.Utils;
-import com.android.systemui.BatteryMeterView;
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
-import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
-import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
-import com.android.systemui.statusbar.policy.UserInfoController;
-import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
-import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * The header group on Keyguard.
  */
-public class KeyguardStatusBarView extends RelativeLayout implements
-        BatteryStateChangeCallback,
-        OnUserInfoChangedListener,
-        ConfigurationListener,
-        SystemStatusAnimationCallback {
+public class KeyguardStatusBarView extends RelativeLayout {
 
     private static final int LAYOUT_NONE = 0;
     private static final int LAYOUT_CUTOUT = 1;
@@ -84,31 +63,25 @@
 
     private boolean mShowPercentAvailable;
     private boolean mBatteryCharging;
-    private boolean mBatteryListening;
 
     private TextView mCarrierLabel;
     private ImageView mMultiUserAvatar;
     private BatteryMeterView mBatteryView;
     private StatusIconContainer mStatusIconContainer;
 
-    private BatteryController mBatteryController;
     private boolean mKeyguardUserSwitcherEnabled;
     private final UserManager mUserManager;
 
+    private boolean mIsPrivacyDotEnabled;
     private int mSystemIconsSwitcherHiddenExpandedMargin;
     private int mStatusBarPaddingEnd;
     private int mMinDotWidth;
     private View mSystemIconsContainer;
-    private TintedIconManager mIconManager;
-    private List<String> mBlockedIcons = new ArrayList<>();
 
     private View mCutoutSpace;
     private ViewGroup mStatusIconArea;
     private int mLayoutState = LAYOUT_NONE;
 
-    private SystemStatusAnimationScheduler mAnimationScheduler;
-    private FeatureFlags mFeatureFlags;
-
     /**
      * Draw this many pixels into the left/right side of the cutout to optimally use the space
      */
@@ -140,17 +113,14 @@
         mCutoutSpace = findViewById(R.id.cutout_space_view);
         mStatusIconArea = findViewById(R.id.status_icon_area);
         mStatusIconContainer = findViewById(R.id.statusIcons);
-
+        mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot);
         loadDimens();
-        loadBlockList();
-        mBatteryController = Dependency.get(BatteryController.class);
-        mAnimationScheduler = Dependency.get(SystemStatusAnimationScheduler.class);
-        mFeatureFlags = Dependency.get(FeatureFlags.class);
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
+        loadDimens();
 
         MarginLayoutParams lp = (MarginLayoutParams) mMultiUserAvatar.getLayoutParams();
         lp.width = lp.height = getResources().getDimensionPixelSize(
@@ -160,6 +130,22 @@
         // System icons
         updateSystemIconsLayoutParams();
 
+        // mStatusIconArea
+        mStatusIconArea.setPaddingRelative(
+                mStatusIconArea.getPaddingStart(),
+                getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top),
+                mStatusIconArea.getPaddingEnd(),
+                mStatusIconArea.getPaddingBottom()
+        );
+
+        // mStatusIconContainer
+        mStatusIconContainer.setPaddingRelative(
+                mStatusIconContainer.getPaddingStart(),
+                mStatusIconContainer.getPaddingTop(),
+                getResources().getDimensionPixelSize(R.dimen.signal_cluster_battery_padding),
+                mStatusIconContainer.getPaddingBottom()
+        );
+
         // Respect font size setting.
         mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                 getResources().getDimensionPixelSize(
@@ -184,7 +170,7 @@
         setLayoutParams(lp);
     }
 
-    private void loadDimens() {
+    void loadDimens() {
         Resources res = getResources();
         mSystemIconsSwitcherHiddenExpandedMargin = res.getDimensionPixelSize(
                 R.dimen.system_icons_switcher_hidden_expanded_margin);
@@ -200,14 +186,6 @@
                 R.dimen.rounded_corner_content_padding);
     }
 
-    // Set hidden status bar items
-    private void loadBlockList() {
-        Resources r = getResources();
-        mBlockedIcons.add(r.getString(com.android.internal.R.string.status_bar_volume));
-        mBlockedIcons.add(r.getString(com.android.internal.R.string.status_bar_alarm_clock));
-        mBlockedIcons.add(r.getString(com.android.internal.R.string.status_bar_call_strength));
-    }
-
     private void updateVisibilities() {
         if (mMultiUserAvatar.getParent() != mStatusIconArea
                 && !mKeyguardUserSwitcherEnabled) {
@@ -293,9 +271,10 @@
                         mDisplayCutout, cornerCutoutMargins, mRoundedCornerPadding);
 
         // consider privacy dot space
-        final int minLeft = isLayoutRtl() ? Math.max(mMinDotWidth, mPadding.first) : mPadding.first;
-        final int minRight = isLayoutRtl() ? mPadding.second :
-                Math.max(mMinDotWidth, mPadding.second);
+        final int minLeft = (isLayoutRtl() && mIsPrivacyDotEnabled)
+                ? Math.max(mMinDotWidth, mPadding.first) : mPadding.first;
+        final int minRight = (!isLayoutRtl() && mIsPrivacyDotEnabled)
+                ? Math.max(mMinDotWidth, mPadding.second) : mPadding.second;
 
         setPadding(minLeft, waterfallTop, minRight, 0);
     }
@@ -358,60 +337,20 @@
         return true;
     }
 
-    public void setListening(boolean listening) {
-        if (listening == mBatteryListening) {
-            return;
-        }
-        mBatteryListening = listening;
-        if (mBatteryListening) {
-            mBatteryController.addCallback(this);
-        } else {
-            mBatteryController.removeCallback(this);
-        }
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        UserInfoController userInfoController = Dependency.get(UserInfoController.class);
-        userInfoController.addCallback(this);
-        userInfoController.reloadUserInfo();
-        Dependency.get(ConfigurationController.class).addCallback(this);
-        mIconManager = new TintedIconManager(findViewById(R.id.statusIcons), mFeatureFlags);
-        mIconManager.setBlockList(mBlockedIcons);
-        Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager);
-        mAnimationScheduler.addCallback(this);
-        onThemeChanged();
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        Dependency.get(UserInfoController.class).removeCallback(this);
-        Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager);
-        Dependency.get(ConfigurationController.class).removeCallback(this);
-        mAnimationScheduler.removeCallback(this);
-    }
-
-    @Override
-    public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
+    /** Should only be called from {@link KeyguardStatusBarViewController}. */
+    void onUserInfoChanged(Drawable picture) {
         mMultiUserAvatar.setImageDrawable(picture);
     }
 
-    @Override
-    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+    /** Should only be called from {@link KeyguardStatusBarViewController}. */
+    void onBatteryLevelChanged(boolean charging) {
         if (mBatteryCharging != charging) {
             mBatteryCharging = charging;
             updateVisibilities();
         }
     }
 
-    @Override
-    public void onPowerSaveChanged(boolean isPowerSave) {
-        // could not care less
-    }
-
-    public void setKeyguardUserSwitcherEnabled(boolean enabled) {
+    void setKeyguardUserSwitcherEnabled(boolean enabled) {
         mKeyguardUserSwitcherEnabled = enabled;
     }
 
@@ -477,28 +416,20 @@
         return false;
     }
 
-    public void onThemeChanged() {
+    /** Should only be called from {@link KeyguardStatusBarViewController}. */
+    void onThemeChanged(StatusBarIconController.TintedIconManager iconManager) {
         mBatteryView.setColorsFromContext(mContext);
-        updateIconsAndTextColors();
-        // Reload user avatar
-        ((UserInfoControllerImpl) Dependency.get(UserInfoController.class))
-                .onDensityOrFontScaleChanged();
+        updateIconsAndTextColors(iconManager);
     }
 
-    @Override
-    public void onDensityOrFontScaleChanged() {
-        loadDimens();
-    }
-
-    @Override
-    public void onOverlayChanged() {
+    /** Should only be called from {@link KeyguardStatusBarViewController}. */
+    void onOverlayChanged() {
         mCarrierLabel.setTextAppearance(
                 Utils.getThemeAttr(mContext, com.android.internal.R.attr.textAppearanceSmall));
-        onThemeChanged();
         mBatteryView.updatePercentView();
     }
 
-    private void updateIconsAndTextColors() {
+    private void updateIconsAndTextColors(StatusBarIconController.TintedIconManager iconManager) {
         @ColorInt int textColor = Utils.getColorAttrDefaultColor(mContext,
                 R.attr.wallpaperTextColor);
         @ColorInt int iconColor = Utils.getColorStateListDefaultColor(mContext,
@@ -506,8 +437,8 @@
                 R.color.light_mode_icon_color_single_tone);
         float intensity = textColor == Color.WHITE ? 0 : 1;
         mCarrierLabel.setTextColor(iconColor);
-        if (mIconManager != null) {
-            mIconManager.setTint(iconColor);
+        if (iconManager != null) {
+            iconManager.setTint(iconColor);
         }
 
         applyDarkness(R.id.battery, mEmptyRect, intensity, iconColor);
@@ -532,10 +463,10 @@
         }
     }
 
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    /** Should only be called from {@link KeyguardStatusBarViewController}. */
+    void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("KeyguardStatusBarView:");
         pw.println("  mBatteryCharging: " + mBatteryCharging);
-        pw.println("  mBatteryListening: " + mBatteryListening);
         pw.println("  mLayoutState: " + mLayoutState);
         pw.println("  mKeyguardUserSwitcherEnabled: " + mKeyguardUserSwitcherEnabled);
         if (mBatteryView != null) {
@@ -543,19 +474,16 @@
         }
     }
 
-    /** SystemStatusAnimationCallback */
-    @Override
-    public void onSystemChromeAnimationStart() {
-        if (mAnimationScheduler.getAnimationState() == ANIMATING_OUT) {
+    void onSystemChromeAnimationStart(boolean isAnimatingOut) {
+        if (isAnimatingOut) {
             mSystemIconsContainer.setVisibility(View.VISIBLE);
             mSystemIconsContainer.setAlpha(0f);
         }
     }
 
-    @Override
-    public void onSystemChromeAnimationEnd() {
+    void onSystemChromeAnimationEnd(boolean isAnimatingIn) {
         // Make sure the system icons are out of the way
-        if (mAnimationScheduler.getAnimationState() == ANIMATING_IN) {
+        if (isAnimatingIn) {
             mSystemIconsContainer.setVisibility(View.INVISIBLE);
             mSystemIconsContainer.setAlpha(0f);
         } else {
@@ -564,9 +492,8 @@
         }
     }
 
-    @Override
-    public void onSystemChromeAnimationUpdate(ValueAnimator anim) {
-        mSystemIconsContainer.setAlpha((float) anim.getAnimatedValue());
+    void onSystemChromeAnimationUpdate(float animatedValue) {
+        mSystemIconsContainer.setAlpha(animatedValue);
     }
 
     @Override
@@ -577,8 +504,10 @@
 
     /**
      * Set the clipping on the top of the view.
+     *
+     * Should only be called from {@link KeyguardStatusBarViewController}.
      */
-    public void setTopClipping(int topClipping) {
+    void setTopClipping(int topClipping) {
         if (topClipping != mTopClipping) {
             mTopClipping = topClipping;
             updateClipping();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 377fb92..9055081 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -16,33 +16,454 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN;
+import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.hardware.biometrics.BiometricSourceType;
+import android.util.MathUtils;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
 import com.android.keyguard.CarrierTextController;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.R;
+import com.android.systemui.animation.Interpolators;
+import com.android.systemui.battery.BatteryMeterViewController;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
+import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
+import com.android.systemui.statusbar.notification.AnimatableProperty;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
+import com.android.systemui.statusbar.notification.stack.AnimationProperties;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.util.ViewController;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
 import javax.inject.Inject;
 
 /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
 public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
+    private static final AnimationProperties KEYGUARD_HUN_PROPERTIES =
+            new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+
+    private float mKeyguardHeadsUpShowingAmount = 0.0f;
+    private final AnimatableProperty mHeadsUpShowingAmountAnimation = AnimatableProperty.from(
+            "KEYGUARD_HEADS_UP_SHOWING_AMOUNT",
+            (view, aFloat) -> {
+                mKeyguardHeadsUpShowingAmount = aFloat;
+                updateViewState();
+            },
+            view -> mKeyguardHeadsUpShowingAmount,
+            R.id.keyguard_hun_animator_tag,
+            R.id.keyguard_hun_animator_end_tag,
+            R.id.keyguard_hun_animator_start_tag);
+
     private final CarrierTextController mCarrierTextController;
+    private final ConfigurationController mConfigurationController;
+    private final SystemStatusAnimationScheduler mAnimationScheduler;
+    private final BatteryController mBatteryController;
+    private final UserInfoController mUserInfoController;
+    private final StatusBarIconController mStatusBarIconController;
+    private final StatusBarIconController.TintedIconManager.Factory mTintedIconManagerFactory;
+    private final BatteryMeterViewController mBatteryMeterViewController;
+    private final NotificationPanelViewController.NotificationPanelViewStateProvider
+            mNotificationPanelViewStateProvider;
+    private final KeyguardStateController mKeyguardStateController;
+    private final KeyguardBypassController mKeyguardBypassController;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final BiometricUnlockController mBiometricUnlockController;
+    private final SysuiStatusBarStateController mStatusBarStateController;
+
+    private final ConfigurationController.ConfigurationListener mConfigurationListener =
+            new ConfigurationController.ConfigurationListener() {
+                @Override
+                public void onDensityOrFontScaleChanged() {
+                    mView.loadDimens();
+                }
+
+                @Override
+                public void onThemeChanged() {
+                    mView.onOverlayChanged();
+                    KeyguardStatusBarViewController.this.onThemeChanged();
+                }
+            };
+
+    private final SystemStatusAnimationCallback mAnimationCallback =
+            new SystemStatusAnimationCallback() {
+                @Override
+                public void onSystemChromeAnimationStart() {
+                    mView.onSystemChromeAnimationStart(
+                            mAnimationScheduler.getAnimationState() == ANIMATING_OUT);
+                }
+
+                @Override
+                public void onSystemChromeAnimationEnd() {
+                    mView.onSystemChromeAnimationEnd(
+                            mAnimationScheduler.getAnimationState() == ANIMATING_IN);
+                }
+
+                @Override
+                public void onSystemChromeAnimationUpdate(@NonNull ValueAnimator anim) {
+                    mView.onSystemChromeAnimationUpdate((float) anim.getAnimatedValue());
+                }
+            };
+
+    private final BatteryController.BatteryStateChangeCallback mBatteryStateChangeCallback =
+            new BatteryController.BatteryStateChangeCallback() {
+                @Override
+                public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+                    mView.onBatteryLevelChanged(charging);
+                }
+            };
+
+    private final UserInfoController.OnUserInfoChangedListener mOnUserInfoChangedListener =
+            (name, picture, userAccount) -> mView.onUserInfoChanged(picture);
+
+    private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener =
+            animation -> {
+                mKeyguardStatusBarAnimateAlpha = (float) animation.getAnimatedValue();
+                updateViewState();
+            };
+
+    private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+                @Override
+                public void onBiometricAuthenticated(
+                        int userId,
+                        BiometricSourceType biometricSourceType,
+                        boolean isStrongBiometric) {
+                    if (mFirstBypassAttempt
+                            && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                                    isStrongBiometric)) {
+                        mDelayShowingKeyguardStatusBar = true;
+                    }
+                }
+
+                @Override
+                public void onBiometricRunningStateChanged(
+                        boolean running,
+                        BiometricSourceType biometricSourceType) {
+                    boolean keyguardOrShadeLocked =
+                            mStatusBarState == KEYGUARD
+                                    || mStatusBarState == StatusBarState.SHADE_LOCKED;
+                    if (!running
+                            && mFirstBypassAttempt
+                            && keyguardOrShadeLocked
+                            && !mDozing
+                            && !mDelayShowingKeyguardStatusBar
+                            && !mBiometricUnlockController.isBiometricUnlock()) {
+                        mFirstBypassAttempt = false;
+                        animateKeyguardStatusBarIn();
+                    }
+                }
+
+                @Override
+                public void onFinishedGoingToSleep(int why) {
+                    mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
+                    mDelayShowingKeyguardStatusBar = false;
+                }
+            };
+
+    private final StatusBarStateController.StateListener mStatusBarStateListener =
+            new StatusBarStateController.StateListener() {
+                @Override
+                public void onStateChanged(int newState) {
+                    mStatusBarState = newState;
+                }
+            };
+
+    private final List<String> mBlockedIcons;
+    private final int mNotificationsHeaderCollideDistance;
+
+    private boolean mBatteryListening;
+    private StatusBarIconController.TintedIconManager mTintedIconManager;
+
+    private float mKeyguardStatusBarAnimateAlpha = 1f;
+    /**
+     * If face auth with bypass is running for the first time after you turn on the screen.
+     * (From aod or screen off)
+     */
+    private boolean mFirstBypassAttempt;
+    /**
+     * If auth happens successfully during {@code mFirstBypassAttempt}, and we should wait until
+     * the keyguard is dismissed to show the status bar.
+     */
+    private boolean mDelayShowingKeyguardStatusBar;
+    private int mStatusBarState;
+    private boolean mDozing;
+    private boolean mShowingKeyguardHeadsUp;
 
     @Inject
     public KeyguardStatusBarViewController(
-            KeyguardStatusBarView view, CarrierTextController carrierTextController) {
+            KeyguardStatusBarView view,
+            CarrierTextController carrierTextController,
+            ConfigurationController configurationController,
+            SystemStatusAnimationScheduler animationScheduler,
+            BatteryController batteryController,
+            UserInfoController userInfoController,
+            StatusBarIconController statusBarIconController,
+            StatusBarIconController.TintedIconManager.Factory tintedIconManagerFactory,
+            BatteryMeterViewController batteryMeterViewController,
+            NotificationPanelViewController.NotificationPanelViewStateProvider
+                    notificationPanelViewStateProvider,
+            KeyguardStateController keyguardStateController,
+            KeyguardBypassController bypassController,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            BiometricUnlockController biometricUnlockController,
+            SysuiStatusBarStateController statusBarStateController) {
         super(view);
         mCarrierTextController = carrierTextController;
+        mConfigurationController = configurationController;
+        mAnimationScheduler = animationScheduler;
+        mBatteryController = batteryController;
+        mUserInfoController = userInfoController;
+        mStatusBarIconController = statusBarIconController;
+        mTintedIconManagerFactory = tintedIconManagerFactory;
+        mBatteryMeterViewController = batteryMeterViewController;
+        mNotificationPanelViewStateProvider = notificationPanelViewStateProvider;
+        mKeyguardStateController = keyguardStateController;
+        mKeyguardBypassController = bypassController;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mBiometricUnlockController = biometricUnlockController;
+        mStatusBarStateController = statusBarStateController;
+
+        mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
+        mKeyguardStateController.addCallback(
+                new KeyguardStateController.Callback() {
+                    @Override
+                    public void onKeyguardFadingAwayChanged() {
+                        if (!mKeyguardStateController.isKeyguardFadingAway()) {
+                            mFirstBypassAttempt = false;
+                            mDelayShowingKeyguardStatusBar = false;
+                        }
+                    }
+                }
+        );
+
+        Resources r = getResources();
+        mBlockedIcons = Collections.unmodifiableList(Arrays.asList(
+                r.getString(com.android.internal.R.string.status_bar_volume),
+                r.getString(com.android.internal.R.string.status_bar_alarm_clock),
+                r.getString(com.android.internal.R.string.status_bar_call_strength)));
+        mNotificationsHeaderCollideDistance = r.getDimensionPixelSize(
+                R.dimen.header_notifications_collide_distance);
     }
 
     @Override
     protected void onInit() {
         super.onInit();
         mCarrierTextController.init();
+        mBatteryMeterViewController.init();
     }
 
     @Override
     protected void onViewAttached() {
+        mConfigurationController.addCallback(mConfigurationListener);
+        mAnimationScheduler.addCallback(mAnimationCallback);
+        mUserInfoController.addCallback(mOnUserInfoChangedListener);
+        mStatusBarStateController.addCallback(mStatusBarStateListener);
+        mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
+        if (mTintedIconManager == null) {
+            mTintedIconManager =
+                    mTintedIconManagerFactory.create(mView.findViewById(R.id.statusIcons));
+            mTintedIconManager.setBlockList(mBlockedIcons);
+            mStatusBarIconController.addIconGroup(mTintedIconManager);
+        }
+        onThemeChanged();
     }
 
     @Override
     protected void onViewDetached() {
+        mConfigurationController.removeCallback(mConfigurationListener);
+        mAnimationScheduler.removeCallback(mAnimationCallback);
+        mUserInfoController.removeCallback(mOnUserInfoChangedListener);
+        mStatusBarStateController.removeCallback(mStatusBarStateListener);
+        mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
+        if (mTintedIconManager != null) {
+            mStatusBarIconController.removeIconGroup(mTintedIconManager);
+        }
     }
+
+    /** Should be called when the theme changes. */
+    public void onThemeChanged() {
+        mView.onThemeChanged(mTintedIconManager);
+    }
+
+    /** Sets whether user switcher is enabled. */
+    public void setKeyguardUserSwitcherEnabled(boolean enabled) {
+        mView.setKeyguardUserSwitcherEnabled(enabled);
+    }
+
+    /** Sets whether this controller should listen to battery updates. */
+    public void setBatteryListening(boolean listening) {
+        if (listening == mBatteryListening) {
+            return;
+        }
+        mBatteryListening = listening;
+        if (mBatteryListening) {
+            mBatteryController.addCallback(mBatteryStateChangeCallback);
+        } else {
+            mBatteryController.removeCallback(mBatteryStateChangeCallback);
+        }
+    }
+
+    /** Set the view to have no top clipping. */
+    public void setNoTopClipping() {
+        mView.setTopClipping(0);
+    }
+
+    /**
+     * Update the view's top clipping based on the value of notificationPanelTop and the view's
+     * current top.
+     *
+     * @param notificationPanelTop the current top of the notification panel view.
+     */
+    public void updateTopClipping(int notificationPanelTop) {
+        mView.setTopClipping(notificationPanelTop - mView.getTop());
+    }
+
+    /** Sets the dozing state. */
+    public void setDozing(boolean dozing) {
+        mDozing = dozing;
+    }
+
+    /** Animate the keyguard status bar in. */
+    public void animateKeyguardStatusBarIn() {
+        mView.setVisibility(View.VISIBLE);
+        mView.setAlpha(0f);
+        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
+        anim.addUpdateListener(mAnimatorUpdateListener);
+        anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        anim.start();
+    }
+
+    /** Animate the keyguard status bar out. */
+    public void animateKeyguardStatusBarOut(long startDelay, long duration) {
+        ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
+        anim.addUpdateListener(mAnimatorUpdateListener);
+        anim.setStartDelay(startDelay);
+        anim.setDuration(duration);
+        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mView.setVisibility(View.INVISIBLE);
+                mView.setAlpha(1f);
+                mKeyguardStatusBarAnimateAlpha = 1f;
+            }
+        });
+        anim.start();
+    }
+
+    /**
+     * Updates the {@link KeyguardStatusBarView} state based on what the
+     * {@link NotificationPanelViewController.NotificationPanelViewStateProvider} and other
+     * controllers provide.
+     */
+    public void updateViewState() {
+        if (!isKeyguardShowing()) {
+            return;
+        }
+
+        float alphaQsExpansion = 1 - Math.min(
+                1, mNotificationPanelViewStateProvider.getQsExpansionFraction() * 2);
+        float newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
+                * mKeyguardStatusBarAnimateAlpha
+                * (1.0f - mKeyguardHeadsUpShowingAmount);
+
+        boolean hideForBypass =
+                mFirstBypassAttempt && mKeyguardUpdateMonitor.shouldListenForFace()
+                        || mDelayShowingKeyguardStatusBar;
+        int newVisibility = newAlpha != 0f && !mDozing && !hideForBypass
+                ? View.VISIBLE : View.INVISIBLE;
+
+        updateViewState(newAlpha, newVisibility);
+    }
+
+    /**
+     * Updates the {@link KeyguardStatusBarView} state based on the provided values.
+     */
+    public void updateViewState(float alpha, int visibility) {
+        mView.setAlpha(alpha);
+        mView.setVisibility(visibility);
+    }
+
+    /**
+     * @return the alpha to be used to fade out the contents on Keyguard (status bar, bottom area)
+     * during swiping up.
+     */
+    private float getKeyguardContentsAlpha() {
+        float alpha;
+        if (isKeyguardShowing()) {
+            // When on Keyguard, we hide the header as soon as we expanded close enough to the
+            // header
+            alpha = mNotificationPanelViewStateProvider.getPanelViewExpandedHeight()
+                    / (mView.getHeight() + mNotificationsHeaderCollideDistance);
+        } else {
+            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
+            // soon as we start translating the stack.
+            alpha = mNotificationPanelViewStateProvider.getPanelViewExpandedHeight()
+                    / mView.getHeight();
+        }
+        alpha = MathUtils.saturate(alpha);
+        alpha = (float) Math.pow(alpha, 0.75);
+        return alpha;
+    }
+
+    /**
+     * Update {@link KeyguardStatusBarView}'s visibility based on whether keyguard is showing and
+     * whether heads up is visible.
+     */
+    public void updateForHeadsUp() {
+        updateForHeadsUp(true);
+    }
+
+    void updateForHeadsUp(boolean animate) {
+        boolean showingKeyguardHeadsUp =
+                isKeyguardShowing() && mNotificationPanelViewStateProvider.shouldHeadsUpBeVisible();
+        if (mShowingKeyguardHeadsUp != showingKeyguardHeadsUp) {
+            mShowingKeyguardHeadsUp = showingKeyguardHeadsUp;
+            if (isKeyguardShowing()) {
+                PropertyAnimator.setProperty(
+                        mView,
+                        mHeadsUpShowingAmountAnimation,
+                        showingKeyguardHeadsUp ? 1.0f : 0.0f,
+                        KEYGUARD_HUN_PROPERTIES,
+                        animate);
+            } else {
+                PropertyAnimator.applyImmediately(mView, mHeadsUpShowingAmountAnimation, 0.0f);
+            }
+        }
+    }
+
+    private boolean isKeyguardShowing() {
+        return mStatusBarState == KEYGUARD;
+    }
+
+    /** */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("KeyguardStatusBarView:");
+        pw.println("  mBatteryListening: " + mBatteryListening);
+        mView.dump(fd, pw, args);
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index 24c9021..570b0ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
@@ -33,6 +34,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -86,8 +88,12 @@
     private boolean mNavbarColorManagedByIme;
 
     @Inject
-    public LightBarController(Context ctx, DarkIconDispatcher darkIconDispatcher,
-            BatteryController batteryController, NavigationModeController navModeController) {
+    public LightBarController(
+            Context ctx,
+            DarkIconDispatcher darkIconDispatcher,
+            BatteryController batteryController,
+            NavigationModeController navModeController,
+            DumpManager dumpManager) {
         mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone));
         mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher;
         mBatteryController = batteryController;
@@ -95,6 +101,10 @@
         mNavigationMode = navModeController.addListener((mode) -> {
             mNavigationMode = mode;
         });
+
+        if (ctx.getDisplayId() == DEFAULT_DISPLAY) {
+            dumpManager.registerDumpable(getClass().getSimpleName(), this);
+        }
     }
 
     public void setNavigationBar(LightBarTransitionsController navigationBar) {
@@ -218,19 +228,19 @@
             }
         }
 
+        // If no one is light, all icons become white.
+        if (numLightStacks == 0) {
+            mStatusBarIconController.getTransitionsController().setIconsDark(
+                    false, animateChange());
+        }
+
         // If all stacks are light, all icons get dark.
-        if (numLightStacks == numStacks) {
+        else if (numLightStacks == numStacks) {
             mStatusBarIconController.setIconsDarkArea(null);
             mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange());
 
         }
 
-        // If no one is light, all icons become white.
-        else if (numLightStacks == 0) {
-            mStatusBarIconController.getTransitionsController().setIconsDark(
-                    false, animateChange());
-        }
-
         // Not the same for every stack, magic!
         else {
             mStatusBarIconController.setIconsDarkArea(
@@ -291,4 +301,33 @@
             pw.println();
         }
     }
+
+    /**
+     * Injectable factory for creating a {@link LightBarController}.
+     */
+    public static class Factory {
+        private final DarkIconDispatcher mDarkIconDispatcher;
+        private final BatteryController mBatteryController;
+        private final NavigationModeController mNavModeController;
+        private final DumpManager mDumpManager;
+
+        @Inject
+        public Factory(
+                DarkIconDispatcher darkIconDispatcher,
+                BatteryController batteryController,
+                NavigationModeController navModeController,
+                DumpManager dumpManager) {
+
+            mDarkIconDispatcher = darkIconDispatcher;
+            mBatteryController = batteryController;
+            mNavModeController = navModeController;
+            mDumpManager = dumpManager;
+        }
+
+        /** Create an {@link LightBarController} */
+        public LightBarController create(Context context) {
+            return new LightBarController(context, mDarkIconDispatcher, mBatteryController,
+                    mNavModeController, mDumpManager);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
index 7d13405..3f33281 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
@@ -21,6 +21,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.annotation.Nullable;
+import android.view.InsetsVisibilities;
 import android.view.View;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
@@ -149,7 +150,8 @@
         @Override
         public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, boolean isFullscreen) {
+                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                String packageName) {
             if (displayId != mDisplayId) {
                 return;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java
index 094ebb9..5f222af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java
@@ -24,7 +24,6 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.UiEventLoggerImpl;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.Dependency;
 import com.android.systemui.EventLogConstants;
 import com.android.systemui.EventLogTags;
 import com.android.systemui.dagger.SysUISingleton;
@@ -88,10 +87,11 @@
     }
 
     private ArrayMap<Integer, Integer> mLegacyMap;
-    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+    private final MetricsLogger mMetricsLogger;
 
     @Inject
-    public LockscreenGestureLogger() {
+    public LockscreenGestureLogger(MetricsLogger metricsLogger) {
+        mMetricsLogger = metricsLogger;
         mLegacyMap = new ArrayMap<>(EventLogConstants.METRICS_GESTURE_TYPE_MAP.length);
         for (int i = 0; i < EventLogConstants.METRICS_GESTURE_TYPE_MAP.length ; i++) {
             mLegacyMap.put(EventLogConstants.METRICS_GESTURE_TYPE_MAP[i], i);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
index 78fcd82..2a13e6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
@@ -119,7 +119,6 @@
         LoaderResult result = loadBitmap(mCurrentUserId, mSelectedUser);
         if (result.success) {
             mCached = true;
-            mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
             mCache = result.bitmap;
         }
         return mCache;
@@ -235,7 +234,6 @@
                 if (result.success) {
                     mCached = true;
                     mCache = result.bitmap;
-                    mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
                     mMediaManager.updateMediaMetaData(
                             true /* metaDataChanged */, true /* allowEnterAnimation */);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
index f27c7d2..dd21c8a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
@@ -23,22 +23,26 @@
 import android.view.ViewGroup;
 
 import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.qs.FooterActionsView;
 import com.android.systemui.qs.QSDetailDisplayer;
 import com.android.systemui.qs.dagger.QSScope;
+import com.android.systemui.qs.user.UserSwitchDialogController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.ViewController;
 
 import javax.inject.Inject;
 
 /** View Controller for {@link MultiUserSwitch}. */
-@QSScope
 public class MultiUserSwitchController extends ViewController<MultiUserSwitch> {
     private final UserManager mUserManager;
     private final UserSwitcherController mUserSwitcherController;
     private final QSDetailDisplayer mQsDetailDisplayer;
     private final FalsingManager mFalsingManager;
+    private final UserSwitchDialogController mUserSwitchDialogController;
+    private final FeatureFlags mFeatureFlags;
 
     private UserSwitcherController.BaseUserAdapter mUserListener;
 
@@ -49,26 +53,61 @@
                 return;
             }
 
-            View center = mView.getChildCount() > 0 ? mView.getChildAt(0) : mView;
+            if (mFeatureFlags.useNewUserSwitcher()) {
+                mUserSwitchDialogController.showDialog(v);
+            } else {
+                View center = mView.getChildCount() > 0 ? mView.getChildAt(0) : mView;
 
-            int[] tmpInt = new int[2];
-            center.getLocationInWindow(tmpInt);
-            tmpInt[0] += center.getWidth() / 2;
-            tmpInt[1] += center.getHeight() / 2;
+                int[] tmpInt = new int[2];
+                center.getLocationInWindow(tmpInt);
+                tmpInt[0] += center.getWidth() / 2;
+                tmpInt[1] += center.getHeight() / 2;
 
-            mQsDetailDisplayer.showDetailAdapter(getUserDetailAdapter(), tmpInt[0], tmpInt[1]);
+                mQsDetailDisplayer.showDetailAdapter(getUserDetailAdapter(), tmpInt[0], tmpInt[1]);
+            }
         }
     };
 
-    @Inject
-    public MultiUserSwitchController(MultiUserSwitch view, UserManager userManager,
+    @QSScope
+    public static class Factory {
+        private final UserManager mUserManager;
+        private final UserSwitcherController mUserSwitcherController;
+        private final QSDetailDisplayer mQsDetailDisplayer;
+        private final FalsingManager mFalsingManager;
+        private final UserSwitchDialogController mUserSwitchDialogController;
+        private final FeatureFlags mFeatureFlags;
+
+        @Inject
+        public Factory(UserManager userManager, UserSwitcherController userSwitcherController,
+                QSDetailDisplayer qsDetailDisplayer, FalsingManager falsingManager,
+                UserSwitchDialogController userSwitchDialogController,
+                FeatureFlags featureFlags) {
+            mUserManager = userManager;
+            mUserSwitcherController = userSwitcherController;
+            mQsDetailDisplayer = qsDetailDisplayer;
+            mFalsingManager = falsingManager;
+            mUserSwitchDialogController = userSwitchDialogController;
+            mFeatureFlags = featureFlags;
+        }
+
+        public MultiUserSwitchController create(FooterActionsView view) {
+            return new MultiUserSwitchController(view.findViewById(R.id.multi_user_switch),
+                    mUserManager, mUserSwitcherController, mQsDetailDisplayer,
+                    mFalsingManager, mUserSwitchDialogController, mFeatureFlags);
+        }
+    }
+
+    private MultiUserSwitchController(MultiUserSwitch view, UserManager userManager,
             UserSwitcherController userSwitcherController, QSDetailDisplayer qsDetailDisplayer,
-            FalsingManager falsingManager) {
+            FalsingManager falsingManager, UserSwitchDialogController userSwitchDialogController,
+            FeatureFlags featureFlags) {
         super(view);
         mUserManager = userManager;
         mUserSwitcherController = userSwitcherController;
         mQsDetailDisplayer = qsDetailDisplayer;
         mFalsingManager = falsingManager;
+        mUserSwitchDialogController = userSwitchDialogController;
+        mFeatureFlags = featureFlags;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 58cbe83..53def01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -21,12 +21,22 @@
 import static androidx.constraintlayout.widget.ConstraintSet.END;
 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
 import static androidx.constraintlayout.widget.ConstraintSet.START;
+import static androidx.constraintlayout.widget.ConstraintSet.TOP;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
+import static com.android.keyguard.KeyguardClockSwitch.LARGE;
+import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
+import static com.android.systemui.statusbar.phone.PanelBar.STATE_CLOSED;
+import static com.android.systemui.statusbar.phone.PanelBar.STATE_OPEN;
+import static com.android.systemui.statusbar.phone.PanelBar.STATE_OPENING;
 
 import static java.lang.Float.isNaN;
 
@@ -50,7 +60,7 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
-import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Bundle;
 import android.os.Handler;
@@ -59,6 +69,8 @@
 import android.os.UserManager;
 import android.os.VibrationEffect;
 import android.provider.Settings;
+import android.transition.ChangeBounds;
+import android.transition.TransitionManager;
 import android.util.Log;
 import android.util.MathUtils;
 import android.view.LayoutInflater;
@@ -70,6 +82,7 @@
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
@@ -85,16 +98,16 @@
 import com.android.keyguard.KeyguardStatusView;
 import com.android.keyguard.KeyguardStatusViewController;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.LockIconViewController;
 import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent;
 import com.android.keyguard.dagger.KeyguardStatusBarViewComponent;
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
 import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
 import com.android.systemui.DejankUtils;
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
@@ -107,6 +120,7 @@
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.MediaHierarchyManager;
+import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.FalsingManager.FalsingTapListener;
@@ -118,7 +132,6 @@
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.KeyguardAffordanceView;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -173,7 +186,6 @@
 import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
-import java.util.function.Function;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -203,7 +215,7 @@
      */
     private static final int FLING_HIDE = 2;
     private static final long ANIMATION_DELAY_ICON_FADE_IN =
-            ActivityLaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
+            LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
                     - CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
 
     private final DozeParameters mDozeParameters;
@@ -227,7 +239,6 @@
 
     @VisibleForTesting final StatusBarStateListener mStatusBarStateListener =
             new StatusBarStateListener();
-    private final BiometricUnlockController mBiometricUnlockController;
     private final NotificationPanelView mView;
     private final VibratorHelper mVibratorHelper;
     private final MetricsLogger mMetricsLogger;
@@ -255,53 +266,6 @@
     private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1);
     private static final Rect EMPTY_RECT = new Rect();
 
-    private final AnimatableProperty KEYGUARD_HEADS_UP_SHOWING_AMOUNT = AnimatableProperty.from(
-            "KEYGUARD_HEADS_UP_SHOWING_AMOUNT",
-            (notificationPanelView, aFloat) -> setKeyguardHeadsUpShowingAmount(aFloat),
-            (Function<NotificationPanelView, Float>) notificationPanelView ->
-                    getKeyguardHeadsUpShowingAmount(),
-            R.id.keyguard_hun_animator_tag, R.id.keyguard_hun_animator_end_tag,
-            R.id.keyguard_hun_animator_start_tag);
-    private static final AnimationProperties
-            KEYGUARD_HUN_PROPERTIES =
-            new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-    @VisibleForTesting
-    final KeyguardUpdateMonitorCallback
-            mKeyguardUpdateCallback =
-            new KeyguardUpdateMonitorCallback() {
-
-                @Override
-                public void onBiometricAuthenticated(int userId,
-                        BiometricSourceType biometricSourceType,
-                        boolean isStrongBiometric) {
-                    if (mFirstBypassAttempt
-                            && mUpdateMonitor.isUnlockingWithBiometricAllowed(isStrongBiometric)) {
-                        mDelayShowingKeyguardStatusBar = true;
-                    }
-                }
-
-                @Override
-                public void onBiometricRunningStateChanged(boolean running,
-                        BiometricSourceType biometricSourceType) {
-                    boolean
-                            keyguardOrShadeLocked =
-                            mBarState == KEYGUARD
-                                    || mBarState == StatusBarState.SHADE_LOCKED;
-                    if (!running && mFirstBypassAttempt && keyguardOrShadeLocked && !mDozing
-                            && !mDelayShowingKeyguardStatusBar
-                            && !mBiometricUnlockController.isBiometricUnlock()) {
-                        mFirstBypassAttempt = false;
-                        animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-                    }
-                }
-
-                @Override
-                public void onFinishedGoingToSleep(int why) {
-                    mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
-                    mDelayShowingKeyguardStatusBar = false;
-                }
-    };
-
     private final LayoutInflater mLayoutInflater;
     private final PowerManager mPowerManager;
     private final AccessibilityManager mAccessibilityManager;
@@ -319,7 +283,6 @@
     private final KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory;
     private final QSDetailDisplayer mQSDetailDisplayer;
     private final FragmentService mFragmentService;
-    private final FeatureFlags mFeatureFlags;
     private final ScrimController mScrimController;
     private final PrivacyDotViewController mPrivacyDotViewController;
     private final QuickAccessWalletController mQuickAccessWalletController;
@@ -331,8 +294,11 @@
     private final int mMaxKeyguardNotifications;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     private final TapAgainViewController mTapAgainViewController;
+    private final SplitShadeHeaderController mSplitShadeHeaderController;
     private final RecordingController mRecordingController;
     private boolean mShouldUseSplitNotificationShade;
+    // The bottom padding reserved for elements of the keyguard measuring notifications
+    private float mKeyguardNotificationBottomPadding;
     // Current max allowed keyguard notifications determined by measuring the panel
     private int mMaxAllowedKeyguardNotifications;
 
@@ -341,13 +307,13 @@
     private KeyguardQsUserSwitchController mKeyguardQsUserSwitchController;
     private KeyguardUserSwitcherController mKeyguardUserSwitcherController;
     private KeyguardStatusBarView mKeyguardStatusBar;
-    private KeyguardStatusBarViewController mKeyguarStatusBarViewController;
-    private ViewGroup mBigClockContainer;
+    private KeyguardStatusBarViewController mKeyguardStatusBarViewController;
     @VisibleForTesting QS mQs;
     private FrameLayout mQsFrame;
     private KeyguardStatusViewController mKeyguardStatusViewController;
     private LockIconViewController mLockIconViewController;
     private NotificationsQuickSettingsContainer mNotificationContainerParent;
+    private NotificationsQSContainerController mNotificationsQSContainerController;
     private boolean mAnimateNextPositionUpdate;
     private float mQuickQsOffsetHeight;
     private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
@@ -371,6 +337,7 @@
     private boolean mKeyguardUserSwitcherEnabled;
     private boolean mDozing;
     private boolean mDozingOnDown;
+    private boolean mBouncerShowing;
     private int mBarState;
     private float mInitialHeightOnTouch;
     private float mInitialTouchX;
@@ -388,13 +355,12 @@
     private FlingAnimationUtils mFlingAnimationUtils;
     private int mStatusBarMinHeight;
     private int mStatusBarHeaderHeightKeyguard;
-    private int mNotificationsHeaderCollideDistance;
     private float mOverStretchAmount;
     private float mDownX;
     private float mDownY;
     private int mDisplayTopInset = 0; // in pixels
     private int mDisplayRightInset = 0; // in pixels
-    private int mSplitShadeNotificationsTopPadding;
+    private int mSplitShadeStatusBarHeight;
 
     private final KeyguardClockPositionAlgorithm
             mClockPositionAlgorithm =
@@ -425,7 +391,6 @@
     private boolean mQsTouchAboveFalsingThreshold;
     private int mQsFalsingThreshold;
 
-    private float mKeyguardStatusBarAnimateAlpha = 1f;
     private HeadsUpTouchHelper mHeadsUpTouchHelper;
     private boolean mListenForHeadsUp;
     private int mNavigationBarBottomHeight;
@@ -443,7 +408,7 @@
 
     private Runnable mHeadsUpExistenceChangedRunnable = () -> {
         setHeadsUpAnimatingAway(false);
-        notifyBarPanelExpansionChanged();
+        updatePanelExpansionAndVisibility();
     };
     // TODO (b/162832756): once migrated to the new pipeline, delete legacy group manager
     private NotificationGroupManagerLegacy mGroupManager;
@@ -471,7 +436,6 @@
     private float mLinearDarkAmount;
 
     private boolean mPulsing;
-    private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
     private boolean mUserSetupComplete;
     private int mQsNotificationTopPadding;
     private boolean mHideIconsDuringLaunchAnimation = true;
@@ -479,7 +443,6 @@
     private ArrayList<Consumer<ExpandableNotificationRow>>
             mTrackingHeadsUpListeners =
             new ArrayList<>();
-    private Runnable mVerticalTranslationListener;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
 
     private int mPanelAlpha;
@@ -507,6 +470,8 @@
     private final NotificationLockscreenUserManager mLockscreenUserManager;
     private final UserManager mUserManager;
     private final MediaDataManager mMediaDataManager;
+    private final SysUiState mSysUiState;
+
     private NotificationShadeDepthController mDepthController;
     private int mDisplayId;
 
@@ -522,8 +487,6 @@
     private int mDarkIconSize;
     private int mHeadsUpInset;
     private boolean mHeadsUpPinnedMode;
-    private float mKeyguardHeadsUpShowingAmount = 0.0f;
-    private boolean mShowingKeyguardHeadsUp;
     private boolean mAllowExpandForSmallExpansion;
     private Runnable mExpandAfterLayoutRunnable;
 
@@ -582,17 +545,6 @@
      */
     private boolean mIsPanelCollapseOnQQS;
 
-    /**
-     * If face auth with bypass is running for the first time after you turn on the screen.
-     * (From aod or screen off)
-     */
-    private boolean mFirstBypassAttempt;
-    /**
-     * If auth happens successfully during {@code mFirstBypassAttempt}, and we should wait until
-     * the keyguard is dismissed to show the status bar.
-     */
-    private boolean mDelayShowingKeyguardStatusBar;
-
     private boolean mAnimatingQS;
 
     /**
@@ -645,6 +597,8 @@
 
     private KeyguardMediaController mKeyguardMediaController;
 
+    private boolean mStatusViewCentered = true;
+
     private View.AccessibilityDelegate mAccessibilityDelegate = new View.AccessibilityDelegate() {
         @Override
         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
@@ -701,8 +655,8 @@
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
             ConversationNotificationManager conversationNotificationManager,
             MediaHierarchyManager mediaHierarchyManager,
-            BiometricUnlockController biometricUnlockController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            NotificationsQSContainerController notificationsQSContainerController,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory,
             KeyguardQsUserSwitchComponent.Factory keyguardQsUserSwitchComponentFactory,
@@ -719,7 +673,6 @@
             NotificationShadeDepthController notificationShadeDepthController,
             AmbientState ambientState,
             LockIconViewController lockIconViewController,
-            FeatureFlags featureFlags,
             KeyguardMediaController keyguardMediaController,
             PrivacyDotViewController privacyDotViewController,
             TapAgainViewController tapAgainViewController,
@@ -730,13 +683,23 @@
             RecordingController recordingController,
             @Main Executor uiExecutor,
             SecureSettings secureSettings,
+            SplitShadeHeaderController splitShadeHeaderController,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
+            LockscreenGestureLogger lockscreenGestureLogger,
             NotificationRemoteInputManager remoteInputManager,
             ControlsComponent controlsComponent) {
-        super(view, falsingManager, dozeLog, keyguardStateController,
-                (SysuiStatusBarStateController) statusBarStateController, vibratorHelper,
-                statusBarKeyguardViewManager, latencyTracker, flingAnimationUtilsBuilder.get(),
-                statusBarTouchableRegionManager, ambientState);
+        super(view,
+                falsingManager,
+                dozeLog,
+                keyguardStateController,
+                (SysuiStatusBarStateController) statusBarStateController,
+                vibratorHelper,
+                statusBarKeyguardViewManager,
+                latencyTracker,
+                flingAnimationUtilsBuilder.get(),
+                statusBarTouchableRegionManager,
+                lockscreenGestureLogger,
+                ambientState);
         mView = view;
         mVibratorHelper = vibratorHelper;
         mKeyguardMediaController = keyguardMediaController;
@@ -749,13 +712,14 @@
         mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
         mMediaHierarchyManager = mediaHierarchyManager;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mNotificationsQSContainerController = notificationsQSContainerController;
+        mNotificationsQSContainerController.init();
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mGroupManager = groupManager;
         mNotificationIconAreaController = notificationIconAreaController;
         mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
         mKeyguardStatusBarViewComponentFactory = keyguardStatusBarViewComponentFactory;
         mDepthController = notificationShadeDepthController;
-        mFeatureFlags = featureFlags;
         mContentResolver = contentResolver;
         mKeyguardQsUserSwitchComponentFactory = keyguardQsUserSwitchComponentFactory;
         mKeyguardUserSwitcherComponentFactory = keyguardUserSwitcherComponentFactory;
@@ -763,8 +727,9 @@
         mFragmentService = fragmentService;
         mSettingsChangeObserver = new SettingsChangeObserver(handler);
         mShouldUseSplitNotificationShade =
-                Utils.shouldUseSplitNotificationShade(mFeatureFlags, mResources);
+                Utils.shouldUseSplitNotificationShade(mResources);
         mView.setWillNotDraw(!DEBUG);
+        mSplitShadeHeaderController = splitShadeHeaderController;
         mLayoutInflater = layoutInflater;
         mFalsingManager = falsingManager;
         mFalsingCollector = falsingCollector;
@@ -778,7 +743,6 @@
         mDisplayId = displayId;
         mPulseExpansionHandler = pulseExpansionHandler;
         mDozeParameters = dozeParameters;
-        mBiometricUnlockController = biometricUnlockController;
         mScrimController = scrimController;
         mScrimController.setClipsQsScrim(!mShouldUseSplitNotificationShade);
         mUserManager = userManager;
@@ -786,6 +750,8 @@
         mTapAgainViewController = tapAgainViewController;
         mUiExecutor = uiExecutor;
         mSecureSettings = secureSettings;
+        // TODO: inject via dagger instead of Dependency
+        mSysUiState = Dependency.get(SysUiState.class);
         pulseExpansionHandler.setPulseExpandAbortListener(() -> {
             if (mQs != null) {
                 mQs.animateHeaderSlidingOut();
@@ -794,21 +760,8 @@
         mThemeResId = mView.getContext().getThemeResId();
         mKeyguardBypassController = bypassController;
         mUpdateMonitor = keyguardUpdateMonitor;
-        mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
-        KeyguardStateController.Callback
-                keyguardMonitorCallback =
-                new KeyguardStateController.Callback() {
-                    @Override
-                    public void onKeyguardFadingAwayChanged() {
-                        if (!mKeyguardStateController.isKeyguardFadingAway()) {
-                            mFirstBypassAttempt = false;
-                            mDelayShowingKeyguardStatusBar = false;
-                        }
-                    }
-                };
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         lockscreenShadeTransitionController.setNotificationPanelController(this);
-        mKeyguardStateController.addCallback(keyguardMonitorCallback);
         DynamicPrivacyControlListener
                 dynamicPrivacyControlListener =
                 new DynamicPrivacyControlListener();
@@ -854,7 +807,6 @@
     private void onFinishInflate() {
         loadDimens();
         mKeyguardStatusBar = mView.findViewById(R.id.keyguard_header);
-        mBigClockContainer = mView.findViewById(R.id.big_clock_container);
 
         FrameLayout userAvatarContainer = null;
         KeyguardUserSwitcherView keyguardUserSwitcherView = null;
@@ -869,10 +821,16 @@
             }
         }
 
+        mKeyguardStatusBarViewController =
+                mKeyguardStatusBarViewComponentFactory.build(
+                        mKeyguardStatusBar,
+                        mNotificationPanelViewStateProvider)
+                        .getKeyguardStatusBarViewController();
+        mKeyguardStatusBarViewController.init();
+
         updateViewControllers(
                 mView.findViewById(R.id.keyguard_status_view),
                 userAvatarContainer,
-                mKeyguardStatusBar,
                 keyguardUserSwitcherView);
         mNotificationContainerParent = mView.findViewById(R.id.notification_container_parent);
         NotificationStackScrollLayout stackScrollLayout = mView.findViewById(
@@ -900,7 +858,7 @@
         mWakeUpCoordinator.addListener(new NotificationWakeUpCoordinator.WakeUpListener() {
             @Override
             public void onFullyHiddenChanged(boolean isFullyHidden) {
-                updateKeyguardStatusBarForHeadsUp();
+                mKeyguardStatusBarViewController.updateForHeadsUp();
             }
 
             @Override
@@ -937,8 +895,6 @@
         mStatusBarHeaderHeightKeyguard = mResources.getDimensionPixelSize(
                 R.dimen.status_bar_header_height_keyguard);
         mQsPeekHeight = mResources.getDimensionPixelSize(R.dimen.qs_peek_height);
-        mNotificationsHeaderCollideDistance = mResources.getDimensionPixelSize(
-                R.dimen.header_notifications_collide_distance);
         mClockPositionAlgorithm.loadDimens(mResources);
         mQsFalsingThreshold = mResources.getDimensionPixelSize(R.dimen.qs_falsing_threshold);
         mPositionMinSideMargin = mResources.getDimensionPixelSize(
@@ -957,7 +913,8 @@
                 R.dimen.pulse_expansion_max_top_overshoot);
         mScrimCornerRadius = mResources.getDimensionPixelSize(
                 R.dimen.notification_scrim_corner_radius);
-        mScreenCornerRadius = (int) ScreenDecorationsUtils.getWindowCornerRadius(mResources);
+        mScreenCornerRadius = (int) ScreenDecorationsUtils.getWindowCornerRadius(
+                mView.getContext());
         mLockscreenNotificationQSPadding = mResources.getDimensionPixelSize(
                 R.dimen.notification_side_paddings);
         mUdfpsMaxYBurnInOffset = mResources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
@@ -965,7 +922,6 @@
 
     private void updateViewControllers(KeyguardStatusView keyguardStatusView,
             FrameLayout userAvatarView,
-            KeyguardStatusBarView keyguardStatusBarView,
             KeyguardUserSwitcherView keyguardUserSwitcherView) {
         // Re-associate the KeyguardStatusViewController
         KeyguardStatusViewComponent statusViewComponent =
@@ -973,12 +929,6 @@
         mKeyguardStatusViewController = statusViewComponent.getKeyguardStatusViewController();
         mKeyguardStatusViewController.init();
 
-        KeyguardStatusBarViewComponent statusBarViewComponent =
-                mKeyguardStatusBarViewComponentFactory.build(keyguardStatusBarView);
-        mKeyguarStatusBarViewController =
-                statusBarViewComponent.getKeyguardStatusBarViewController();
-        mKeyguarStatusBarViewController.init();
-
         if (mKeyguardUserSwitcherController != null) {
             // Try to close the switcher so that callbacks are triggered if necessary.
             // Otherwise, NPV can get into a state where some of the views are still hidden
@@ -996,16 +946,16 @@
                     userSwitcherComponent.getKeyguardQsUserSwitchController();
             mKeyguardQsUserSwitchController.setNotificationPanelViewController(this);
             mKeyguardQsUserSwitchController.init();
-            mKeyguardStatusBar.setKeyguardUserSwitcherEnabled(true);
+            mKeyguardStatusBarViewController.setKeyguardUserSwitcherEnabled(true);
         } else if (keyguardUserSwitcherView != null) {
             KeyguardUserSwitcherComponent userSwitcherComponent =
                     mKeyguardUserSwitcherComponentFactory.build(keyguardUserSwitcherView);
             mKeyguardUserSwitcherController =
                     userSwitcherComponent.getKeyguardUserSwitcherController();
             mKeyguardUserSwitcherController.init();
-            mKeyguardStatusBar.setKeyguardUserSwitcherEnabled(true);
+            mKeyguardStatusBarViewController.setKeyguardUserSwitcherEnabled(true);
         } else {
-            mKeyguardStatusBar.setKeyguardUserSwitcherEnabled(false);
+            mKeyguardStatusBarViewController.setKeyguardUserSwitcherEnabled(false);
         }
     }
 
@@ -1025,16 +975,21 @@
     public void updateResources() {
         mQuickQsOffsetHeight = mResources.getDimensionPixelSize(
                 com.android.internal.R.dimen.quick_qs_offset_height);
-        mSplitShadeNotificationsTopPadding =
-                mResources.getDimensionPixelSize(R.dimen.notifications_top_padding_split_shade);
+        mSplitShadeStatusBarHeight =
+                mResources.getDimensionPixelSize(R.dimen.split_shade_header_height);
         int qsWidth = mResources.getDimensionPixelSize(R.dimen.qs_panel_width);
         int panelWidth = mResources.getDimensionPixelSize(R.dimen.notification_panel_width);
         mShouldUseSplitNotificationShade =
-                Utils.shouldUseSplitNotificationShade(mFeatureFlags, mResources);
+                Utils.shouldUseSplitNotificationShade(mResources);
         mScrimController.setClipsQsScrim(!mShouldUseSplitNotificationShade);
         if (mQs != null) {
-            mQs.setTranslateWhileExpanding(mShouldUseSplitNotificationShade);
+            mQs.setInSplitShade(mShouldUseSplitNotificationShade);
         }
+
+        int topMargin = mShouldUseSplitNotificationShade ? mSplitShadeStatusBarHeight :
+                mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_top);
+        mSplitShadeHeaderController.setSplitShadeMode(mShouldUseSplitNotificationShade);
+
         // To change the constraints at runtime, all children of the ConstraintLayout must have ids
         ensureAllViewsHaveIds(mNotificationContainerParent);
         ConstraintSet constraintSet = new ConstraintSet();
@@ -1047,15 +1002,19 @@
             constraintSet.connect(
                     R.id.notification_stack_scroller, START,
                     R.id.qs_edge_guideline, START);
-            constraintSet.connect(R.id.keyguard_status_view, END, R.id.qs_edge_guideline, END);
         } else {
             constraintSet.connect(R.id.qs_frame, END, PARENT_ID, END);
             constraintSet.connect(R.id.notification_stack_scroller, START, PARENT_ID, START);
-            constraintSet.connect(R.id.keyguard_status_view, END, PARENT_ID, END);
         }
         constraintSet.getConstraint(R.id.notification_stack_scroller).layout.mWidth = panelWidth;
         constraintSet.getConstraint(R.id.qs_frame).layout.mWidth = qsWidth;
+        constraintSet.setMargin(R.id.notification_stack_scroller, TOP, topMargin);
+        constraintSet.setMargin(R.id.qs_frame, TOP, topMargin);
         constraintSet.applyTo(mNotificationContainerParent);
+        mAmbientState.setStackTopMargin(topMargin);
+        mNotificationsQSContainerController.setSplitShadeEnabled(mShouldUseSplitNotificationShade);
+
+        updateKeyguardStatusViewAlignment(/* animate= */false);
 
         mKeyguardMediaController.refreshMediaPosition();
     }
@@ -1102,6 +1061,9 @@
         keyguardStatusView = (KeyguardStatusView) mLayoutInflater.inflate(
                 R.layout.keyguard_status_view, mNotificationContainerParent, false);
         mNotificationContainerParent.addView(keyguardStatusView, statusIndex);
+        // When it's reinflated, this is centered by default. If it shouldn't be, this will update
+        // below when resources are updated.
+        mStatusViewCentered = true;
         attachSplitShadeMediaPlayerContainer(
                 keyguardStatusView.findViewById(R.id.status_view_media_container));
 
@@ -1128,9 +1090,8 @@
                         R.layout.keyguard_user_switcher /* layoutId */,
                         showKeyguardUserSwitcher /* enabled */);
 
-        mBigClockContainer.removeAllViews();
         updateViewControllers(mView.findViewById(R.id.keyguard_status_view), userAvatarView,
-                mKeyguardStatusBar, keyguardUserSwitcherView);
+                keyguardUserSwitcherView);
 
         // Update keyguard bottom area
         int index = mView.indexOfChild(mKeyguardBottomArea);
@@ -1146,10 +1107,6 @@
         mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
                 mStatusBarStateController.getInterpolatedDozeAmount());
 
-        if (mKeyguardStatusBar != null) {
-            mKeyguardStatusBar.onThemeChanged();
-        }
-
         mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
                 mBarState,
                 false,
@@ -1195,9 +1152,12 @@
         if (mKeyguardShowing && !mKeyguardBypassController.getBypassEnabled()) {
             mNotificationStackScrollLayoutController.setMaxDisplayedNotifications(
                     mMaxAllowedKeyguardNotifications);
+            mNotificationStackScrollLayoutController.setKeyguardBottomPadding(
+                    mKeyguardNotificationBottomPadding);
         } else {
             // no max when not on the keyguard
             mNotificationStackScrollLayoutController.setMaxDisplayedNotifications(-1);
+            mNotificationStackScrollLayoutController.setKeyguardBottomPadding(-1f);
         }
     }
 
@@ -1275,11 +1235,19 @@
             updateClockAppearance();
         }
         if (!onKeyguard) {
-            stackScrollerPadding = getUnlockedStackScrollerPadding();
+            if (mShouldUseSplitNotificationShade) {
+                // Quick settings are not on the top of the notifications
+                // when in split shade mode (they are on the left side),
+                // so we should not add a padding for them
+                stackScrollerPadding = 0;
+            } else {
+                stackScrollerPadding = getUnlockedStackScrollerPadding();
+            }
         } else {
             stackScrollerPadding = mClockPositionResult.stackScrollerPaddingExpanded;
         }
 
+        mSplitShadeHeaderController.setShadeExpandedFraction(getExpandedFraction());
         mNotificationStackScrollLayoutController.setIntrinsicPadding(stackScrollerPadding);
         mKeyguardBottomArea.setAntiBurnInOffsetX(mClockPositionResult.clockX);
 
@@ -1290,13 +1258,19 @@
     }
 
     private void updateClockAppearance() {
-        int totalHeight = mView.getHeight();
-        int bottomPadding = Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding);
         int userSwitcherPreferredY = mStatusBarHeaderHeightKeyguard;
         boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled();
         final boolean hasVisibleNotifications = mNotificationStackScrollLayoutController
                 .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia();
-        mKeyguardStatusViewController.setHasVisibleNotifications(hasVisibleNotifications);
+        boolean splitShadeWithActiveMedia =
+                mShouldUseSplitNotificationShade && mMediaDataManager.hasActiveMedia();
+        if ((hasVisibleNotifications && !mShouldUseSplitNotificationShade)
+                || (splitShadeWithActiveMedia && !mDozing)) {
+            mKeyguardStatusViewController.displayClock(SMALL);
+        } else {
+            mKeyguardStatusViewController.displayClock(LARGE);
+        }
+        updateKeyguardStatusViewAlignment(true /* animate */);
         int userIconHeight = mKeyguardQsUserSwitchController != null
                 ? mKeyguardQsUserSwitchController.getUserIconHeight() : 0;
         float expandedFraction =
@@ -1309,20 +1283,18 @@
         float udfpsAodTopLocation = -1f;
         if (mUpdateMonitor.isUdfpsEnrolled() && mAuthController.getUdfpsProps().size() > 0) {
             FingerprintSensorPropertiesInternal props = mAuthController.getUdfpsProps().get(0);
-            udfpsAodTopLocation = props.sensorLocationY - props.sensorRadius
+            final SensorLocationInternal location = props.getLocation();
+            udfpsAodTopLocation = location.sensorLocationY - location.sensorRadius
                     - mUdfpsMaxYBurnInOffset;
         }
 
         mClockPositionAlgorithm.setup(
                 mStatusBarHeaderHeightKeyguard,
-                totalHeight - bottomPadding,
-                mNotificationStackScrollLayoutController.getIntrinsicContentHeight(),
                 expandedFraction,
-                totalHeight,
                 mKeyguardStatusViewController.getLockscreenHeight(),
                 userIconHeight,
-                userSwitcherPreferredY, hasCustomClock(),
-                hasVisibleNotifications, darkamount, mOverStretchAmount,
+                userSwitcherPreferredY,
+                darkamount, mOverStretchAmount,
                 bypassEnabled, getUnlockedStackScrollerPadding(),
                 computeQsExpansionFraction(),
                 mDisplayTopInset,
@@ -1352,6 +1324,27 @@
         updateClock();
     }
 
+    private void updateKeyguardStatusViewAlignment(boolean animate) {
+        boolean hasVisibleNotifications = mNotificationStackScrollLayoutController
+                .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia();
+        boolean shouldBeCentered =
+                !mShouldUseSplitNotificationShade || !hasVisibleNotifications || mDozing;
+        if (mStatusViewCentered != shouldBeCentered) {
+            mStatusViewCentered = shouldBeCentered;
+            ConstraintSet constraintSet = new ConstraintSet();
+            constraintSet.clone(mNotificationContainerParent);
+            int statusConstraint = shouldBeCentered ? PARENT_ID : R.id.qs_edge_guideline;
+            constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END);
+            if (animate) {
+                ChangeBounds transition = new ChangeBounds();
+                transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+                transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+                TransitionManager.beginDelayedTransition(mNotificationContainerParent, transition);
+            }
+            constraintSet.applyTo(mNotificationContainerParent);
+        }
+    }
+
     /**
      * @return the padding of the stackscroller when unlocked
      */
@@ -1379,6 +1372,7 @@
 
         float bottomPadding = Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding);
         bottomPadding = Math.max(lockIconPadding, bottomPadding);
+        mKeyguardNotificationBottomPadding = bottomPadding;
 
         float availableSpace =
                 mNotificationStackScrollLayoutController.getHeight()
@@ -1532,6 +1526,24 @@
         mNotificationStackScrollLayoutController.resetScrollPosition();
     }
 
+    /** Collapses the panel. */
+    public void collapsePanel(boolean animate, boolean delayed, float speedUpFactor) {
+        boolean waiting = false;
+        if (animate && !isFullyCollapsed()) {
+            collapse(delayed, speedUpFactor);
+            waiting = true;
+        } else {
+            resetViews(false /* animate */);
+            setExpandedFraction(0); // just in case
+        }
+        if (!waiting) {
+            // it's possible that nothing animated, so we replicate the termination
+            // conditions of panelExpansionChanged here
+            // TODO(b/200063118): This can likely go away in a future refactor CL.
+            mBar.updateState(STATE_CLOSED);
+        }
+    }
+
     @Override
     public void collapse(boolean delayed, float speedUpFactor) {
         if (!canPanelBeCollapsed()) {
@@ -1575,7 +1587,7 @@
 
     private boolean isQsExpansionEnabled() {
         return mQsExpansionEnabledPolicy && mQsExpansionEnabledAmbient
-                && !mRemoteInputManager.getController().isRemoteInputActive();
+                && !mRemoteInputManager.isRemoteInputActive();
     }
 
     public void expandWithQs() {
@@ -1812,15 +1824,6 @@
         return !mQsTouchAboveFalsingThreshold;
     }
 
-    /**
-     * Percentage of panel expansion offset, caused by pulling down on a heads-up.
-     */
-    @Override
-    public void setMinFraction(float minFraction) {
-        mMinFraction = minFraction;
-        mDepthController.setPanelPullDownMinFraction(mMinFraction);
-    }
-
     private float computeQsExpansionFraction() {
         if (mQSAnimatingHiddenFromCollapsed) {
             // When hiding QS from collapsed state, the expansion can sometimes temporarily
@@ -1853,6 +1856,9 @@
 
 
     private boolean handleQsTouch(MotionEvent event) {
+        if (mShouldUseSplitNotificationShade && touchXOutsideOfQs(event.getX())) {
+            return false;
+        }
         final int action = event.getActionMasked();
         if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
                 && mBarState != KEYGUARD && !mQsExpanded && isQsExpansionEnabled()) {
@@ -1894,8 +1900,12 @@
         return false;
     }
 
+    private boolean touchXOutsideOfQs(float touchX) {
+        return touchX < mQsFrame.getX() || touchX > mQsFrame.getX() + mQsFrame.getWidth();
+    }
+
     private boolean isInQsArea(float x, float y) {
-        if (x < mQsFrame.getX() || x > mQsFrame.getX() + mQsFrame.getWidth()) {
+        if (touchXOutsideOfQs(x)) {
             return false;
         }
         // Let's reject anything at the very bottom around the home handle in gesture nav
@@ -2113,7 +2123,7 @@
             requestPanelHeightUpdate();
             mFalsingCollector.setQsExpanded(expanded);
             mStatusBar.setQsExpanded(expanded);
-            mNotificationContainerParent.setQsExpanded(expanded);
+            mNotificationsQSContainerController.setQsExpanded(expanded);
             mPulseExpansionHandler.setQsExpanded(expanded);
             mKeyguardBypassController.setQSExpanded(expanded);
             mStatusBarKeyguardViewManager.setQsExpanded(expanded);
@@ -2132,59 +2142,6 @@
         }
     }
 
-    private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mKeyguardStatusBar.setVisibility(View.INVISIBLE);
-            mKeyguardStatusBar.setAlpha(1f);
-            mKeyguardStatusBarAnimateAlpha = 1f;
-        }
-    };
-
-    private void animateKeyguardStatusBarOut() {
-        ValueAnimator anim = ValueAnimator.ofFloat(mKeyguardStatusBar.getAlpha(), 0f);
-        anim.addUpdateListener(mStatusBarAnimateAlphaListener);
-        anim.setStartDelay(mKeyguardStateController.isKeyguardFadingAway()
-                ? mKeyguardStateController.getKeyguardFadingAwayDelay() : 0);
-
-        long duration;
-        if (mKeyguardStateController.isKeyguardFadingAway()) {
-            duration = mKeyguardStateController.getShortenedFadingAwayDuration();
-        } else {
-            duration = StackStateAnimator.ANIMATION_DURATION_STANDARD;
-        }
-        anim.setDuration(duration);
-
-        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-        anim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimateKeyguardStatusBarInvisibleEndRunnable.run();
-            }
-        });
-        anim.start();
-    }
-
-    private final ValueAnimator.AnimatorUpdateListener
-            mStatusBarAnimateAlphaListener =
-            new ValueAnimator.AnimatorUpdateListener() {
-                @Override
-                public void onAnimationUpdate(ValueAnimator animation) {
-                    mKeyguardStatusBarAnimateAlpha = (float) animation.getAnimatedValue();
-                    updateHeaderKeyguardAlpha();
-                }
-            };
-
-    private void animateKeyguardStatusBarIn(long duration) {
-        mKeyguardStatusBar.setVisibility(View.VISIBLE);
-        mKeyguardStatusBar.setAlpha(0f);
-        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
-        anim.addUpdateListener(mStatusBarAnimateAlphaListener);
-        anim.setDuration(duration);
-        anim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-        anim.start();
-    }
-
     private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() {
         @Override
         public void run() {
@@ -2237,11 +2194,10 @@
         mQsExpansionHeight = height;
         updateQsExpansion();
         requestScrollerTopPaddingUpdate(false /* animate */);
-        updateHeaderKeyguardAlpha();
+        mKeyguardStatusBarViewController.updateViewState();
         if (mBarState == StatusBarState.SHADE_LOCKED || mBarState == KEYGUARD) {
             updateKeyguardBottomAreaAlpha();
             positionClockAndNotifications();
-            updateBigClockAlpha();
         }
 
         if (mAccessibilityManager.isEnabled()) {
@@ -2265,7 +2221,7 @@
     private void updateQsExpansion() {
         if (mQs == null) return;
         float qsExpansionFraction = computeQsExpansionFraction();
-        mQs.setQsExpansion(qsExpansionFraction, getHeaderTranslation());
+        mQs.setQsExpansion(qsExpansionFraction, getExpandedFraction(), getHeaderTranslation());
         mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
         int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction);
         mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY);
@@ -2289,15 +2245,10 @@
         updateQSExpansionEnabledAmbient();
     }
 
-    @Override
-    public void setIsShadeOpening(boolean opening) {
-        mAmbientState.setIsShadeOpening(opening);
-        updateQSExpansionEnabledAmbient();
-    }
-
     private void updateQSExpansionEnabledAmbient() {
         final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsOffsetHeight;
-        mQsExpansionEnabledAmbient = mAmbientState.getScrollY() <= scrollRangeToTop;
+        mQsExpansionEnabledAmbient = mShouldUseSplitNotificationShade
+                || (mAmbientState.getScrollY() <= scrollRangeToTop);
         setQsExpansionEnabled();
     }
 
@@ -2346,8 +2297,8 @@
             left = 0;
             right = getView().getRight() + mDisplayRightInset;
         } else {
-            top = Math.min(qsPanelBottomY, mSplitShadeNotificationsTopPadding);
-            bottom = mNotificationStackScrollLayoutController.getHeight();
+            top = Math.min(qsPanelBottomY, mSplitShadeStatusBarHeight);
+            bottom = top + mNotificationStackScrollLayoutController.getHeight();
             left = mNotificationStackScrollLayoutController.getLeft();
             right = mNotificationStackScrollLayoutController.getRight();
         }
@@ -2409,7 +2360,6 @@
             boolean qsVisible) {
         // Fancy clipping for quick settings
         int radius = mScrimCornerRadius;
-        int statusBarClipTop = 0;
         boolean clipStatusView = false;
         if (!mShouldUseSplitNotificationShade) {
             // The padding on this area is large enough that we can use a cheaper clipping strategy
@@ -2418,7 +2368,6 @@
             float screenCornerRadius = mRecordingController.isRecording() ? 0 : mScreenCornerRadius;
             radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius,
                     Math.min(top / (float) mScrimCornerRadius, 1f));
-            statusBarClipTop = top - mKeyguardStatusBar.getTop();
         }
         if (mQs != null) {
             float qsTranslation = 0;
@@ -2456,12 +2405,17 @@
             mScrimController.setNotificationsBounds(left, top, right, bottom);
         }
 
+        if (mShouldUseSplitNotificationShade) {
+            mKeyguardStatusBarViewController.setNoTopClipping();
+        } else {
+            mKeyguardStatusBarViewController.updateTopClipping(top);
+        }
+
         mScrimController.setScrimCornerRadius(radius);
-        mKeyguardStatusBar.setTopClipping(statusBarClipTop);
         int nsslLeft = left - mNotificationStackScrollLayoutController.getLeft();
         int nsslRight = right - mNotificationStackScrollLayoutController.getLeft();
         int nsslTop = top - mNotificationStackScrollLayoutController.getTop();
-        int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
+        int nsslBottom = bottom;
         int bottomRadius = mShouldUseSplitNotificationShade ? radius : 0;
         mNotificationStackScrollLayoutController.setRoundedClippingBounds(
                 nsslLeft, nsslTop, nsslRight, nsslBottom, radius, bottomRadius);
@@ -2502,7 +2456,7 @@
 
     private float calculateNotificationsTopPadding() {
         if (mShouldUseSplitNotificationShade && !mKeyguardShowing) {
-            return mSplitShadeNotificationsTopPadding;
+            return 0;
         }
         if (mKeyguardShowing && (mQsExpandImmediate
                 || mIsExpanding && mQsExpandedWhenExpandingStarted)) {
@@ -2761,8 +2715,9 @@
      * @return Whether we should intercept a gesture to open Quick Settings.
      */
     private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
-        if (!isQsExpansionEnabled() || mCollapsedOnDown || (mKeyguardShowing
-                && mKeyguardBypassController.getBypassEnabled())) {
+        if (!isQsExpansionEnabled() || mCollapsedOnDown
+                || (mKeyguardShowing && mKeyguardBypassController.getBypassEnabled())
+                || (mKeyguardShowing && mShouldUseSplitNotificationShade)) {
             return false;
         }
         View header = mKeyguardShowing || mQs == null ? mKeyguardStatusBar : mQs.getHeader();
@@ -2963,7 +2918,7 @@
      */
     private void updateHeader() {
         if (mBarState == KEYGUARD) {
-            updateHeaderKeyguardAlpha();
+            mKeyguardStatusBarViewController.updateViewState();
         }
         updateQsExpansion();
     }
@@ -2987,47 +2942,6 @@
         return Math.min(0, translation);
     }
 
-    /**
-     * @return the alpha to be used to fade out the contents on Keyguard (status bar, bottom area)
-     * during swiping up
-     */
-    private float getKeyguardContentsAlpha() {
-        float alpha;
-        if (mBarState == KEYGUARD) {
-
-            // When on Keyguard, we hide the header as soon as we expanded close enough to the
-            // header
-            alpha =
-                    getExpandedHeight() / (mKeyguardStatusBar.getHeight()
-                            + mNotificationsHeaderCollideDistance);
-        } else {
-
-            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
-            // soon as we start translating the stack.
-            alpha = getExpandedHeight() / mKeyguardStatusBar.getHeight();
-        }
-        alpha = MathUtils.saturate(alpha);
-        alpha = (float) Math.pow(alpha, 0.75);
-        return alpha;
-    }
-
-    private void updateHeaderKeyguardAlpha() {
-        if (!mKeyguardShowing) {
-            return;
-        }
-        float alphaQsExpansion = 1 - Math.min(1, computeQsExpansionFraction() * 2);
-        float newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
-                * mKeyguardStatusBarAnimateAlpha;
-        newAlpha *= 1.0f - mKeyguardHeadsUpShowingAmount;
-        mKeyguardStatusBar.setAlpha(newAlpha);
-        boolean
-                hideForBypass =
-                mFirstBypassAttempt && mUpdateMonitor.shouldListenForFace()
-                        || mDelayShowingKeyguardStatusBar;
-        mKeyguardStatusBar.setVisibility(
-                newAlpha != 0f && !mDozing && !hideForBypass ? View.VISIBLE : View.INVISIBLE);
-    }
-
     private void updateKeyguardBottomAreaAlpha() {
         // There are two possible panel expansion behaviors:
         // • User dragging up to unlock: we want to fade out as quick as possible
@@ -3051,20 +2965,6 @@
         mLockIconViewController.setAlpha(alpha);
     }
 
-    /**
-     * Custom clock fades away when user drags up to unlock or pulls down quick settings.
-     *
-     * Updates alpha of custom clock to match the alpha of the KeyguardBottomArea. See
-     * {@link #updateKeyguardBottomAreaAlpha}.
-     */
-    private void updateBigClockAlpha() {
-        float expansionAlpha = MathUtils.map(
-                isUnlockHintRunning() ? 0 : KeyguardBouncer.ALPHA_EXPANSION_THRESHOLD, 1f, 0f, 1f,
-                getExpandedFraction());
-        float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction());
-        mBigClockContainer.setAlpha(alpha);
-    }
-
     @Override
     protected void onExpandingStarted() {
         super.onExpandingStarted();
@@ -3085,7 +2985,7 @@
 
     @Override
     protected void onExpandingFinished() {
-        super.onExpandingFinished();
+        mScrimController.onExpandingFinished();
         mNotificationStackScrollLayoutController.onExpansionStopped();
         mHeadsUpManager.onExpandingFinished();
         mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed());
@@ -3128,7 +3028,7 @@
     }
 
     private void setListening(boolean listening) {
-        mKeyguardStatusBar.setListening(listening);
+        mKeyguardStatusBarViewController.setBatteryListening(listening);
         if (mQs == null) return;
         mQs.setListening(listening);
     }
@@ -3160,6 +3060,7 @@
     protected void onTrackingStarted() {
         mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen());
         super.onTrackingStarted();
+        mScrimController.onTrackingStarted();
         if (mQsFullyExpanded) {
             mQsExpandImmediate = true;
             if (!mShouldUseSplitNotificationShade) {
@@ -3170,6 +3071,7 @@
             mAffordanceHelper.animateHideLeftRightIcon();
         }
         mNotificationStackScrollLayoutController.onPanelTrackingStarted();
+        cancelPendingPanelCollapse();
     }
 
     @Override
@@ -3254,7 +3156,7 @@
     private void updateDozingVisibilities(boolean animate) {
         mKeyguardBottomArea.setDozing(mDozing, animate);
         if (!mDozing && animate) {
-            animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+            mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
         }
     }
 
@@ -3323,39 +3225,22 @@
         mPanelAlphaEndAction = r;
     }
 
-    private void updateKeyguardStatusBarForHeadsUp() {
-        boolean
-                showingKeyguardHeadsUp =
-                mKeyguardShowing && mHeadsUpAppearanceController.shouldBeVisible();
-        if (mShowingKeyguardHeadsUp != showingKeyguardHeadsUp) {
-            mShowingKeyguardHeadsUp = showingKeyguardHeadsUp;
-            if (mKeyguardShowing) {
-                PropertyAnimator.setProperty(mView, KEYGUARD_HEADS_UP_SHOWING_AMOUNT,
-                        showingKeyguardHeadsUp ? 1.0f : 0.0f, KEYGUARD_HUN_PROPERTIES,
-                        true /* animate */);
-            } else {
-                PropertyAnimator.applyImmediately(mView, KEYGUARD_HEADS_UP_SHOWING_AMOUNT, 0.0f);
-            }
-        }
-    }
-
-    private void setKeyguardHeadsUpShowingAmount(float amount) {
-        mKeyguardHeadsUpShowingAmount = amount;
-        updateHeaderKeyguardAlpha();
-    }
-
-    private float getKeyguardHeadsUpShowingAmount() {
-        return mKeyguardHeadsUpShowingAmount;
-    }
-
     public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
         mHeadsUpAnimatingAway = headsUpAnimatingAway;
         mNotificationStackScrollLayoutController.setHeadsUpAnimatingAway(headsUpAnimatingAway);
-        updateHeadsUpVisibility();
+        updateVisibility();
     }
 
-    private void updateHeadsUpVisibility() {
-        ((PhoneStatusBarView) mBar).setHeadsUpVisible(mHeadsUpAnimatingAway || mHeadsUpPinnedMode);
+    /** Set whether the bouncer is showing. */
+    public void setBouncerShowing(boolean bouncerShowing) {
+        mBouncerShowing = bouncerShowing;
+        updateVisibility();
+    }
+
+    @Override
+    protected boolean shouldPanelBeVisible() {
+        boolean headsUpVisible = mHeadsUpAnimatingAway || mHeadsUpPinnedMode;
+        return headsUpVisible || isExpanded() || mBouncerShowing;
     }
 
     @Override
@@ -3376,8 +3261,7 @@
 
     @Override
     protected void onClosingFinished() {
-        super.onClosingFinished();
-        resetHorizontalPanelPosition();
+        mStatusBar.onClosingFinished();
         setClosingWithAlphaFadeout(false);
         mMediaHierarchyManager.closeGuts();
     }
@@ -3387,47 +3271,6 @@
         mNotificationStackScrollLayoutController.forceNoOverlappingRendering(closing);
     }
 
-    /**
-     * Updates the horizontal position of the panel so it is positioned closer to the touch
-     * responsible for opening the panel.
-     *
-     * @param x the x-coordinate the touch event
-     */
-    protected void updateHorizontalPanelPosition(float x) {
-        if (mNotificationStackScrollLayoutController.getWidth() * 1.75f > mView.getWidth()
-                || mShouldUseSplitNotificationShade) {
-            resetHorizontalPanelPosition();
-            return;
-        }
-        float leftMost = mPositionMinSideMargin
-                + mNotificationStackScrollLayoutController.getWidth() / 2;
-        float
-                rightMost =
-                mView.getWidth() - mPositionMinSideMargin
-                        - mNotificationStackScrollLayoutController.getWidth() / 2;
-        if (Math.abs(x - mView.getWidth() / 2)
-                < mNotificationStackScrollLayoutController.getWidth() / 4) {
-            x = mView.getWidth() / 2;
-        }
-        x = Math.min(rightMost, Math.max(leftMost, x));
-        float
-                center = mNotificationStackScrollLayoutController.getLeft()
-                + mNotificationStackScrollLayoutController.getWidth() / 2;
-        setHorizontalPanelTranslation(x - center);
-    }
-
-    private void resetHorizontalPanelPosition() {
-        setHorizontalPanelTranslation(0f);
-    }
-
-    protected void setHorizontalPanelTranslation(float translation) {
-        mNotificationStackScrollLayoutController.setTranslationX(translation);
-        mQsFrame.setTranslationX(translation);
-        if (mVerticalTranslationListener != null) {
-            mVerticalTranslationListener.run();
-        }
-    }
-
     protected void updateExpandedHeight(float expandedHeight) {
         if (mTracking) {
             mNotificationStackScrollLayoutController
@@ -3439,7 +3282,6 @@
         }
         mNotificationStackScrollLayoutController.setExpandedHeight(expandedHeight);
         updateKeyguardBottomAreaAlpha();
-        updateBigClockAlpha();
         updateStatusBarIcons();
     }
 
@@ -3470,8 +3312,14 @@
         return mBarState == KEYGUARD;
     }
 
+    /**
+     * Sets the minimum fraction for the panel expansion offset. This may be non-zero in certain
+     * cases, such as if there's a heads-up notification.
+     */
     public void setPanelScrimMinFraction(float minFraction) {
-        mBar.onPanelMinFractionChanged(minFraction);
+        mMinFraction = minFraction;
+        mDepthController.setPanelPullDownMinFraction(mMinFraction);
+        mScrimController.setPanelScrimMinFraction(mMinFraction);
     }
 
     public void clearNotificationEffects() {
@@ -3587,7 +3435,7 @@
             mQs.setExpandClickListener(mOnClickListener);
             mQs.setHeaderClickable(isQsExpansionEnabled());
             mQs.setOverscrolling(mStackScrollerOverscrolling);
-            mQs.setTranslateWhileExpanding(mShouldUseSplitNotificationShade);
+            mQs.setInSplitShade(mShouldUseSplitNotificationShade);
 
             // recompute internal state when qspanel height changes
             mQs.getView().addOnLayoutChangeListener(
@@ -3650,6 +3498,7 @@
         mDozing = dozing;
         mNotificationStackScrollLayoutController.setDozing(mDozing, animate, wakeUpTouchLocation);
         mKeyguardBottomArea.setDozing(mDozing, animate);
+        mKeyguardStatusBarViewController.setDozing(mDozing);
 
         if (dozing) {
             mBottomAreaShadeAlphaAnimator.cancel();
@@ -3716,7 +3565,7 @@
     }
 
     public void applyLaunchAnimationProgress(float linearProgress) {
-        boolean hideIcons = ActivityLaunchAnimator.getProgress(linearProgress,
+        boolean hideIcons = LaunchAnimator.getProgress(linearProgress,
                 ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
         if (hideIcons != mHideIconsDuringLaunchAnimation) {
             mHideIconsDuringLaunchAnimation = hideIcons;
@@ -3734,10 +3583,6 @@
         mTrackingHeadsUpListeners.remove(listener);
     }
 
-    public void setVerticalTranslationListener(Runnable verticalTranslationListener) {
-        mVerticalTranslationListener = verticalTranslationListener;
-    }
-
     public void setHeadsUpAppearanceController(
             HeadsUpAppearanceController headsUpAppearanceController) {
         mHeadsUpAppearanceController = headsUpAppearanceController;
@@ -3764,6 +3609,11 @@
         }
     }
 
+    /** */
+    public void setImportantForAccessibility(int mode) {
+        mView.setImportantForAccessibility(mode);
+    }
+
     /**
      * Do not let the user drag the shade up and down for the current touch session.
      * This is necessary to avoid shade expansion while/after the bouncer is dismissed.
@@ -3777,10 +3627,9 @@
         super.dump(fd, pw, args);
         pw.println("    gestureExclusionRect: " + calculateGestureExclusionRect()
                 + " applyQSClippingImmediately: top(" + mQsClipTop + ") bottom(" + mQsClipBottom
-                + ") qsVisible(" + mQsVisible
-        );
-        if (mKeyguardStatusBar != null) {
-            mKeyguardStatusBar.dump(fd, pw, args);
+                + ") qsVisible(" + mQsVisible);
+        if (mKeyguardStatusBarViewController != null) {
+            mKeyguardStatusBarViewController.dump(fd, pw, args);
         }
     }
 
@@ -3842,13 +3691,27 @@
         mNotificationStackScrollLayoutController.setScrollingEnabled(b);
     }
 
+    private Runnable mHideExpandedRunnable;
+    private final Runnable mMaybeHideExpandedRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (getExpansionFraction() == 0.0f) {
+                mView.post(mHideExpandedRunnable);
+            }
+        }
+    };
+
     /**
      * Initialize objects instead of injecting to avoid circular dependencies.
+     *
+     * @param hideExpandedRunnable a runnable to run when we need to hide the expanded panel.
      */
     public void initDependencies(
             StatusBar statusBar,
+            Runnable hideExpandedRunnable,
             NotificationShelfController notificationShelfController) {
         setStatusBar(statusBar);
+        mHideExpandedRunnable = hideExpandedRunnable;
         mNotificationStackScrollLayoutController.setShelfController(notificationShelfController);
         mNotificationShelfController = notificationShelfController;
         mLockscreenShadeTransitionController.bindController(notificationShelfController);
@@ -3905,8 +3768,47 @@
             private long mLastTouchDownTime = -1L;
 
             @Override
+            public boolean onTouchForwardedFromStatusBar(MotionEvent event) {
+                // TODO(b/202981994): Move the touch debugging in this method to a central location.
+                //  (Right now, it's split between StatusBar and here.)
+
+                // If panels aren't enabled, ignore the gesture and don't pass it down to the
+                // panel view.
+                if (!mCommandQueue.panelsEnabled()) {
+                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                        Log.v(
+                                TAG,
+                                String.format(
+                                        "onTouchForwardedFromStatusBar: "
+                                                + "panel disabled, ignoring touch at (%d,%d)",
+                                        (int) event.getX(),
+                                        (int) event.getY()
+                                )
+                        );
+                    }
+                    return false;
+                }
+
+                // If the view that would receive the touch is disabled, just have status bar eat
+                // the gesture.
+                if (event.getAction() == MotionEvent.ACTION_DOWN && !mView.isEnabled()) {
+                    Log.v(TAG,
+                            String.format(
+                                    "onTouchForwardedFromStatusBar: "
+                                            + "panel view disabled, eating touch at (%d,%d)",
+                                    (int) event.getX(),
+                                    (int) event.getY()
+                            )
+                    );
+                    return true;
+                }
+
+                return mView.dispatchTouchEvent(event);
+            }
+
+            @Override
             public boolean onInterceptTouchEvent(MotionEvent event) {
-                if (mBlockTouches || mQsFullyExpanded && mQs.disallowPanelTouches()) {
+                if (mBlockTouches || mQs.disallowPanelTouches()) {
                     return false;
                 }
                 initDownStates(event);
@@ -3915,7 +3817,9 @@
                 if (mStatusBar.isBouncerShowing()) {
                     return true;
                 }
-                if (mBar.panelEnabled() && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+                if (mCommandQueue.panelsEnabled()
+                        && !mNotificationStackScrollLayoutController.isLongPressInProgress()
+                        && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
                     mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
                     mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
                     return true;
@@ -3980,6 +3884,7 @@
                     return true;
                 }
                 if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
+                        && !mNotificationStackScrollLayoutController.isLongPressInProgress()
                         && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
                     mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
                 }
@@ -3998,7 +3903,6 @@
                 }
                 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
                     mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
-                    updateHorizontalPanelPosition(event.getX());
                     handled = true;
                 }
 
@@ -4059,6 +3963,20 @@
         mContentResolver.unregisterContentObserver(mSettingsChangeObserver);
     }
 
+    /**
+     * Updates notification panel-specific flags on {@link SysUiState}.
+     */
+    public void updateSystemUiStateFlags() {
+        if (SysUiState.DEBUG) {
+            Log.d(TAG, "Updating panel sysui state flags: fullyExpanded="
+                    + isFullyExpanded() + " inQs=" + isInSettings());
+        }
+        mSysUiState.setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
+                isFullyExpanded() && !isInSettings())
+                .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isInSettings())
+                .commitUpdate(mDisplayId);
+    }
+
     private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener {
         @Override
         public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
@@ -4127,9 +4045,7 @@
         @Override
         public void flingTopOverscroll(float velocity, boolean open) {
             // in split shade mode we want to expand/collapse QS only when touch happens within QS
-            if (mShouldUseSplitNotificationShade
-                    && (mInitialTouchX < mQsFrame.getX()
-                        || mInitialTouchX > mQsFrame.getX() + mQsFrame.getWidth())) {
+            if (mShouldUseSplitNotificationShade && touchXOutsideOfQs(mInitialTouchX)) {
                 return;
             }
             mLastOverscroll = 0f;
@@ -4324,8 +4240,8 @@
             }
             updateGestureExclusionRect();
             mHeadsUpPinnedMode = inPinnedMode;
-            updateHeadsUpVisibility();
-            updateKeyguardStatusBarForHeadsUp();
+            updateVisibility();
+            mKeyguardStatusBarViewController.updateForHeadsUp();
         }
 
         @Override
@@ -4370,12 +4286,7 @@
         @Override
         public void onThemeChanged() {
             if (DEBUG) Log.d(TAG, "onThemeChanged");
-            final int themeResId = mView.getContext().getThemeResId();
-            if (mThemeResId == themeResId) {
-                return;
-            }
-            mThemeResId = themeResId;
-
+            mThemeResId = mView.getContext().getThemeResId();
             reInflateViews();
         }
 
@@ -4389,12 +4300,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            if (DEBUG) Log.d(TAG, "onOverlayChanged");
-            reInflateViews();
-        }
-
-        @Override
         public void onDensityOrFontScaleChanged() {
             if (DEBUG) Log.d(TAG, "onDensityOrFontScaleChanged");
             reInflateViews();
@@ -4448,11 +4353,22 @@
 
             if (oldState == KEYGUARD && (goingToFullShade
                     || statusBarState == StatusBarState.SHADE_LOCKED)) {
-                animateKeyguardStatusBarOut();
+
+                long startDelay;
+                long duration;
+                if (mKeyguardStateController.isKeyguardFadingAway()) {
+                    startDelay = mKeyguardStateController.getKeyguardFadingAwayDelay();
+                    duration = mKeyguardStateController.getShortenedFadingAwayDuration();
+                } else {
+                    startDelay = 0;
+                    duration = StackStateAnimator.ANIMATION_DURATION_STANDARD;
+                }
+                mKeyguardStatusBarViewController.animateKeyguardStatusBarOut(startDelay, duration);
                 updateQSMinHeight();
             } else if (oldState == StatusBarState.SHADE_LOCKED
                     && statusBarState == KEYGUARD) {
-                animateKeyguardStatusBarIn(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+                mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
+
                 mNotificationStackScrollLayoutController.resetScrollPosition();
                 // Only animate header if the header is visible. If not, it will partially
                 // animate out
@@ -4464,15 +4380,23 @@
                     }
                 }
             } else {
-                mKeyguardStatusBar.setAlpha(1f);
-                mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
+                final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE
+                        && statusBarState == KEYGUARD
+                        && mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying();
+                if (!animatingUnlockedShadeToKeyguard) {
+                    // Only make the status bar visible if we're not animating the screen off, since
+                    // we only want to be showing the clock/notifications during the animation.
+                    mKeyguardStatusBarViewController.updateViewState(
+                            /* alpha= */ 1f,
+                            keyguardShowing ? View.VISIBLE : View.INVISIBLE);
+                }
                 if (keyguardShowing && oldState != mBarState) {
                     if (mQs != null) {
                         mQs.hideImmediately();
                     }
                 }
             }
-            updateKeyguardStatusBarForHeadsUp();
+            mKeyguardStatusBarViewController.updateForHeadsUp();
             if (keyguardShowing) {
                 updateDozingVisibilities(false /* animate */);
             }
@@ -4481,8 +4405,9 @@
             // The update needs to happen after the headerSlide in above, otherwise the translation
             // would reset
             maybeAnimateBottomAreaAlpha();
-            resetHorizontalPanelPosition();
             updateQsState();
+            mSplitShadeHeaderController.setShadeExpanded(
+                    mBarState == SHADE || mBarState == SHADE_LOCKED);
         }
 
         @Override
@@ -4496,6 +4421,43 @@
     }
 
     /**
+     * An interface that provides the current state of the notification panel and related views,
+     * which is needed to calculate {@link KeyguardStatusBarView}'s state in
+     * {@link KeyguardStatusBarViewController}.
+     */
+    public interface NotificationPanelViewStateProvider {
+        /** Returns the expanded height of the panel view. */
+        float getPanelViewExpandedHeight();
+        /** Returns the fraction of QS that's expanded. */
+        float getQsExpansionFraction();
+        /**
+         * Returns true if heads up should be visible.
+         *
+         * TODO(b/138786270): If HeadsUpAppearanceController was injectable, we could inject it into
+         * {@link KeyguardStatusBarViewController} and remove this method.
+         */
+        boolean shouldHeadsUpBeVisible();
+    }
+
+    private final NotificationPanelViewStateProvider mNotificationPanelViewStateProvider =
+            new NotificationPanelViewStateProvider() {
+                @Override
+                public float getPanelViewExpandedHeight() {
+                    return getExpandedHeight();
+                }
+
+                @Override
+                public float getQsExpansionFraction() {
+                    return computeQsExpansionFraction();
+                }
+
+                @Override
+                public boolean shouldHeadsUpBeVisible() {
+                    return mHeadsUpAppearanceController.shouldBeVisible();
+                }
+            };
+
+    /**
      * Reconfigures the shade to show the AOD UI (clock, smartspace, etc). This is called by the
      * screen off animation controller in order to animate in AOD without "actually" fully switching
      * to the KEYGUARD state, which is a heavy transition that causes jank as 10+ files react to the
@@ -4527,7 +4489,6 @@
                             .addTagListener(QS.TAG, mFragmentListener);
             mStatusBarStateController.addCallback(mStatusBarStateListener);
             mConfigurationController.addCallback(mConfigurationListener);
-            mUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
             // Theme might have changed between inflating this view and attaching it to the
             // window, so
             // force a call to onThemeChanged
@@ -4544,7 +4505,6 @@
                             .removeTagListener(QS.TAG, mFragmentListener);
             mStatusBarStateController.removeCallback(mStatusBarStateListener);
             mConfigurationController.removeCallback(mConfigurationListener);
-            mUpdateMonitor.removeCallback(mKeyguardUpdateCallback);
             mFalsingManager.removeTapListener(mFalsingTapListener);
         }
     }
@@ -4670,9 +4630,6 @@
         public void onConfigurationChanged(Configuration newConfig) {
             super.onConfigurationChanged(newConfig);
             mAffordanceHelper.onConfigurationChanged();
-            if (newConfig.orientation != mLastOrientation) {
-                resetHorizontalPanelPosition();
-            }
             mLastOrientation = newConfig.orientation;
         }
     }
@@ -4690,4 +4647,45 @@
             return insets;
         }
     }
+
+    /** Removes any pending runnables that would collapse the panel. */
+    public void cancelPendingPanelCollapse() {
+        mView.removeCallbacks(mMaybeHideExpandedRunnable);
+    }
+
+    private final PanelBar.PanelStateChangeListener mPanelStateChangeListener =
+            new PanelBar.PanelStateChangeListener() {
+
+                @PanelBar.PanelState
+                private int mCurrentState = STATE_CLOSED;
+
+                @Override
+                public void onStateChanged(@PanelBar.PanelState int state) {
+                    mAmbientState.setIsShadeOpening(state == STATE_OPENING);
+                    updateQSExpansionEnabledAmbient();
+
+                    if (state == STATE_OPEN && mCurrentState != state) {
+                        mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+                    }
+                    if (state == STATE_OPENING) {
+                        mStatusBar.makeExpandedVisible(false);
+                    }
+                    if (state == STATE_CLOSED) {
+                        // Close the status bar in the next frame so we can show the end of the
+                        // animation.
+                        mView.post(mMaybeHideExpandedRunnable);
+                    }
+                    mCurrentState = state;
+                }
+            };
+
+    public PanelBar.PanelStateChangeListener getPanelStateChangeListener() {
+        return mPanelStateChangeListener;
+    }
+
+
+    /** Returns the handler that the status bar should forward touches to. */
+    public PhoneStatusBarView.TouchEventHandler getStatusBarTouchEventHandler() {
+        return getTouchHandler()::onTouchForwardedFromStatusBar;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
index c26782b..030a895 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
@@ -84,8 +84,8 @@
     private final WindowManager mWindowManager;
     private final IActivityManager mActivityManager;
     private final DozeParameters mDozeParameters;
+    private final KeyguardStateController mKeyguardStateController;
     private final LayoutParams mLpChanged;
-    private final boolean mKeyguardScreenRotation;
     private final long mLockScreenDisplayTimeout;
     private final float mKeyguardPreferredRefreshRate; // takes precedence over max
     private final float mKeyguardMaxRefreshRate;
@@ -123,8 +123,8 @@
         mContext = context;
         mWindowManager = windowManager;
         mActivityManager = activityManager;
-        mKeyguardScreenRotation = keyguardStateController.isKeyguardScreenRotationAllowed();
         mDozeParameters = dozeParameters;
+        mKeyguardStateController = keyguardStateController;
         mScreenBrightnessDoze = mDozeParameters.getScreenBrightnessDoze();
         mLpChanged = new LayoutParams();
         mKeyguardViewMediator = keyguardViewMediator;
@@ -323,7 +323,7 @@
 
     private void adjustScreenOrientation(State state) {
         if (state.isKeyguardShowingAndNotOccluded() || state.mDozing) {
-            if (mKeyguardScreenRotation) {
+            if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) {
                 mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER;
             } else {
                 mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
@@ -473,7 +473,8 @@
         for (StatusBarWindowCallback cb : activeCallbacks) {
             cb.onStateChanged(mCurrentState.mKeyguardShowing,
                     mCurrentState.mKeyguardOccluded,
-                    mCurrentState.mBouncerShowing);
+                    mCurrentState.mBouncerShowing,
+                    mCurrentState.mDozing);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 3807b46..9d06d2b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -48,7 +48,6 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -108,7 +107,7 @@
     private boolean mExpandingBelowNotch;
     private final DockManager mDockManager;
     private final NotificationPanelViewController mNotificationPanelViewController;
-    private final SuperStatusBarViewFactory mStatusBarViewFactory;
+    private final StatusBarWindowView mStatusBarWindowView;
 
     // Used for determining view / touch intersection
     private int[] mTempLocation = new int[2];
@@ -138,7 +137,7 @@
             NotificationShadeDepthController depthController,
             NotificationShadeWindowView notificationShadeWindowView,
             NotificationPanelViewController notificationPanelViewController,
-            SuperStatusBarViewFactory statusBarViewFactory,
+            StatusBarWindowView statusBarWindowView,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             LockIconViewController lockIconViewController) {
@@ -163,7 +162,7 @@
         mDockManager = dockManager;
         mNotificationPanelViewController = notificationPanelViewController;
         mDepthController = depthController;
-        mStatusBarViewFactory = statusBarViewFactory;
+        mStatusBarWindowView = statusBarWindowView;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLockIconViewController = lockIconViewController;
@@ -172,6 +171,13 @@
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
     }
 
+    /**
+     * @return Location where to place the KeyguardBouncer
+     */
+    public ViewGroup getBouncerContainer() {
+        return mView.findViewById(R.id.keyguard_bouncer_container);
+    }
+
     /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
     public void setupExpandedStatusBar() {
         mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
@@ -491,11 +497,10 @@
 
     public void setStatusBarView(PhoneStatusBarView statusBarView) {
         mStatusBarView = statusBarView;
-        if (statusBarView != null && mStatusBarViewFactory != null) {
+        if (statusBarView != null) {
             mBarTransitions = new PhoneStatusBarTransitions(
                     statusBarView,
-                    mStatusBarViewFactory.getStatusBarWindowView()
-                            .findViewById(R.id.status_bar_container));
+                    mStatusBarWindowView.findViewById(R.id.status_bar_container));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt
new file mode 100644
index 0000000..34bb6d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt
@@ -0,0 +1,141 @@
+package com.android.systemui.statusbar.phone
+
+import android.view.WindowInsets
+import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.plugins.qs.QS
+import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+import com.android.systemui.shared.system.QuickStepContract
+import com.android.systemui.util.ViewController
+import java.util.function.Consumer
+import javax.inject.Inject
+
+class NotificationsQSContainerController @Inject constructor(
+    view: NotificationsQuickSettingsContainer,
+    private val navigationModeController: NavigationModeController,
+    private val overviewProxyService: OverviewProxyService
+) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
+
+    var qsExpanded = false
+        set(value) {
+            if (field != value) {
+                field = value
+                mView.invalidate()
+            }
+        }
+    var splitShadeEnabled = false
+        set(value) {
+            if (field != value) {
+                field = value
+                // in case device configuration changed while showing QS details/customizer
+                updateBottomSpacing()
+            }
+        }
+
+    private var isQSDetailShowing = false
+    private var isQSCustomizing = false
+    private var isQSCustomizerAnimating = false
+
+    private var notificationsBottomMargin = 0
+    private var bottomStableInsets = 0
+    private var bottomCutoutInsets = 0
+
+    private var isGestureNavigation = true
+    private var taskbarVisible = false
+    private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener {
+        override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
+            taskbarVisible = visible
+        }
+    }
+    private val windowInsetsListener: Consumer<WindowInsets> = Consumer { insets ->
+        // when taskbar is visible, stableInsetBottom will include its height
+        bottomStableInsets = insets.stableInsetBottom
+        bottomCutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
+        updateBottomSpacing()
+    }
+
+    override fun onInit() {
+        val currentMode: Int = navigationModeController.addListener { mode: Int ->
+            isGestureNavigation = QuickStepContract.isGesturalMode(mode)
+        }
+        isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
+    }
+
+    public override fun onViewAttached() {
+        notificationsBottomMargin = mView.defaultNotificationsMarginBottom
+        overviewProxyService.addCallback(taskbarVisibilityListener)
+        mView.setInsetsChangedListener(windowInsetsListener)
+        mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
+    }
+
+    override fun onViewDetached() {
+        overviewProxyService.removeCallback(taskbarVisibilityListener)
+        mView.removeOnInsetsChangedListener()
+        mView.removeQSFragmentAttachedListener()
+    }
+
+    override fun setCustomizerAnimating(animating: Boolean) {
+        if (isQSCustomizerAnimating != animating) {
+            isQSCustomizerAnimating = animating
+            mView.invalidate()
+        }
+    }
+
+    override fun setCustomizerShowing(showing: Boolean) {
+        isQSCustomizing = showing
+        updateBottomSpacing()
+    }
+
+    override fun setDetailShowing(showing: Boolean) {
+        isQSDetailShowing = showing
+        updateBottomSpacing()
+    }
+
+    private fun updateBottomSpacing() {
+        val (containerPadding, notificationsMargin) = calculateBottomSpacing()
+        var qsScrollPaddingBottom = 0
+        if (!(splitShadeEnabled || isQSCustomizing || isQSDetailShowing || isGestureNavigation ||
+                        taskbarVisible)) {
+            // no taskbar, portrait, navigation buttons enabled:
+            // padding is needed so QS can scroll up over bottom insets - to reach the point when
+            // the whole QS is above bottom insets
+            qsScrollPaddingBottom = bottomStableInsets
+        }
+        mView.setPadding(0, 0, 0, containerPadding)
+        mView.setNotificationsMarginBottom(notificationsMargin)
+        mView.setQSScrollPaddingBottom(qsScrollPaddingBottom)
+    }
+
+    private fun calculateBottomSpacing(): Pair<Int, Int> {
+        val containerPadding: Int
+        var stackScrollMargin = notificationsBottomMargin
+        if (splitShadeEnabled) {
+            if (isGestureNavigation) {
+                // only default cutout padding, taskbar always hides
+                containerPadding = bottomCutoutInsets
+            } else if (taskbarVisible) {
+                // navigation buttons + visible taskbar means we're NOT on homescreen
+                containerPadding = bottomStableInsets
+            } else {
+                // navigation buttons + hidden taskbar means we're on homescreen
+                containerPadding = 0
+                // we need extra margin for notifications as navigation buttons are below them
+                stackScrollMargin = bottomStableInsets + notificationsBottomMargin
+            }
+        } else {
+            if (isQSCustomizing || isQSDetailShowing) {
+                // Clear out bottom paddings/margins so the qs customization can be full height.
+                containerPadding = 0
+                stackScrollMargin = 0
+            } else if (isGestureNavigation) {
+                containerPadding = bottomCutoutInsets
+            } else if (taskbarVisible) {
+                containerPadding = bottomStableInsets
+            } else {
+                containerPadding = 0
+            }
+        }
+        return containerPadding to stackScrollMargin
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
index ed8fb31..9210a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -22,7 +22,6 @@
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.WindowInsets;
-import android.widget.FrameLayout;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 
@@ -31,10 +30,10 @@
 import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.function.Consumer;
 
 /**
  * The container with notification stack scroller and quick settings inside.
@@ -42,20 +41,18 @@
 public class NotificationsQuickSettingsContainer extends ConstraintLayout
         implements FragmentListener, AboveShelfObserver.HasViewAboveShelfChangedListener {
 
-    private FrameLayout mQsFrame;
-    private NotificationStackScrollLayout mStackScroller;
+    private View mQsFrame;
+    private View mStackScroller;
     private View mKeyguardStatusBar;
-    private boolean mQsExpanded;
-    private boolean mCustomizerAnimating;
-    private boolean mCustomizing;
-    private boolean mDetailShowing;
 
-    private int mBottomPadding;
     private int mStackScrollerMargin;
-    private boolean mHasViewsAboveShelf;
     private ArrayList<View> mDrawingOrderedChildren = new ArrayList<>();
     private ArrayList<View> mLayoutDrawingOrder = new ArrayList<>();
     private final Comparator<View> mIndexComparator = Comparator.comparingInt(this::indexOfChild);
+    private Consumer<WindowInsets> mInsetsChangedListener = insets -> {};
+    private Consumer<QS> mQSFragmentAttachedListener = qs -> {};
+    private QS mQs;
+    private View mQSScrollView;
 
     public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -71,6 +68,59 @@
     }
 
     @Override
+    public void onFragmentViewCreated(String tag, Fragment fragment) {
+        mQs = (QS) fragment;
+        mQSFragmentAttachedListener.accept(mQs);
+        mQSScrollView = mQs.getView().findViewById(R.id.expanded_qs_scroll_view);
+    }
+
+    @Override
+    public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
+        invalidate();
+    }
+
+    public void setNotificationsMarginBottom(int margin) {
+        LayoutParams params = (LayoutParams) mStackScroller.getLayoutParams();
+        params.bottomMargin = margin;
+        mStackScroller.setLayoutParams(params);
+    }
+
+    public void setQSScrollPaddingBottom(int paddingBottom) {
+        if (mQSScrollView != null) {
+            mQSScrollView.setPaddingRelative(
+                    mQSScrollView.getPaddingLeft(),
+                    mQSScrollView.getPaddingTop(),
+                    mQSScrollView.getPaddingRight(),
+                    paddingBottom
+            );
+        }
+    }
+
+    public int getDefaultNotificationsMarginBottom() {
+        return mStackScrollerMargin;
+    }
+
+    public void setInsetsChangedListener(Consumer<WindowInsets> onInsetsChangedListener) {
+        mInsetsChangedListener = onInsetsChangedListener;
+    }
+
+    public void removeOnInsetsChangedListener() {
+        mInsetsChangedListener = insets -> {};
+    }
+
+    public void setQSFragmentAttachedListener(Consumer<QS> qsFragmentAttachedListener) {
+        mQSFragmentAttachedListener = qsFragmentAttachedListener;
+        // listener might be attached after fragment is attached
+        if (mQs != null) {
+            mQSFragmentAttachedListener.accept(mQs);
+        }
+    }
+
+    public void removeQSFragmentAttachedListener() {
+        mQSFragmentAttachedListener = qs -> {};
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         FragmentHostManager.get(this).addTagListener(QS.TAG, this);
@@ -84,8 +134,7 @@
 
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mBottomPadding = insets.getStableInsetBottom();
-        setPadding(0, 0, 0, mBottomPadding);
+        mInsetsChangedListener.accept(insets);
         return insets;
     }
 
@@ -121,57 +170,4 @@
         }
     }
 
-    @Override
-    public void onFragmentViewCreated(String tag, Fragment fragment) {
-        QS container = (QS) fragment;
-        container.setContainer(this);
-    }
-
-    public void setQsExpanded(boolean expanded) {
-        if (mQsExpanded != expanded) {
-            mQsExpanded = expanded;
-            invalidate();
-        }
-    }
-
-    public void setCustomizerAnimating(boolean isAnimating) {
-        if (mCustomizerAnimating != isAnimating) {
-            mCustomizerAnimating = isAnimating;
-            invalidate();
-        }
-    }
-
-    public void setCustomizerShowing(boolean isShowing) {
-        mCustomizing = isShowing;
-        updateBottomMargin();
-        mStackScroller.setQsCustomizerShowing(isShowing);
-    }
-
-    public void setDetailShowing(boolean isShowing) {
-        mDetailShowing = isShowing;
-        updateBottomMargin();
-    }
-
-    private void updateBottomMargin() {
-        if (mCustomizing || mDetailShowing) {
-            // Clear out bottom paddings/margins so the qs customization can be full height.
-            setPadding(0, 0, 0, 0);
-            setBottomMargin(mStackScroller, 0);
-        } else {
-            setPadding(0, 0, 0, mBottomPadding);
-            setBottomMargin(mStackScroller, mStackScrollerMargin);
-        }
-    }
-
-    private void setBottomMargin(View v, int bottomMargin) {
-        LayoutParams params = (LayoutParams) v.getLayoutParams();
-        params.bottomMargin = bottomMargin;
-        v.setLayoutParams(params);
-    }
-
-    @Override
-    public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
-        mHasViewsAboveShelf = hasViewsAboveShelf;
-        invalidate();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
index eca91a3..e90258d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
@@ -17,24 +17,26 @@
 package com.android.systemui.statusbar.phone;
 
 import static java.lang.Float.isNaN;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-import android.annotation.CallSuper;
+import android.annotation.IntDef;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.view.MotionEvent;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+
 public abstract class PanelBar extends FrameLayout {
     public static final boolean DEBUG = false;
     public static final String TAG = PanelBar.class.getSimpleName();
     private static final boolean SPEW = false;
     private static final String PANEL_BAR_SUPER_PARCELABLE = "panel_bar_super_parcelable";
     private static final String STATE = "state";
-    private boolean mBouncerShowing;
-    private boolean mExpanded;
     protected float mPanelFraction;
 
     public static final void LOG(String fmt, Object... args) {
@@ -42,24 +44,32 @@
         Log.v(TAG, String.format(fmt, args));
     }
 
+    /** Enum for the current state of the panel. */
+    @Retention(SOURCE)
+    @IntDef({STATE_CLOSED, STATE_OPENING, STATE_OPEN})
+    @interface PanelState {}
     public static final int STATE_CLOSED = 0;
     public static final int STATE_OPENING = 1;
     public static final int STATE_OPEN = 2;
 
-    PanelViewController mPanel;
+    @Nullable private PanelStateChangeListener mPanelStateChangeListener;
     private int mState = STATE_CLOSED;
     private boolean mTracking;
 
-    public void go(int state) {
-        if (DEBUG) LOG("go state: %d -> %d", mState, state);
-        mState = state;
-        if (mPanel != null) {
-            mPanel.setIsShadeOpening(state == STATE_OPENING);
+    /** Updates the panel state if necessary. */
+    public void updateState(@PanelState int state) {
+        if (DEBUG) LOG("update state: %d -> %d", mState, state);
+        if (mState != state) {
+            go(state);
         }
     }
 
-    protected boolean isShadeOpening() {
-        return mState == STATE_OPENING;
+    private void go(@PanelState int state) {
+        if (DEBUG) LOG("go state: %d -> %d", mState, state);
+        mState = state;
+        if (mPanelStateChangeListener != null) {
+            mPanelStateChangeListener.onStateChanged(state);
+        }
     }
 
     @Override
@@ -93,82 +103,9 @@
         super.onFinishInflate();
     }
 
-    /** Set the PanelViewController */
-    public void setPanel(PanelViewController pv) {
-        mPanel = pv;
-        pv.setBar(this);
-    }
-
-    public void setBouncerShowing(boolean showing) {
-        mBouncerShowing = showing;
-        int important = showing ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
-
-        setImportantForAccessibility(important);
-        updateVisibility();
-
-        if (mPanel != null) mPanel.getView().setImportantForAccessibility(important);
-    }
-
-    public float getExpansionFraction() {
-        return mPanelFraction;
-    }
-
-    public boolean isExpanded() {
-        return mExpanded;
-    }
-
-    protected void updateVisibility() {
-        mPanel.getView().setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
-    }
-
-    protected boolean shouldPanelBeVisible() {
-        return mExpanded || mBouncerShowing;
-    }
-
-    public boolean panelEnabled() {
-        return true;
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        // Allow subclasses to implement enable/disable semantics
-        if (!panelEnabled()) {
-            if (event.getAction() == MotionEvent.ACTION_DOWN) {
-                Log.v(TAG, String.format("onTouch: all panels disabled, ignoring touch at (%d,%d)",
-                        (int) event.getX(), (int) event.getY()));
-            }
-            return false;
-        }
-
-        if (event.getAction() == MotionEvent.ACTION_DOWN) {
-            final PanelViewController panel = mPanel;
-            if (panel == null) {
-                // panel is not there, so we'll eat the gesture
-                Log.v(TAG, String.format("onTouch: no panel for touch at (%d,%d)",
-                        (int) event.getX(), (int) event.getY()));
-                return true;
-            }
-            boolean enabled = panel.isEnabled();
-            if (DEBUG) LOG("PanelBar.onTouch: state=%d ACTION_DOWN: panel %s %s", mState, panel,
-                    (enabled ? "" : " (disabled)"));
-            if (!enabled) {
-                // panel is disabled, so we'll eat the gesture
-                Log.v(TAG, String.format(
-                        "onTouch: panel (%s) is disabled, ignoring touch at (%d,%d)",
-                        panel, (int) event.getX(), (int) event.getY()));
-                return true;
-            }
-        }
-        return mPanel == null || mPanel.getView().dispatchTouchEvent(event);
-    }
-
-    /**
-     * Percentage of panel expansion offset, caused by pulling down on a heads-up.
-     */
-    @CallSuper
-    public void onPanelMinFractionChanged(float minFraction) {
-        mPanel.setMinFraction(minFraction);
+    /** Sets the listener that will be notified of panel state changes. */
+    public void setPanelStateChangeListener(PanelStateChangeListener listener) {
+        mPanelStateChangeListener = listener;
     }
 
     /**
@@ -182,69 +119,30 @@
         }
         boolean fullyClosed = true;
         boolean fullyOpened = false;
-        if (SPEW) LOG("panelExpansionChanged: start state=%d", mState);
-        PanelViewController pv = mPanel;
-        mExpanded = expanded;
+        if (SPEW) LOG("panelExpansionChanged: start state=%d, f=%.1f", mState, frac);
         mPanelFraction = frac;
-        updateVisibility();
         // adjust any other panels that may be partially visible
         if (expanded) {
             if (mState == STATE_CLOSED) {
                 go(STATE_OPENING);
-                onPanelPeeked();
             }
             fullyClosed = false;
-            final float thisFrac = pv.getExpandedFraction();
-            if (SPEW) LOG("panelExpansionChanged:  -> %s: f=%.1f", pv.getName(), thisFrac);
-            fullyOpened = thisFrac >= 1f;
+            fullyOpened = frac >= 1f;
         }
         if (fullyOpened && !mTracking) {
             go(STATE_OPEN);
-            onPanelFullyOpened();
         } else if (fullyClosed && !mTracking && mState != STATE_CLOSED) {
             go(STATE_CLOSED);
-            onPanelCollapsed();
         }
 
         if (SPEW) LOG("panelExpansionChanged: end state=%d [%s%s ]", mState,
                 fullyOpened?" fullyOpened":"", fullyClosed?" fullyClosed":"");
     }
 
-    public void collapsePanel(boolean animate, boolean delayed, float speedUpFactor) {
-        boolean waiting = false;
-        PanelViewController pv = mPanel;
-        if (animate && !pv.isFullyCollapsed()) {
-            pv.collapse(delayed, speedUpFactor);
-            waiting = true;
-        } else {
-            pv.resetViews(false /* animate */);
-            pv.setExpandedFraction(0); // just in case
-        }
-        if (DEBUG) LOG("collapsePanel: animate=%s waiting=%s", animate, waiting);
-        if (!waiting && mState != STATE_CLOSED) {
-            // it's possible that nothing animated, so we replicate the termination
-            // conditions of panelExpansionChanged here
-            go(STATE_CLOSED);
-            onPanelCollapsed();
-        }
-    }
-
-    public void onPanelPeeked() {
-        if (DEBUG) LOG("onPanelPeeked");
-    }
-
     public boolean isClosed() {
         return mState == STATE_CLOSED;
     }
 
-    public void onPanelCollapsed() {
-        if (DEBUG) LOG("onPanelCollapsed");
-    }
-
-    public void onPanelFullyOpened() {
-        if (DEBUG) LOG("onPanelFullyOpened");
-    }
-
     public void onTrackingStarted() {
         mTracking = true;
     }
@@ -253,11 +151,9 @@
         mTracking = false;
     }
 
-    public void onExpandingFinished() {
-        if (DEBUG) LOG("onExpandingFinished");
-    }
-
-    public void onClosingFinished() {
-
+    /** An interface that will be notified of panel state changes. */
+    public interface PanelStateChangeListener {
+        /** Called when the state changes. */
+        void onStateChanged(@PanelState int state);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
index 99c0c13..e5296af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
 import static com.android.systemui.classifier.Classifier.GENERIC;
@@ -79,7 +82,6 @@
     protected long mDownTime;
     protected boolean mTouchSlopExceededBeforeDown;
     private float mMinExpandHeight;
-    private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
     private boolean mPanelUpdateWhenAnimatorEnds;
     private boolean mVibrateOnOpening;
     protected boolean mIsLaunchAnimationRunning;
@@ -182,10 +184,10 @@
     protected final KeyguardStateController mKeyguardStateController;
     protected final SysuiStatusBarStateController mStatusBarStateController;
     protected final AmbientState mAmbientState;
+    protected final LockscreenGestureLogger mLockscreenGestureLogger;
+    private final TouchHandler mTouchHandler;
 
-    protected void onExpandingFinished() {
-        mBar.onExpandingFinished();
-    }
+    protected abstract void onExpandingFinished();
 
     protected void onExpandingStarted() {
     }
@@ -217,10 +219,13 @@
             LatencyTracker latencyTracker,
             FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
+            LockscreenGestureLogger lockscreenGestureLogger,
             AmbientState ambientState) {
         mAmbientState = ambientState;
         mView = view;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mLockscreenGestureLogger = lockscreenGestureLogger;
+        mTouchHandler = createTouchHandler();
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -233,7 +238,7 @@
         });
 
         mView.addOnLayoutChangeListener(createLayoutChangeListener());
-        mView.setOnTouchListener(createTouchHandler());
+        mView.setOnTouchListener(mTouchHandler);
         mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
 
         mResources = mView.getResources();
@@ -284,6 +289,10 @@
                 : mTouchSlop;
     }
 
+    protected TouchHandler getTouchHandler() {
+        return mTouchHandler;
+    }
+
     private void addMovement(MotionEvent event) {
         // Add movement to velocity tracker using raw screen X and Y coordinates instead
         // of window coordinates because the window frame may be moving at the same time.
@@ -313,7 +322,7 @@
     }
 
     private void startOpening(MotionEvent event) {
-        notifyBarPanelExpansionChanged();
+        updatePanelExpansionAndVisibility();
         maybeVibrateOnOpening();
 
         //TODO: keyguard opens QS a different way; log that too?
@@ -338,13 +347,6 @@
     protected abstract float getOpeningHeight();
 
     /**
-     * Minimum fraction from where expansion should start. This is set when pulling down on a
-     * heads-up notification.
-     * @param minFraction Fraction from 0 to 1.
-     */
-    public abstract void setMinFraction(float minFraction);
-
-    /**
      * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
      * horizontal direction
      */
@@ -387,11 +389,22 @@
 
             final boolean expand;
             if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
-                // If we get a cancel, put the shade back to the state it was in when the gesture
-                // started
-                if (onKeyguard) {
+                // If the keyguard is fading away, don't expand it again. This can happen if you're
+                // swiping to unlock, the app below the keyguard is in landscape, and the screen
+                // rotates while your finger is still down after the swipe to unlock.
+                if (mKeyguardStateController.isKeyguardFadingAway()) {
+                    expand = false;
+                } else if (onKeyguard) {
                     expand = true;
+                } else if (mKeyguardStateController.isKeyguardFadingAway()) {
+                    // If we're in the middle of dismissing the keyguard, don't expand due to the
+                    // cancelled gesture. Gesture cancellation during an unlock is expected in some
+                    // situations, such keeping your finger down while swiping to unlock to an app
+                    // that is locked in landscape (the rotation will cancel the touch event).
+                    expand = false;
                 } else {
+                    // If we get a cancel, put the shade back to the state it was in when the
+                    // gesture started
                     expand = !mPanelClosedOnDown;
                 }
             } else {
@@ -445,15 +458,17 @@
     protected void onTrackingStopped(boolean expand) {
         mTracking = false;
         mBar.onTrackingStopped(expand);
-        notifyBarPanelExpansionChanged();
+        mStatusBar.onTrackingStopped(expand);
+        updatePanelExpansionAndVisibility();
     }
 
     protected void onTrackingStarted() {
         endClosing();
         mTracking = true;
         mBar.onTrackingStarted();
+        mStatusBar.onTrackingStarted();
         notifyExpandingStarted();
-        notifyBarPanelExpansionChanged();
+        updatePanelExpansionAndVisibility();
     }
 
     /**
@@ -683,7 +698,7 @@
         } else {
             cancelJankMonitoring(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
         }
-        notifyBarPanelExpansionChanged();
+        updatePanelExpansionAndVisibility();
     }
 
     protected abstract boolean shouldUseDismissingAnimation();
@@ -758,7 +773,7 @@
         mExpandedFraction = Math.min(1f,
                 maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
         onHeightUpdated(mExpandedHeight);
-        notifyBarPanelExpansionChanged();
+        updatePanelExpansionAndVisibility();
     }
 
     /**
@@ -876,7 +891,7 @@
         if (mExpanding) {
             notifyExpandingFinished();
         }
-        notifyBarPanelExpansionChanged();
+        updatePanelExpansionAndVisibility();
 
         // Wait for window manager to pickup the change, so we know the maximum height of the panel
         // then.
@@ -914,7 +929,7 @@
         }
         if (mInstantExpanding) {
             mInstantExpanding = false;
-            notifyBarPanelExpansionChanged();
+            updatePanelExpansionAndVisibility();
         }
     }
 
@@ -924,10 +939,7 @@
         mView.removeCallbacks(mFlingCollapseRunnable);
     }
 
-    protected void onClosingFinished() {
-        mBar.onClosingFinished();
-    }
-
+    protected abstract void onClosingFinished();
 
     protected void startUnlockHintAnimation() {
 
@@ -1020,7 +1032,7 @@
             public void onAnimationEnd(Animator animation) {
                 setAnimator(null);
                 onAnimationFinished.run();
-                notifyBarPanelExpansionChanged();
+                updatePanelExpansionAndVisibility();
             }
         });
         animator.start();
@@ -1057,19 +1069,39 @@
         return animator;
     }
 
-    protected void notifyBarPanelExpansionChanged() {
+    /** Update the visibility of {@link PanelView} if necessary. */
+    public void updateVisibility() {
+        mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
+    }
+
+    /** Returns true if {@link PanelView} should be visible. */
+    abstract boolean shouldPanelBeVisible();
+
+    /**
+     * Updates the panel expansion and {@link PanelView} visibility if necessary.
+     *
+     * TODO(b/200063118): Could public calls to this method be replaced with calls to
+     *   {@link #updateVisibility()}? That would allow us to make this method private.
+     */
+    public void updatePanelExpansionAndVisibility() {
         if (mBar != null) {
-            mBar.panelExpansionChanged(
-                    mExpandedFraction,
-                    mExpandedFraction > 0f || mInstantExpanding
-                            || isPanelVisibleBecauseOfHeadsUp() || mTracking
-                            || mHeightAnimator != null && !mIsSpringBackAnimation);
+            mBar.panelExpansionChanged(mExpandedFraction, isExpanded());
         }
+        updateVisibility();
         for (int i = 0; i < mExpansionListeners.size(); i++) {
             mExpansionListeners.get(i).onPanelExpansionChanged(mExpandedFraction, mTracking);
         }
     }
 
+    public boolean isExpanded() {
+        return mExpandedFraction > 0f
+                || mInstantExpanding
+                || isPanelVisibleBecauseOfHeadsUp()
+                || mTracking
+                || mHeightAnimator != null
+                && !mIsSpringBackAnimation;
+    }
+
     public void addExpansionListener(PanelExpansionListener panelExpansionListener) {
         mExpansionListeners.add(panelExpansionListener);
     }
@@ -1130,28 +1162,28 @@
         return mView;
     }
 
-    public boolean isEnabled() {
-        return mView.isEnabled();
-    }
-
     public OnLayoutChangeListener createLayoutChangeListener() {
         return new OnLayoutChangeListener();
     }
 
-    protected TouchHandler createTouchHandler() {
-        return new TouchHandler();
-    }
+    protected abstract TouchHandler createTouchHandler();
 
     protected OnConfigurationChangedListener createOnConfigurationChangedListener() {
         return new OnConfigurationChangedListener();
     }
 
-    /**
-     * Set that the panel is currently opening and not fully opened or closed.
-     */
-    public abstract void setIsShadeOpening(boolean opening);
+    public abstract class TouchHandler implements View.OnTouchListener {
+        /**
+         * Method called when a touch has occurred on {@link PhoneStatusBarView}.
+         *
+         * Touches that occur on the status bar view may have ramifications for the notification
+         * panel (e.g. a touch that pulls down the shade could start on the status bar), so we need
+         * to notify the panel controller when these touches occur.
+         *
+         * Returns true if the event was handled and false otherwise.
+         */
+        public abstract boolean onTouchForwardedFromStatusBar(MotionEvent event);
 
-    public class TouchHandler implements View.OnTouchListener {
         public boolean onInterceptTouchEvent(MotionEvent event) {
             if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
                     && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
@@ -1405,8 +1437,7 @@
 
     private void beginJankMonitoring(int cuj) {
         InteractionJankMonitor.Configuration.Builder builder =
-                new InteractionJankMonitor.Configuration.Builder(cuj)
-                        .setView(mView)
+                InteractionJankMonitor.Configuration.Builder.withView(cuj, mView)
                         .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
         InteractionJankMonitor.getInstance().begin(builder);
     }
@@ -1418,4 +1449,8 @@
     private void cancelJankMonitoring(int cuj) {
         InteractionJankMonitor.getInstance().cancel(cuj);
     }
+
+    protected float getExpansionFraction() {
+        return mExpandedFraction;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 2ca36b6..06a31c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -18,15 +18,13 @@
 
 import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection;
 
-import static java.lang.Float.isNaN;
-
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.EventLog;
+import android.util.Log;
 import android.util.Pair;
 import android.view.DisplayCutout;
 import android.view.Gravity;
@@ -38,11 +36,9 @@
 import android.widget.LinearLayout;
 
 import com.android.systemui.Dependency;
-import com.android.systemui.EventLogTags;
 import com.android.systemui.R;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.util.leak.RotationUtils;
 
 import java.util.List;
@@ -52,22 +48,11 @@
     private static final String TAG = "PhoneStatusBarView";
     private static final boolean DEBUG = StatusBar.DEBUG;
     private static final boolean DEBUG_GESTURES = false;
-    private final CommandQueue mCommandQueue;
     private final StatusBarContentInsetsProvider mContentInsetsProvider;
 
     StatusBar mBar;
 
-    boolean mIsFullyOpenedPanel = false;
     private ScrimController mScrimController;
-    private float mMinFraction;
-    private Runnable mHideExpandedRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mPanelFraction == 0.0f) {
-                mBar.makeExpandedInvisible();
-            }
-        }
-    };
     private DarkReceiver mBattery;
     private DarkReceiver mClock;
     private int mRotationOrientation = -1;
@@ -80,16 +65,16 @@
     private int mStatusBarHeight;
     @Nullable
     private List<StatusBar.ExpansionChangedListener> mExpansionChangedListeners;
+    @Nullable
+    private TouchEventHandler mTouchEventHandler;
 
     /**
      * Draw this many pixels into the left/right side of the cutout to optimally use the space
      */
     private int mCutoutSideNudge = 0;
-    private boolean mHeadsUpVisible;
 
     public PhoneStatusBarView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mCommandQueue = Dependency.get(CommandQueue.class);
         mContentInsetsProvider = Dependency.get(StatusBarContentInsetsProvider.class);
     }
 
@@ -102,6 +87,10 @@
         mExpansionChangedListeners = listeners;
     }
 
+    void setTouchEventHandler(TouchEventHandler handler) {
+        mTouchEventHandler = handler;
+    }
+
     public void setScrimController(ScrimController scrimController) {
         mScrimController = scrimController;
     }
@@ -176,11 +165,6 @@
     }
 
     @Override
-    public boolean panelEnabled() {
-        return mCommandQueue.panelsEnabled();
-    }
-
-    @Override
     public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
         if (super.onRequestSendAccessibilityEventInternal(child, event)) {
             // The status bar is very small so augment the view that the user is touching
@@ -196,98 +180,31 @@
     }
 
     @Override
-    public void onPanelPeeked() {
-        super.onPanelPeeked();
-        mBar.makeExpandedVisible(false);
-    }
-
-    @Override
-    public void onPanelCollapsed() {
-        super.onPanelCollapsed();
-        // Close the status bar in the next frame so we can show the end of the animation.
-        post(mHideExpandedRunnable);
-        mIsFullyOpenedPanel = false;
-    }
-
-    public void removePendingHideExpandedRunnables() {
-        removeCallbacks(mHideExpandedRunnable);
-    }
-
-    @Override
-    public void onPanelFullyOpened() {
-        super.onPanelFullyOpened();
-        if (!mIsFullyOpenedPanel) {
-            mPanel.getView().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
-        }
-        mIsFullyOpenedPanel = true;
-    }
-
-    @Override
     public boolean onTouchEvent(MotionEvent event) {
-        boolean barConsumedEvent = mBar.interceptTouchEvent(event);
-
-        if (DEBUG_GESTURES) {
-            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
-                EventLog.writeEvent(EventLogTags.SYSUI_PANELBAR_TOUCH,
-                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),
-                        barConsumedEvent ? 1 : 0);
-            }
+        mBar.onTouchEvent(event);
+        if (mTouchEventHandler == null) {
+            Log.w(
+                    TAG,
+                    String.format(
+                            "onTouch: No touch handler provided; eating gesture at (%d,%d)",
+                            (int) event.getX(),
+                            (int) event.getY()
+                    )
+            );
+            return true;
         }
-
-        return barConsumedEvent || super.onTouchEvent(event);
-    }
-
-    @Override
-    public void onTrackingStarted() {
-        super.onTrackingStarted();
-        mBar.onTrackingStarted();
-        mScrimController.onTrackingStarted();
-        removePendingHideExpandedRunnables();
-    }
-
-    @Override
-    public void onClosingFinished() {
-        super.onClosingFinished();
-        mBar.onClosingFinished();
-    }
-
-    @Override
-    public void onTrackingStopped(boolean expand) {
-        super.onTrackingStopped(expand);
-        mBar.onTrackingStopped(expand);
-    }
-
-    @Override
-    public void onExpandingFinished() {
-        super.onExpandingFinished();
-        mScrimController.onExpandingFinished();
+        return mTouchEventHandler.handleTouchEvent(event);
     }
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
-        return mBar.interceptTouchEvent(event) || super.onInterceptTouchEvent(event);
-    }
-
-    @Override
-    public void onPanelMinFractionChanged(float minFraction) {
-        if (isNaN(minFraction)) {
-            throw new IllegalArgumentException("minFraction cannot be NaN");
-        }
-        super.onPanelMinFractionChanged(minFraction);
-        if (mMinFraction != minFraction) {
-            mMinFraction = minFraction;
-            updateScrimFraction();
-        }
+        mBar.onTouchEvent(event);
+        return super.onInterceptTouchEvent(event);
     }
 
     @Override
     public void panelExpansionChanged(float frac, boolean expanded) {
         super.panelExpansionChanged(frac, expanded);
-        updateScrimFraction();
-        if ((frac == 0 || frac == 1) && mBar.getNavigationBarView() != null) {
-            mBar.getNavigationBarView().onStatusBarPanelStateChanged();
-        }
-
         if (mExpansionChangedListeners != null) {
             for (StatusBar.ExpansionChangedListener listener : mExpansionChangedListeners) {
                 listener.onExpansionChanged(frac, expanded);
@@ -295,15 +212,6 @@
         }
     }
 
-    private void updateScrimFraction() {
-        float scrimFraction = mPanelFraction;
-        if (mMinFraction < 1.0f) {
-            scrimFraction = Math.max((mPanelFraction - mMinFraction) / (1.0f - mMinFraction),
-                    0);
-        }
-        mScrimController.setPanelExpansion(scrimFraction);
-    }
-
     public void updateResources() {
         mCutoutSideNudge = getResources().getDimensionPixelSize(
                 R.dimen.display_cutout_margin_consumption);
@@ -383,13 +291,14 @@
                 getPaddingBottom());
     }
 
-    public void setHeadsUpVisible(boolean headsUpVisible) {
-        mHeadsUpVisible = headsUpVisible;
-        updateVisibility();
-    }
-
-    @Override
-    protected boolean shouldPanelBeVisible() {
-        return mHeadsUpVisible || super.shouldPanelBeVisible();
+    /**
+     * A handler repsonsible for all touch event handling on the status bar.
+     *
+     * The handler will be notified each time {@link this#onTouchEvent} is called, and the return
+     * value from the handler will be returned from {@link this#onTouchEvent}.
+     **/
+    public interface TouchEventHandler {
+        /** Called each time {@link this#onTouchEvent} is called. */
+        boolean handleTouchEvent(MotionEvent event);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
new file mode 100644
index 0000000..e27f458
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.phone
+
+import android.graphics.Point
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.R
+import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.ViewController
+
+/** Controller for [PhoneStatusBarView].  */
+class PhoneStatusBarViewController(
+    view: PhoneStatusBarView,
+    statusBarMoveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
+    touchEventHandler: PhoneStatusBarView.TouchEventHandler,
+) : ViewController<PhoneStatusBarView>(view) {
+
+    override fun onViewAttached() {}
+    override fun onViewDetached() {}
+
+    init {
+        mView.setTouchEventHandler(touchEventHandler)
+
+        statusBarMoveFromCenterAnimationController?.let { animationController ->
+            val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_left_side)
+            val systemIconArea: ViewGroup = mView.findViewById(R.id.system_icon_area)
+
+            val viewCenterProvider = StatusBarViewsCenterProvider()
+            val viewsToAnimate = arrayOf(
+                statusBarLeftSide,
+                systemIconArea
+            )
+
+            animationController.init(viewsToAnimate, viewCenterProvider)
+
+            mView.addOnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ ->
+                val widthChanged = right - left != oldRight - oldLeft
+                if (widthChanged) {
+                    statusBarMoveFromCenterAnimationController.onStatusBarWidthChanged()
+                }
+            }
+        }
+    }
+
+    fun setImportantForAccessibility(mode: Int) {
+        mView.importantForAccessibility = mode
+    }
+
+    private class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider {
+        override fun getViewCenter(view: View, outPoint: Point) =
+            when (view.id) {
+                R.id.status_bar_left_side -> {
+                    // items aligned to the start, return start center point
+                    getViewEdgeCenter(view, outPoint, isStart = true)
+                }
+                R.id.system_icon_area -> {
+                    // items aligned to the end, return end center point
+                    getViewEdgeCenter(view, outPoint, isStart = false)
+                }
+                else -> super.getViewCenter(view, outPoint)
+            }
+
+        /**
+         * Returns start or end (based on [isStart]) center point of the view
+         */
+        private fun getViewEdgeCenter(view: View, outPoint: Point, isStart: Boolean) {
+            val isRtl = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
+            val isLeftEdge = isRtl xor isStart
+
+            val viewLocation = IntArray(2)
+            view.getLocationOnScreen(viewLocation)
+
+            val viewX = viewLocation[0]
+            val viewY = viewLocation[1]
+
+            outPoint.x = viewX + if (isLeftEdge) view.height / 2 else view.width - view.height / 2
+            outPoint.y = viewY + view.height / 2
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 27c129a..1921357 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -34,6 +34,7 @@
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -46,7 +47,7 @@
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
+import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
@@ -150,12 +151,6 @@
     public static final float BUSY_SCRIM_ALPHA = 1f;
 
     /**
-     * The default scrim under the expanded bubble stack.
-     * This should not be lower than 0.54, otherwise we won't pass GAR.
-     */
-    public static final float BUBBLE_SCRIM_ALPHA = 0.6f;
-
-    /**
      * Scrim opacity that can have text on top.
      */
     public static final float GAR_SCRIM_ALPHA = 0.6f;
@@ -170,8 +165,6 @@
     private ScrimView mScrimInFront;
     private ScrimView mNotificationsScrim;
     private ScrimView mScrimBehind;
-    @Nullable
-    private ScrimView mScrimForBubble;
 
     private Runnable mScrimBehindChangeRunnable;
 
@@ -191,8 +184,10 @@
     private float mScrimBehindAlphaKeyguard = KEYGUARD_SCRIM_ALPHA;
     private final float mDefaultScrimAlpha;
 
-    // Assuming the shade is expanded during initialization
-    private float mPanelExpansion = 1f;
+    private float mRawPanelExpansionFraction;
+    private float mPanelScrimMinFraction;
+    // Calculated based on mRawPanelExpansionFraction and mPanelScrimMinFraction
+    private float mPanelExpansionFraction = 1f; // Assume shade is expanded during initialization
     private float mQsExpansion;
     private boolean mQsBottomVisible;
 
@@ -209,12 +204,10 @@
     private float mInFrontAlpha = NOT_INITIALIZED;
     private float mBehindAlpha = NOT_INITIALIZED;
     private float mNotificationsAlpha = NOT_INITIALIZED;
-    private float mBubbleAlpha = NOT_INITIALIZED;
 
     private int mInFrontTint;
     private int mBehindTint;
     private int mNotificationsTint;
-    private int mBubbleTint;
 
     private boolean mWallpaperVisibilityTimedOut;
     private int mScrimsVisibility;
@@ -243,7 +236,6 @@
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         mScrimStateListener = lightBarController::setScrimState;
         mDefaultScrimAlpha = BUSY_SCRIM_ALPHA;
-        ScrimState.BUBBLE_EXPANDED.setBubbleAlpha(BUBBLE_SCRIM_ALPHA);
 
         mKeyguardStateController = keyguardStateController;
         mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
@@ -273,11 +265,6 @@
             }
 
             @Override
-            public void onOverlayChanged() {
-                ScrimController.this.onThemeChanged();
-            }
-
-            @Override
             public void onUiModeChanged() {
                 ScrimController.this.onThemeChanged();
             }
@@ -290,11 +277,10 @@
      * Attach the controller to the supplied views.
      */
     public void attachViews(ScrimView behindScrim, ScrimView notificationsScrim,
-                            ScrimView scrimInFront, @Nullable ScrimView scrimForBubble) {
+                            ScrimView scrimInFront) {
         mNotificationsScrim = notificationsScrim;
         mScrimBehind = behindScrim;
         mScrimInFront = scrimInFront;
-        mScrimForBubble = scrimForBubble;
         updateThemeColors();
 
         behindScrim.enableBottomEdgeConcave(mClipsQsScrim);
@@ -307,8 +293,7 @@
 
         final ScrimState[] states = ScrimState.values();
         for (int i = 0; i < states.length; i++) {
-            states[i].init(mScrimInFront, mScrimBehind, mScrimForBubble, mDozeParameters,
-                    mDockManager);
+            states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager);
             states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard);
             states[i].setDefaultScrimAlpha(mDefaultScrimAlpha);
         }
@@ -316,9 +301,6 @@
         mScrimBehind.setDefaultFocusHighlightEnabled(false);
         mNotificationsScrim.setDefaultFocusHighlightEnabled(false);
         mScrimInFront.setDefaultFocusHighlightEnabled(false);
-        if (mScrimForBubble != null) {
-            mScrimForBubble.setDefaultFocusHighlightEnabled(false);
-        }
         updateScrims();
         mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback);
     }
@@ -373,21 +355,7 @@
         mAnimateChange = state.getAnimateChange();
         mAnimationDuration = state.getAnimationDuration();
 
-        mInFrontTint = state.getFrontTint();
-        mBehindTint = state.getBehindTint();
-        mNotificationsTint = state.getNotifTint();
-        mBubbleTint = state.getBubbleTint();
-
-        mInFrontAlpha = state.getFrontAlpha();
-        mBehindAlpha = state.getBehindAlpha();
-        mBubbleAlpha = state.getBubbleAlpha();
-        mNotificationsAlpha = state.getNotifAlpha();
-        if (isNaN(mBehindAlpha) || isNaN(mInFrontAlpha) || isNaN(mNotificationsAlpha)) {
-            throw new IllegalStateException("Scrim opacity is NaN for state: " + state + ", front: "
-                    + mInFrontAlpha + ", back: " + mBehindAlpha + ", notif: "
-                    + mNotificationsAlpha);
-        }
-        applyStateToAlpha();
+        applyState();
 
         // Scrim might acquire focus when user is navigating with a D-pad or a keyboard.
         // We need to disable focus otherwise AOD would end up with a gray overlay.
@@ -513,20 +481,44 @@
      *
      * The expansion fraction is tied to the scrim opacity.
      *
-     * @param fraction From 0 to 1 where 0 means collapsed and 1 expanded.
+     * See {@link PanelBar#panelExpansionChanged}.
+     *
+     * @param rawPanelExpansionFraction From 0 to 1 where 0 means collapsed and 1 expanded.
      */
-    public void setPanelExpansion(float fraction) {
-        if (isNaN(fraction)) {
-            throw new IllegalArgumentException("Fraction should not be NaN");
+    public void setRawPanelExpansionFraction(
+            @FloatRange(from = 0.0, to = 1.0) float rawPanelExpansionFraction) {
+        if (isNaN(rawPanelExpansionFraction)) {
+            throw new IllegalArgumentException("rawPanelExpansionFraction should not be NaN");
         }
-        if (mPanelExpansion != fraction) {
-            mPanelExpansion = fraction;
+        mRawPanelExpansionFraction = rawPanelExpansionFraction;
+        calculateAndUpdatePanelExpansion();
+    }
+
+    /** See {@link NotificationPanelViewController#setPanelScrimMinFraction(float)}. */
+    public void setPanelScrimMinFraction(float minFraction) {
+        if (isNaN(minFraction)) {
+            throw new IllegalArgumentException("minFraction should not be NaN");
+        }
+        mPanelScrimMinFraction = minFraction;
+        calculateAndUpdatePanelExpansion();
+    }
+
+    private void calculateAndUpdatePanelExpansion() {
+        float panelExpansionFraction = mRawPanelExpansionFraction;
+        if (mPanelScrimMinFraction < 1.0f) {
+            panelExpansionFraction = Math.max(
+                    (mRawPanelExpansionFraction - mPanelScrimMinFraction)
+                            / (1.0f - mPanelScrimMinFraction),
+                    0);
+        }
+
+        if (mPanelExpansionFraction != panelExpansionFraction) {
+            mPanelExpansionFraction = panelExpansionFraction;
 
             boolean relevantState = (mState == ScrimState.UNLOCKED
                     || mState == ScrimState.KEYGUARD
                     || mState == ScrimState.SHADE_LOCKED
-                    || mState == ScrimState.PULSING
-                    || mState == ScrimState.BUBBLE_EXPANDED);
+                    || mState == ScrimState.PULSING);
             if (!(relevantState && mExpansionAffectsAlpha)) {
                 return;
             }
@@ -587,16 +579,14 @@
         if (isNaN(expansionFraction)) {
             return;
         }
-        expansionFraction = Interpolators
-                .getNotificationScrimAlpha(expansionFraction, false /* notification */);
+        expansionFraction = ShadeInterpolation.getNotificationScrimAlpha(expansionFraction);
         boolean qsBottomVisible = qsPanelBottomY > 0;
         if (mQsExpansion != expansionFraction || mQsBottomVisible != qsBottomVisible) {
             mQsExpansion = expansionFraction;
             mQsBottomVisible = qsBottomVisible;
             boolean relevantState = (mState == ScrimState.SHADE_LOCKED
                     || mState == ScrimState.KEYGUARD
-                    || mState == ScrimState.PULSING
-                    || mState == ScrimState.BUBBLE_EXPANDED);
+                    || mState == ScrimState.PULSING);
             if (!(relevantState && mExpansionAffectsAlpha)) {
                 return;
             }
@@ -654,12 +644,22 @@
         }
     }
 
-    private void applyStateToAlpha() {
+    private void applyState() {
+        mInFrontTint = mState.getFrontTint();
+        mBehindTint = mState.getBehindTint();
+        mNotificationsTint = mState.getNotifTint();
+
+        mInFrontAlpha = mState.getFrontAlpha();
+        mBehindAlpha = mState.getBehindAlpha();
+        mNotificationsAlpha = mState.getNotifAlpha();
+
+        assertAlphasValid();
+
         if (!mExpansionAffectsAlpha) {
             return;
         }
 
-        if (mState == ScrimState.UNLOCKED || mState == ScrimState.BUBBLE_EXPANDED) {
+        if (mState == ScrimState.UNLOCKED) {
             // Darken scrim as you pull down the shade when unlocked, unless the shade is expanding
             // because we're doing the screen off animation.
             if (!mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()) {
@@ -674,6 +674,12 @@
                 }
                 mInFrontAlpha = 0;
             }
+        } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) {
+            float behindFraction = getInterpolatedFraction();
+            behindFraction = (float) Math.pow(behindFraction, 0.8f);
+
+            mBehindAlpha = behindFraction * mDefaultScrimAlpha;
+            mNotificationsAlpha = mBehindAlpha;
         } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED
                 || mState == ScrimState.PULSING) {
             Pair<Integer, Float> result = calculateBackStateForState(mState);
@@ -711,12 +717,24 @@
                 mNotificationsTint = mState.getNotifTint();
                 mBehindTint = behindTint;
             }
+
+            // At the end of a launch animation over the lockscreen, the state is either KEYGUARD or
+            // SHADE_LOCKED and this code is called. We have to set the notification alpha to 0
+            // otherwise there is a flicker to its previous value.
+            if (mKeyguardOccluded) {
+                mNotificationsAlpha = 0;
+            }
             if (mUnOcclusionAnimationRunning && mState == ScrimState.KEYGUARD) {
                 // We're unoccluding the keyguard and don't want to have a bright flash.
                 mNotificationsAlpha = KEYGUARD_SCRIM_ALPHA;
                 mNotificationsTint = ScrimState.KEYGUARD.getNotifTint();
             }
         }
+
+        assertAlphasValid();
+    }
+
+    private void assertAlphasValid() {
         if (isNaN(mBehindAlpha) || isNaN(mInFrontAlpha) || isNaN(mNotificationsAlpha)) {
             throw new IllegalStateException("Scrim opacity is NaN for state: " + mState
                     + ", front: " + mInFrontAlpha + ", back: " + mBehindAlpha + ", notif: "
@@ -756,14 +774,13 @@
 
 
     private void applyAndDispatchState() {
-        applyStateToAlpha();
+        applyState();
         if (mUpdatePending) {
             return;
         }
         setOrAdaptCurrentAnimation(mScrimBehind);
         setOrAdaptCurrentAnimation(mNotificationsScrim);
         setOrAdaptCurrentAnimation(mScrimInFront);
-        setOrAdaptCurrentAnimation(mScrimForBubble);
         dispatchBackScrimState(mScrimBehind.getViewAlpha());
 
         // Reset wallpaper timeout if it's already timeout like expanding panel while PULSING
@@ -871,11 +888,6 @@
         setScrimAlpha(mScrimBehind, mBehindAlpha);
         setScrimAlpha(mNotificationsScrim, mNotificationsAlpha);
 
-        if (mScrimForBubble != null) {
-            boolean animateScrimForBubble = mScrimForBubble.getViewAlpha() != 0 && !mBlankScreen;
-            mScrimForBubble.setColors(mColors, animateScrimForBubble);
-            setScrimAlpha(mScrimForBubble, mBubbleAlpha);
-        }
         // The animation could have all already finished, let's call onFinished just in case
         onFinished(mState);
         dispatchScrimsVisible();
@@ -908,7 +920,7 @@
     }
 
     private float getInterpolatedFraction() {
-        return Interpolators.getNotificationScrimAlpha(mPanelExpansion, false /* notification */);
+        return ShadeInterpolation.getNotificationScrimAlpha(mPanelExpansionFraction);
     }
 
     private void setScrimAlpha(ScrimView scrim, float alpha) {
@@ -928,8 +940,6 @@
             return "behind_scrim";
         } else if (scrim == mNotificationsScrim) {
             return "notifications_scrim";
-        } else if (scrim == mScrimForBubble) {
-            return "bubble_scrim";
         }
         return "unknown_scrim";
     }
@@ -1002,8 +1012,6 @@
             return mBehindAlpha;
         } else if (scrim == mNotificationsScrim) {
             return mNotificationsAlpha;
-        } else if (scrim == mScrimForBubble) {
-            return mBubbleAlpha;
         } else {
             throw new IllegalArgumentException("Unknown scrim view");
         }
@@ -1016,8 +1024,6 @@
             return mBehindTint;
         } else if (scrim == mNotificationsScrim) {
             return mNotificationsTint;
-        } else if (scrim == mScrimForBubble) {
-            return mBubbleTint;
         } else {
             throw new IllegalArgumentException("Unknown scrim view");
         }
@@ -1049,8 +1055,7 @@
         }
         if (isAnimating(mScrimBehind)
                 || isAnimating(mNotificationsScrim)
-                || isAnimating(mScrimInFront)
-                || isAnimating(mScrimForBubble)) {
+                || isAnimating(mScrimInFront)) {
             if (callback != null && callback != mCallback) {
                 // Since we only notify the callback that we're finished once everything has
                 // finished, we need to make sure that any changing callbacks are also invoked
@@ -1077,13 +1082,9 @@
             mInFrontTint = Color.TRANSPARENT;
             mBehindTint = mState.getBehindTint();
             mNotificationsTint = mState.getNotifTint();
-            mBubbleTint = Color.TRANSPARENT;
             updateScrimColor(mScrimInFront, mInFrontAlpha, mInFrontTint);
             updateScrimColor(mScrimBehind, mBehindAlpha, mBehindTint);
             updateScrimColor(mNotificationsScrim, mNotificationsAlpha, mNotificationsTint);
-            if (mScrimForBubble != null) {
-                updateScrimColor(mScrimForBubble, mBubbleAlpha, mBubbleTint);
-            }
         }
     }
 
@@ -1225,6 +1226,7 @@
         pw.println(" ScrimController: ");
         pw.print("  state: ");
         pw.println(mState);
+        pw.println("    mClipQsScrim = " + mState.mClipQsScrim);
 
         pw.print("  frontScrim:");
         pw.print(" viewAlpha=");
@@ -1250,20 +1252,12 @@
         pw.print(" tint=0x");
         pw.println(Integer.toHexString(mNotificationsScrim.getTint()));
 
-        pw.print("  bubbleScrim:");
-        pw.print(" viewAlpha=");
-        pw.print(mScrimForBubble.getViewAlpha());
-        pw.print(" alpha=");
-        pw.print(mBubbleAlpha);
-        pw.print(" tint=0x");
-        pw.println(Integer.toHexString(mScrimForBubble.getTint()));
-
         pw.print("  mTracking=");
         pw.println(mTracking);
         pw.print("  mDefaultScrimAlpha=");
         pw.println(mDefaultScrimAlpha);
-        pw.print("  mExpansionFraction=");
-        pw.println(mPanelExpansion);
+        pw.print("  mPanelExpansionFraction=");
+        pw.println(mPanelExpansionFraction);
         pw.print("  mExpansionAffectsAlpha=");
         pw.println(mExpansionAffectsAlpha);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 15b8c67..9246c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -19,8 +19,6 @@
 import android.graphics.Color;
 import android.os.Trace;
 
-import androidx.annotation.Nullable;
-
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
@@ -43,11 +41,9 @@
         public void prepare(ScrimState previousState) {
             mFrontTint = Color.BLACK;
             mBehindTint = Color.BLACK;
-            mBubbleTint = previousState.mBubbleTint;
 
             mFrontAlpha = 1f;
             mBehindAlpha = 1f;
-            mBubbleAlpha = previousState.mBubbleAlpha;
 
             mAnimationDuration = ScrimController.ANIMATION_DURATION_LONG;
         }
@@ -81,18 +77,27 @@
             mFrontTint = Color.BLACK;
             mBehindTint = Color.BLACK;
             mNotifTint = mClipQsScrim ? Color.BLACK : Color.TRANSPARENT;
-            mBubbleTint = Color.TRANSPARENT;
 
             mFrontAlpha = 0;
             mBehindAlpha = mClipQsScrim ? 1 : mScrimBehindAlphaKeyguard;
             mNotifAlpha = mClipQsScrim ? mScrimBehindAlphaKeyguard : 0;
-            mBubbleAlpha = 0;
             if (mClipQsScrim) {
                 updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK);
             }
         }
     },
 
+    AUTH_SCRIMMED_SHADE {
+        @Override
+        public void prepare(ScrimState previousState) {
+            // notif & behind scrim alpha values are determined by ScrimController#applyState
+            // based on the shade expansion
+
+            mFrontTint = Color.BLACK;
+            mFrontAlpha = .66f;
+        }
+    },
+
     AUTH_SCRIMMED {
         @Override
         public void prepare(ScrimState previousState) {
@@ -118,7 +123,6 @@
             mNotifAlpha = mClipQsScrim ? mDefaultScrimAlpha : 0;
             mNotifTint = Color.TRANSPARENT;
             mFrontAlpha = 0f;
-            mBubbleAlpha = 0f;
         }
     },
 
@@ -129,7 +133,6 @@
         @Override
         public void prepare(ScrimState previousState) {
             mBehindAlpha = 0;
-            mBubbleAlpha = 0f;
             mFrontAlpha = mDefaultScrimAlpha;
         }
     },
@@ -139,7 +142,6 @@
         public void prepare(ScrimState previousState) {
             mBehindAlpha = mClipQsScrim ? 1 : mDefaultScrimAlpha;
             mNotifAlpha = 1f;
-            mBubbleAlpha = 0f;
             mFrontAlpha = 0f;
             mBehindTint = Color.BLACK;
 
@@ -163,7 +165,6 @@
         public void prepare(ScrimState previousState) {
             mBehindAlpha = 0;
             mFrontAlpha = 0;
-            mBubbleAlpha = 0;
         }
     },
 
@@ -185,9 +186,6 @@
             mBehindTint = Color.BLACK;
             mBehindAlpha = ScrimController.TRANSPARENT;
 
-            mBubbleTint = Color.TRANSPARENT;
-            mBubbleAlpha = ScrimController.TRANSPARENT;
-
             mAnimationDuration = ScrimController.ANIMATION_DURATION_LONG;
             // DisplayPowerManager may blank the screen for us, or we might blank it for ourselves
             // by animating the screen off via the LightRevelScrim. In either case we just need to
@@ -214,7 +212,6 @@
         @Override
         public void prepare(ScrimState previousState) {
             mFrontAlpha = mAodFrontScrimAlpha;
-            mBubbleAlpha = 0f;
             mBehindTint = Color.BLACK;
             mFrontTint = Color.BLACK;
             mBlankScreen = mDisplayRequiresBlanking;
@@ -238,7 +235,6 @@
             mBehindAlpha = mClipQsScrim ? 1 : 0;
             mNotifAlpha = 0;
             mFrontAlpha = 0;
-            mBubbleAlpha = 0;
 
             mAnimationDuration = mKeyguardFadingAway
                     ? mKeyguardFadingAwayDuration
@@ -249,21 +245,16 @@
 
             mFrontTint = Color.TRANSPARENT;
             mBehindTint = Color.BLACK;
-            mBubbleTint = Color.TRANSPARENT;
             mBlankScreen = false;
 
             if (previousState == ScrimState.AOD) {
                 // Set all scrims black, before they fade transparent.
                 updateScrimColor(mScrimInFront, 1f /* alpha */, Color.BLACK /* tint */);
                 updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK /* tint */);
-                if (mScrimForBubble != null) {
-                    updateScrimColor(mScrimForBubble, 1f /* alpha */, Color.BLACK /* tint */);
-                }
 
                 // Scrims should still be black at the end of the transition.
                 mFrontTint = Color.BLACK;
                 mBehindTint = Color.BLACK;
-                mBubbleTint = Color.BLACK;
                 mBlankScreen = true;
             }
 
@@ -271,71 +262,24 @@
                 updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK);
             }
         }
-    },
-
-    /**
-     * Unlocked with a bubble expanded.
-     */
-    BUBBLE_EXPANDED {
-        @Override
-        public void prepare(ScrimState previousState) {
-            mBehindAlpha = mClipQsScrim ? 1 : 0;
-            mNotifAlpha = 0;
-            mFrontAlpha = 0;
-
-            mAnimationDuration = mKeyguardFadingAway
-                    ? mKeyguardFadingAwayDuration
-                    : StatusBar.FADE_KEYGUARD_DURATION;
-
-            mAnimateChange = !mLaunchingAffordanceWithPreview;
-
-            mFrontTint = Color.TRANSPARENT;
-            mBehindTint = Color.BLACK;
-            mBubbleTint = Color.BLACK;
-            mBlankScreen = false;
-
-            if (previousState == ScrimState.AOD) {
-                // Set all scrims black, before they fade transparent.
-                updateScrimColor(mScrimInFront, 1f /* alpha */, Color.BLACK /* tint */);
-                updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK /* tint */);
-                if (mScrimForBubble != null) {
-                    updateScrimColor(mScrimForBubble, 1f /* alpha */, Color.BLACK /* tint */);
-                }
-
-                // Scrims should still be black at the end of the transition.
-                mFrontTint = Color.BLACK;
-                mBehindTint = Color.BLACK;
-                mBubbleTint = Color.BLACK;
-                mBlankScreen = true;
-            }
-
-            if (mClipQsScrim) {
-                updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK);
-            }
-
-            mAnimationDuration = ScrimController.ANIMATION_DURATION;
-        }
     };
 
     boolean mBlankScreen = false;
     long mAnimationDuration = ScrimController.ANIMATION_DURATION;
     int mFrontTint = Color.TRANSPARENT;
     int mBehindTint = Color.TRANSPARENT;
-    int mBubbleTint = Color.TRANSPARENT;
     int mNotifTint = Color.TRANSPARENT;
 
     boolean mAnimateChange = true;
     float mAodFrontScrimAlpha;
     float mFrontAlpha;
     float mBehindAlpha;
-    float mBubbleAlpha;
     float mNotifAlpha;
 
     float mScrimBehindAlphaKeyguard;
     float mDefaultScrimAlpha;
     ScrimView mScrimInFront;
     ScrimView mScrimBehind;
-    @Nullable ScrimView mScrimForBubble;
 
     DozeParameters mDozeParameters;
     DockManager mDockManager;
@@ -348,11 +292,10 @@
     long mKeyguardFadingAwayDuration;
     boolean mClipQsScrim;
 
-    public void init(ScrimView scrimInFront, ScrimView scrimBehind, ScrimView scrimForBubble,
-            DozeParameters dozeParameters, DockManager dockManager) {
+    public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters,
+            DockManager dockManager) {
         mScrimInFront = scrimInFront;
         mScrimBehind = scrimBehind;
-        mScrimForBubble = scrimForBubble;
 
         mDozeParameters = dozeParameters;
         mDockManager = dockManager;
@@ -379,10 +322,6 @@
         return mNotifAlpha;
     }
 
-    public float getBubbleAlpha() {
-        return mBubbleAlpha;
-    }
-
     public int getFrontTint() {
         return mFrontTint;
     }
@@ -395,10 +334,6 @@
         return mNotifTint;
     }
 
-    public int getBubbleTint() {
-        return mBubbleTint;
-    }
-
     public long getAnimationDuration() {
         return mAnimationDuration;
     }
@@ -436,10 +371,6 @@
         mDefaultScrimAlpha = defaultScrimAlpha;
     }
 
-    public void setBubbleAlpha(float alpha) {
-        mBubbleAlpha = alpha;
-    }
-
     public void setWallpaperSupportsAmbientMode(boolean wallpaperSupportsAmbientMode) {
         mWallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
index d4458e2..a54251a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
@@ -48,7 +48,7 @@
     protected final NotificationShadeWindowController mNotificationShadeWindowController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final int mDisplayId;
-    protected final Lazy<StatusBar> mStatusBarLazy;
+    protected final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     private final Lazy<AssistManager> mAssistManagerLazy;
     private final Optional<Bubbles> mBubblesOptional;
 
@@ -61,7 +61,7 @@
             NotificationShadeWindowController notificationShadeWindowController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             WindowManager windowManager,
-            Lazy<StatusBar> statusBarLazy,
+            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             Lazy<AssistManager> assistManagerLazy,
             Optional<Bubbles> bubblesOptional
     ) {
@@ -71,7 +71,7 @@
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mDisplayId = windowManager.getDefaultDisplay().getDisplayId();
         // TODO: Remove circular reference to StatusBar when possible.
-        mStatusBarLazy = statusBarLazy;
+        mStatusBarOptionalLazy = statusBarOptionalLazy;
         mAssistManagerLazy = assistManagerLazy;
         mBubblesOptional = bubblesOptional;
     }
@@ -118,10 +118,6 @@
                     + " flags=" + flags);
         }
 
-        if ((flags & CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL) == 0) {
-            getStatusBar().postHideRecentApps();
-        }
-
         // TODO(b/62444020): remove when this bug is fixed
         Log.v(TAG, "NotificationShadeWindow: " + getNotificationShadeWindowView()
                 + " canPanelBeCollapsed(): "
@@ -132,7 +128,8 @@
             mNotificationShadeWindowController.setNotificationShadeFocusable(false);
 
             getStatusBar().getNotificationShadeWindowViewController().cancelExpandHelper();
-            getStatusBarView().collapsePanel(true /* animate */, delayed, speedUpFactor);
+            getNotificationPanelViewController()
+                    .collapsePanel(true /* animate */, delayed, speedUpFactor);
         } else if (mBubblesOptional.isPresent()) {
             mBubblesOptional.get().collapseStack();
         }
@@ -210,7 +207,7 @@
     }
 
     private StatusBar getStatusBar() {
-        return mStatusBarLazy.get();
+        return mStatusBarOptionalLazy.get().get();
     }
 
     private NotificationPresenter getPresenter() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SplitShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SplitShadeHeaderController.kt
new file mode 100644
index 0000000..a7ecd06
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SplitShadeHeaderController.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.view.View
+import com.android.systemui.R
+import com.android.systemui.animation.ShadeInterpolation
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.qs.carrier.QSCarrierGroupController
+import com.android.systemui.statusbar.phone.dagger.StatusBarComponent.StatusBarScope
+import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.SPLIT_SHADE_HEADER
+import javax.inject.Inject
+import javax.inject.Named
+
+@StatusBarScope
+class SplitShadeHeaderController @Inject constructor(
+    @Named(SPLIT_SHADE_HEADER) private val statusBar: View,
+    private val statusBarIconController: StatusBarIconController,
+    qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder,
+    featureFlags: FeatureFlags,
+    batteryMeterViewController: BatteryMeterViewController
+) {
+
+    // TODO(b/194178072) Handle RSSI hiding when multi carrier
+    private val iconManager: StatusBarIconController.IconManager
+    private val qsCarrierGroupController: QSCarrierGroupController
+    private var visible = false
+
+    var shadeExpanded = false
+        set(value) {
+            field = value
+            updateVisibility()
+        }
+
+    var splitShadeMode = false
+        set(value) {
+            field = value
+            updateVisibility()
+        }
+
+    var shadeExpandedFraction = -1f
+        set(value) {
+            if (visible && field != value) {
+                statusBar.alpha = ShadeInterpolation.getContentAlpha(value)
+                field = value
+            }
+        }
+
+    init {
+        batteryMeterViewController.init()
+        val batteryIcon: BatteryMeterView = statusBar.findViewById(R.id.batteryRemainingIcon)
+
+        // battery settings same as in QS icons
+        batteryMeterViewController.ignoreTunerUpdates()
+        batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+
+        val iconContainer: StatusIconContainer = statusBar.findViewById(R.id.statusIcons)
+        iconManager = StatusBarIconController.IconManager(iconContainer, featureFlags)
+        qsCarrierGroupController = qsCarrierGroupControllerBuilder
+                .setQSCarrierGroup(statusBar.findViewById(R.id.carrier_group))
+                .build()
+    }
+
+    private fun updateVisibility() {
+        val shouldBeVisible = shadeExpanded && splitShadeMode
+        if (visible != shouldBeVisible) {
+            visible = shouldBeVisible
+            statusBar.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE
+            updateListeners(shouldBeVisible)
+        }
+    }
+
+    private fun updateListeners(visible: Boolean) {
+        qsCarrierGroupController.setListening(visible)
+        if (visible) {
+            statusBarIconController.addIconGroup(iconManager)
+        } else {
+            statusBarIconController.removeIconGroup(iconManager)
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 88434cc..df30e67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -18,7 +18,6 @@
 
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
-import static android.app.StatusBarManager.WindowType;
 import static android.app.StatusBarManager.WindowVisibleState;
 import static android.app.StatusBarManager.windowStateToString;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
@@ -27,21 +26,19 @@
 import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS;
 
+import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
 import static androidx.lifecycle.Lifecycle.State.RESUMED;
 
 import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
 import static com.android.systemui.charging.WirelessChargingLayout.UNKNOWN_BATTERY_LEVEL;
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
-import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
-import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
 import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
 import static com.android.wm.shell.bubbles.BubbleController.TASKBAR_CHANGED_BROADCAST;
 
@@ -56,6 +53,7 @@
 import android.app.PendingIntent;
 import android.app.StatusBarManager;
 import android.app.TaskInfo;
+import android.app.TaskStackBuilder;
 import android.app.UiModeManager;
 import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
@@ -71,17 +69,13 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.PointF;
-import android.media.AudioAttributes;
 import android.metrics.LogMaker;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -89,8 +83,6 @@
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
 import android.provider.Settings;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamManager;
@@ -105,7 +97,6 @@
 import android.view.Display;
 import android.view.IRemoteAnimationRunner;
 import android.view.IWindowManager;
-import android.view.InsetsState.InternalInsetsType;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationAdapter;
@@ -113,7 +104,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsetsController.Appearance;
-import android.view.WindowInsetsController.Behavior;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityManager;
@@ -133,7 +123,6 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.RegisterStatusBarResult;
-import com.android.internal.view.AppearanceRegion;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.ViewMediatorCallback;
@@ -146,24 +135,25 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.SystemUI;
-import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DelegateLaunchAnimatorController;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.battery.BatteryMeterViewController;
 import com.android.systemui.biometrics.AuthRippleController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.camera.CameraIntents;
 import com.android.systemui.charging.WirelessChargingAnimation;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
-import com.android.systemui.demomode.DemoMode;
-import com.android.systemui.demomode.DemoModeCommandReceiver;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.emergency.EmergencyGesture;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.fragments.ExtensionFragmentListener;
 import com.android.systemui.fragments.FragmentHostManager;
-import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.KeyguardService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -190,7 +180,6 @@
 import com.android.systemui.statusbar.BackDropView;
 import com.android.systemui.statusbar.CircleReveal;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.KeyboardShortcuts;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -205,19 +194,18 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
+import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.PowerButtonReveal;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.charging.WiredChargingRippleController;
+import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.interruption.BypassHeadsUpNotifier;
@@ -239,11 +227,15 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.statusbar.policy.ExtensionController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
-import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
+import com.android.systemui.unfold.UnfoldTransitionWallpaperController;
+import com.android.systemui.unfold.config.UnfoldTransitionConfig;
+import com.android.systemui.util.WallpaperController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.concurrency.MessageRouter;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.wmshell.BubblesManager;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -261,27 +253,18 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Named;
-import javax.inject.Provider;
 
 import dagger.Lazy;
 
-public class StatusBar extends SystemUI implements DemoMode,
-        ActivityStarter, KeyguardStateController.Callback,
-        OnHeadsUpChangedListener, CommandQueue.Callbacks,
-        ColorExtractor.OnColorsChangedListener, ConfigurationListener,
-        StatusBarStateController.StateListener,
-        LifecycleOwner, BatteryController.BatteryStateChangeCallback,
-        ActivityLaunchAnimator.Callback {
+/** */
+public class StatusBar extends SystemUI implements
+        ActivityStarter,
+        LifecycleOwner {
     public static final boolean MULTIUSER_DEBUG = false;
 
-    protected static final int MSG_HIDE_RECENT_APPS = 1020;
-    protected static final int MSG_PRELOAD_RECENT_APPS = 1022;
-    protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023;
-    protected static final int MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU = 1026;
     protected static final int MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU = 1027;
 
     // Should match the values in PhoneWindowManager
-    public static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
     public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
     static public final String SYSTEM_DIALOG_REASON_SCREENSHOT = "screenshot";
 
@@ -306,14 +289,12 @@
 
     public static final String ACTION_FAKE_ARTWORK = "fake_artwork";
 
-    private static final int MSG_OPEN_NOTIFICATION_PANEL = 1000;
-    private static final int MSG_CLOSE_PANELS = 1001;
     private static final int MSG_OPEN_SETTINGS_PANEL = 1002;
     private static final int MSG_LAUNCH_TRANSITION_TIMEOUT = 1003;
     // 1020-1040 reserved for BaseStatusBar
 
     // Time after we abort the launch transition.
-    private static final long LAUNCH_TRANSITION_TIMEOUT_MS = 5000;
+    static final long LAUNCH_TRANSITION_TIMEOUT_MS = 5000;
 
     protected static final boolean CLOSE_PANEL_WHEN_EMPTIED = true;
 
@@ -322,11 +303,6 @@
      */
     private static final int HINT_RESET_DELAY_MS = 1200;
 
-    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
-            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
-            .build();
-
     public static final int FADE_KEYGUARD_START_DELAY = 100;
     public static final int FADE_KEYGUARD_DURATION = 300;
     public static final int FADE_KEYGUARD_DURATION_PULSING = 96;
@@ -352,14 +328,115 @@
         try {
             IPackageManager packageManager =
                     IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
-            onlyCoreApps = packageManager.isOnlyCoreApps();
+            onlyCoreApps = packageManager != null && packageManager.isOnlyCoreApps();
         } catch (RemoteException e) {
             onlyCoreApps = false;
         }
         ONLY_CORE_APPS = onlyCoreApps;
     }
 
-    private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    private boolean mCallingFadingAwayAfterReveal;
+    private StatusBarCommandQueueCallbacks mCommandQueueCallbacks;
+
+    void setWindowState(int state) {
+        mStatusBarWindowState =  state;
+        mStatusBarWindowHidden = state == WINDOW_STATE_HIDDEN;
+    }
+
+    void acquireGestureWakeLock(long time) {
+        mGestureWakeLock.acquire(time);
+    }
+
+    boolean setAppearance(int appearance) {
+        if (mAppearance != appearance) {
+            mAppearance = appearance;
+            return updateBarMode(barMode(isTransientShown(), appearance));
+        }
+
+        return false;
+    }
+
+    int getBarMode() {
+        return mStatusBarMode;
+    }
+
+    boolean getWereIconsJustHidden() {
+        return mWereIconsJustHidden;
+    }
+
+    void setWereIconsJustHidden(boolean justHidden) {
+        mWereIconsJustHidden = justHidden;
+    }
+
+    void resendMessage(int msg) {
+        mMessageRouter.cancelMessages(msg);
+        mMessageRouter.sendMessage(msg);
+    }
+
+    void resendMessage(Object msg) {
+        mMessageRouter.cancelMessages(msg.getClass());
+        mMessageRouter.sendMessage(msg);
+    }
+
+    int getDisabled1() {
+        return mDisabled1;
+    }
+
+    void setDisabled1(int disabled) {
+        mDisabled1 = disabled;
+    }
+
+    int getDisabled2() {
+        return mDisabled2;
+    }
+
+    void setDisabled2(int disabled) {
+        mDisabled2 = disabled;
+    }
+
+    void setLastCameraLaunchSource(int source) {
+        mLastCameraLaunchSource = source;
+    }
+
+    void setLaunchCameraOnFinishedGoingToSleep(boolean launch) {
+        mLaunchCameraOnFinishedGoingToSleep = launch;
+    }
+
+    void setLaunchCameraOnFinishedWaking(boolean launch) {
+        mLaunchCameraWhenFinishedWaking = launch;
+    }
+
+    void setLaunchEmergencyActionOnFinishedGoingToSleep(boolean launch) {
+        mLaunchEmergencyActionOnFinishedGoingToSleep = launch;
+    }
+
+    void setLaunchEmergencyActionOnFinishedWaking(boolean launch) {
+        mLaunchEmergencyActionWhenFinishedWaking = launch;
+    }
+
+    void setTopHidesStatusBar(boolean hides) {
+        mTopHidesStatusBar = hides;
+    }
+
+    QSPanelController getQSPanelController() {
+        return mQSPanelController;
+    }
+
+    /** */
+    public void animateExpandNotificationsPanel() {
+        mCommandQueueCallbacks.animateExpandNotificationsPanel();
+    }
+
+    /** */
+    public void animateExpandSettingsPanel(@Nullable String subpanel) {
+        mCommandQueueCallbacks.animateExpandSettingsPanel(subpanel);
+    }
+
+    /** */
+    public void animateCollapsePanels(int flags, boolean force) {
+        mCommandQueueCallbacks.animateCollapsePanels(flags, force);
+    }
 
     public interface ExpansionChangedListener {
         void onExpansionChanged(float expansion, boolean expanded);
@@ -371,8 +448,7 @@
     protected int mState; // TODO: remove this. Just use StatusBarStateController
     protected boolean mBouncerShowing;
 
-    private PhoneStatusBarPolicy mIconPolicy;
-    private StatusBarSignalPolicy mSignalPolicy;
+    private final PhoneStatusBarPolicy mIconPolicy;
 
     private final VolumeComponent mVolumeComponent;
     private BrightnessMirrorController mBrightnessMirrorController;
@@ -380,17 +456,17 @@
     private BiometricUnlockController mBiometricUnlockController;
     private final LightBarController mLightBarController;
     private final Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
+    private final LockscreenGestureLogger mLockscreenGestureLogger;
     @Nullable
     protected LockscreenWallpaper mLockscreenWallpaper;
     private final AutoHideController mAutoHideController;
-    @Nullable
-    private final KeyguardLiftController mKeyguardLiftController;
+    private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger;
 
     private final Point mCurrentDisplaySize = new Point();
 
     protected NotificationShadeWindowView mNotificationShadeWindowView;
-    protected StatusBarWindowView mPhoneStatusBarWindow;
     protected PhoneStatusBarView mStatusBarView;
+    private PhoneStatusBarViewController mPhoneStatusBarViewController;
     private AuthRippleController mAuthRippleController;
     private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
     protected NotificationShadeWindowController mNotificationShadeWindowController;
@@ -401,7 +477,6 @@
     private boolean mWakeUpComingFromTouch;
     private PointF mWakeUpTouchLocation;
     private LightRevealScrim mLightRevealScrim;
-    private WiredChargingRippleController mChargingRippleAnimationController;
     private PowerButtonReveal mPowerButtonReveal;
 
     private final Object mQueueLock = new Object();
@@ -421,13 +496,13 @@
     protected NotificationShadeWindowViewController mNotificationShadeWindowViewController;
     private final DozeParameters mDozeParameters;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
-    private final Provider<StatusBarComponent.Builder> mStatusBarComponentBuilder;
+    private final StatusBarComponent.Factory mStatusBarComponentFactory;
     private final PluginManager mPluginManager;
     private final Optional<LegacySplitScreen> mSplitScreenOptional;
     private final StatusBarNotificationActivityStarter.Builder
             mStatusBarNotificationActivityStarterBuilder;
     private final ShadeController mShadeController;
-    private final SuperStatusBarViewFactory mSuperStatusBarViewFactory;
+    private final StatusBarWindowView mStatusBarWindowView;
     private final LightsOutNotifController mLightsOutNotifController;
     private final InitController mInitController;
 
@@ -435,9 +510,8 @@
     private final KeyguardDismissUtil mKeyguardDismissUtil;
     private final ExtensionController mExtensionController;
     private final UserInfoControllerImpl mUserInfoControllerImpl;
-    private final DismissCallbackRegistry mDismissCallbackRegistry;
     private final DemoModeController mDemoModeController;
-    private NotificationsController mNotificationsController;
+    private final NotificationsController mNotificationsController;
     private final OngoingCallController mOngoingCallController;
     private final SystemStatusAnimationScheduler mAnimationScheduler;
     private final StatusBarLocationPublisher mStatusBarLocationPublisher;
@@ -450,16 +524,16 @@
     // settings
     private QSPanelController mQSPanelController;
 
+    private final OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
     KeyguardIndicationController mKeyguardIndicationController;
 
-    private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
-
     private View mReportRejectedTouch;
 
     private boolean mExpandedVisible;
 
     private final int[] mAbsPos = new int[2];
 
+    protected final NotificationEntryManager mEntryManager;
     private final NotificationGutsManager mGutsManager;
     private final NotificationLogger mNotificationLogger;
     private final NotificationViewHierarchyManager mViewHierarchyManager;
@@ -467,16 +541,25 @@
     protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
     private final BrightnessSlider.Factory mBrightnessSliderFactory;
     private final FeatureFlags mFeatureFlags;
+    private final UnfoldTransitionConfig mUnfoldTransitionConfig;
+    private final Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimation;
+    private final Lazy<UnfoldTransitionWallpaperController> mUnfoldWallpaperController;
+    private final Lazy<StatusBarMoveFromCenterAnimationController> mMoveFromCenterAnimation;
+    private final WallpaperController mWallpaperController;
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
+    private final MessageRouter mMessageRouter;
+    private final WallpaperManager mWallpaperManager;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    private final TunerService mTunerService;
 
     private final List<ExpansionChangedListener> mExpansionChangedListeners;
 
-    // for disabling the status bar
+    // Flags for disabling the status bar
+    // Two variables becaseu the first one evidently ran out of room for new flags.
     private int mDisabled1 = 0;
     private int mDisabled2 = 0;
 
-    /** @see android.view.WindowInsetsController#setSystemBarsAppearance(int) */
+    /** @see android.view.WindowInsetsController#setSystemBarsAppearance(int, int) */
     private @Appearance int mAppearance;
 
     private boolean mTransientShown;
@@ -495,29 +578,6 @@
     // ensure quick settings is disabled until the current user makes it through the setup wizard
     @VisibleForTesting
     protected boolean mUserSetup = false;
-    private final DeviceProvisionedListener mUserSetupObserver = new DeviceProvisionedListener() {
-        @Override
-        public void onUserSetupChanged() {
-            final boolean userSetup = mDeviceProvisionedController.isUserSetup(
-                    mDeviceProvisionedController.getCurrentUser());
-            Log.d(TAG, "mUserSetupObserver - DeviceProvisionedListener called for user "
-                    + mDeviceProvisionedController.getCurrentUser());
-            if (MULTIUSER_DEBUG) {
-                Log.d(TAG, String.format("User setup changed: userSetup=%s mUserSetup=%s",
-                        userSetup, mUserSetup));
-            }
-
-            if (userSetup != mUserSetup) {
-                mUserSetup = userSetup;
-                if (!mUserSetup && mStatusBarView != null)
-                    animateCollapseQuickSettings();
-                if (mNotificationPanelViewController != null) {
-                    mNotificationPanelViewController.setUserSetupComplete(mUserSetup);
-                }
-                updateQsExpansionEnabled();
-            }
-        }
-    };
 
     @VisibleForTesting
     public enum StatusBarUiEvent implements UiEventLogger.UiEventEnum {
@@ -557,64 +617,32 @@
         }
     }
 
-    protected final H mHandler = createHandler();
+    private Handler mMainHandler;
+    private final DelayableExecutor mMainExecutor;
 
     private int mInteractingWindows;
     private @TransitionMode int mStatusBarMode;
 
-    private ViewMediatorCallback mKeyguardViewMediatorCallback;
+    private final ViewMediatorCallback mKeyguardViewMediatorCallback;
     private final ScrimController mScrimController;
     protected DozeScrimController mDozeScrimController;
     private final Executor mUiBgExecutor;
 
     protected boolean mDozing;
+    private boolean mIsFullscreen;
 
     private final NotificationMediaManager mMediaManager;
     private final NotificationLockscreenUserManager mLockscreenUserManager;
     private final NotificationRemoteInputManager mRemoteInputManager;
     private boolean mWallpaperSupported;
 
-    private final BroadcastReceiver mWallpaperChangedReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (!mWallpaperSupported) {
-                // Receiver should not have been registered at all...
-                Log.wtf(TAG, "WallpaperManager not supported");
-                return;
-            }
-            WallpaperManager wallpaperManager = context.getSystemService(WallpaperManager.class);
-            WallpaperInfo info = wallpaperManager.getWallpaperInfo(UserHandle.USER_CURRENT);
-            final boolean deviceSupportsAodWallpaper = mContext.getResources().getBoolean(
-                    com.android.internal.R.bool.config_dozeSupportsAodWallpaper);
-            // If WallpaperInfo is null, it must be ImageWallpaper.
-            final boolean supportsAmbientMode = deviceSupportsAodWallpaper
-                    && (info != null && info.supportsAmbientMode());
-
-            mNotificationShadeWindowController.setWallpaperSupportsAmbientMode(supportsAmbientMode);
-            mScrimController.setWallpaperSupportsAmbientMode(supportsAmbientMode);
-            mKeyguardViewMediator.setWallpaperSupportsAmbientMode(supportsAmbientMode);
-        }
-    };
-
-    BroadcastReceiver mTaskbarChangeReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (mBubblesOptional.isPresent()) {
-                mBubblesOptional.get().onTaskbarChanged(intent.getExtras());
-            }
-        }
-    };
-
     private Runnable mLaunchTransitionEndRunnable;
-    private NotificationEntry mDraggedDownEntry;
     private boolean mLaunchCameraWhenFinishedWaking;
     private boolean mLaunchCameraOnFinishedGoingToSleep;
     private boolean mLaunchEmergencyActionWhenFinishedWaking;
     private boolean mLaunchEmergencyActionOnFinishedGoingToSleep;
     private int mLastCameraLaunchSource;
     protected PowerManager.WakeLock mGestureWakeLock;
-    private Vibrator mVibrator;
-    private VibrationEffect mCameraLaunchGestureVibrationEffect;
 
     private final int[] mTmpInt2 = new int[2];
 
@@ -626,29 +654,7 @@
     private boolean mIsOccluded;
     private boolean mWereIconsJustHidden;
     private boolean mBouncerWasShowingWhenHidden;
-
-    // Notifies StatusBarKeyguardViewManager every time the keyguard transition is over,
-    // this animation is tied to the scrim for historic reasons.
-    // TODO: notify when keyguard has faded away instead of the scrim.
-    private final ScrimController.Callback mUnlockScrimCallback = new ScrimController
-            .Callback() {
-        @Override
-        public void onFinished() {
-            if (mStatusBarKeyguardViewManager == null) {
-                Log.w(TAG, "Tried to notify keyguard visibility when "
-                        + "mStatusBarKeyguardViewManager was null");
-                return;
-            }
-            if (mKeyguardStateController.isKeyguardFadingAway()) {
-                mStatusBarKeyguardViewManager.onKeyguardFadedAway();
-            }
-        }
-
-        @Override
-        public void onCancelled() {
-            onFinished();
-        }
-    };
+    private boolean mIsLaunchingActivityOverLockscreen;
 
     private final UserSwitcherController mUserSwitcherController;
     private final NetworkController mNetworkController;
@@ -667,51 +673,25 @@
     private boolean mNoAnimationOnNextBarModeChange;
     private final SysuiStatusBarStateController mStatusBarStateController;
 
-    private final KeyguardUpdateMonitorCallback mUpdateCallback =
-            new KeyguardUpdateMonitorCallback() {
-                @Override
-                public void onDreamingStateChanged(boolean dreaming) {
-                    if (dreaming) {
-                        maybeEscalateHeadsUp();
-                    }
-                }
-
-                // TODO: (b/145659174) remove when moving to NewNotifPipeline. Replaced by
-                //  KeyguardCoordinator
-                @Override
-                public void onStrongAuthStateChanged(int userId) {
-                    super.onStrongAuthStateChanged(userId);
-                    mNotificationsController.requestNotificationUpdate("onStrongAuthStateChanged");
-                }
-            };
-
-
-    private final FalsingManager.FalsingBeliefListener mFalsingBeliefListener =
-            new FalsingManager.FalsingBeliefListener() {
-                @Override
-                public void onFalse() {
-                    // Hides quick settings, bouncer, and quick-quick settings.
-                    mStatusBarKeyguardViewManager.reset(true);
-                }
-            };
-
-    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
-
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
-    private boolean mVibrateOnOpening;
-    private final VibratorHelper mVibratorHelper;
-    private ActivityLaunchAnimator mActivityLaunchAnimator;
+    private final ActivityLaunchAnimator mActivityLaunchAnimator;
+    private final DialogLaunchAnimator mDialogLaunchAnimator;
     private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
     protected StatusBarNotificationPresenter mPresenter;
     private NotificationActivityStarter mNotificationActivityStarter;
-    private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
+    private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
     private final Optional<BubblesManager> mBubblesManagerOptional;
     private final Optional<Bubbles> mBubblesOptional;
     private final Bubbles.BubbleExpandListener mBubbleExpandListener;
     private final Optional<StartingSurface> mStartingSurfaceOptional;
 
-    private ActivityIntentHelper mActivityIntentHelper;
+    private final ActivityIntentHelper mActivityIntentHelper;
     private NotificationStackScrollLayoutController mStackScrollerController;
+    private BatteryMeterViewController mBatteryMeterViewController;
+
+    private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener =
+            (extractor, which) -> updateTheme();
+
 
     /**
      * Public constructor for StatusBar.
@@ -726,7 +706,6 @@
             LightBarController lightBarController,
             AutoHideController autoHideController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            StatusBarSignalPolicy signalPolicy,
             PulseExpansionHandler pulseExpansionHandler,
             NotificationWakeUpCoordinator notificationWakeUpCoordinator,
             KeyguardBypassController keyguardBypassController,
@@ -737,7 +716,7 @@
             FalsingManager falsingManager,
             FalsingCollector falsingCollector,
             BroadcastDispatcher broadcastDispatcher,
-            RemoteInputQuickSettingsDisabler remoteInputQuickSettingsDisabler,
+            NotificationEntryManager notificationEntryManager,
             NotificationGutsManager notificationGutsManager,
             NotificationLogger notificationLogger,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
@@ -756,20 +735,18 @@
             ScreenLifecycle screenLifecycle,
             WakefulnessLifecycle wakefulnessLifecycle,
             SysuiStatusBarStateController statusBarStateController,
-            VibratorHelper vibratorHelper,
             Optional<BubblesManager> bubblesManagerOptional,
             Optional<Bubbles> bubblesOptional,
             VisualStabilityManager visualStabilityManager,
             DeviceProvisionedController deviceProvisionedController,
             NavigationBarController navigationBarController,
-            AccessibilityFloatingMenuController accessibilityFloatingMenuController,
             Lazy<AssistManager> assistManagerLazy,
             ConfigurationController configurationController,
             NotificationShadeWindowController notificationShadeWindowController,
             DozeParameters dozeParameters,
             ScrimController scrimController,
-            @Nullable KeyguardLiftController keyguardLiftController,
             Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
+            LockscreenGestureLogger lockscreenGestureLogger,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
             DozeServiceHost dozeServiceHost,
             PowerManager powerManager,
@@ -777,14 +754,15 @@
             DozeScrimController dozeScrimController,
             VolumeComponent volumeComponent,
             CommandQueue commandQueue,
-            Provider<StatusBarComponent.Builder> statusBarComponentBuilder,
+            CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger,
+            StatusBarComponent.Factory statusBarComponentFactory,
             PluginManager pluginManager,
             Optional<LegacySplitScreen> splitScreenOptional,
             LightsOutNotifController lightsOutNotifController,
             StatusBarNotificationActivityStarter.Builder
                     statusBarNotificationActivityStarterBuilder,
             ShadeController shadeController,
-            SuperStatusBarViewFactory superStatusBarViewFactory,
+            StatusBarWindowView statusBarWindowView,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             ViewMediatorCallback viewMediatorCallback,
             InitController initController,
@@ -793,15 +771,19 @@
             KeyguardDismissUtil keyguardDismissUtil,
             ExtensionController extensionController,
             UserInfoControllerImpl userInfoControllerImpl,
+            OperatorNameViewController.Factory operatorNameViewControllerFactory,
             PhoneStatusBarPolicy phoneStatusBarPolicy,
             KeyguardIndicationController keyguardIndicationController,
-            DismissCallbackRegistry dismissCallbackRegistry,
             DemoModeController demoModeController,
             Lazy<NotificationShadeDepthController> notificationShadeDepthControllerLazy,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
             NotificationIconAreaController notificationIconAreaController,
             BrightnessSlider.Factory brightnessSliderFactory,
-            WiredChargingRippleController chargingRippleAnimationController,
+            UnfoldTransitionConfig unfoldTransitionConfig,
+            Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
+            Lazy<UnfoldTransitionWallpaperController> unfoldTransitionWallpaperController,
+            Lazy<StatusBarMoveFromCenterAnimationController> statusBarUnfoldAnimationController,
+            WallpaperController wallpaperController,
             OngoingCallController ongoingCallController,
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
@@ -809,19 +791,27 @@
             LockscreenShadeTransitionController lockscreenShadeTransitionController,
             FeatureFlags featureFlags,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
+            @Main Handler mainHandler,
+            @Main DelayableExecutor delayableExecutor,
+            @Main MessageRouter messageRouter,
+            WallpaperManager wallpaperManager,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
-            Optional<StartingSurface> startingSurfaceOptional) {
+            Optional<StartingSurface> startingSurfaceOptional,
+            TunerService tunerService,
+            DumpManager dumpManager,
+            ActivityLaunchAnimator activityLaunchAnimator,
+            DialogLaunchAnimator dialogLaunchAnimator) {
         super(context);
         mNotificationsController = notificationsController;
         mLightBarController = lightBarController;
         mAutoHideController = autoHideController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mSignalPolicy = signalPolicy;
         mPulseExpansionHandler = pulseExpansionHandler;
         mWakeUpCoordinator = notificationWakeUpCoordinator;
         mKeyguardBypassController = keyguardBypassController;
         mKeyguardStateController = keyguardStateController;
         mHeadsUpManager = headsUpManagerPhone;
+        mOperatorNameViewControllerFactory = operatorNameViewControllerFactory;
         mKeyguardIndicationController = keyguardIndicationController;
         mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
         mDynamicPrivacyController = dynamicPrivacyController;
@@ -829,7 +819,7 @@
         mFalsingCollector = falsingCollector;
         mFalsingManager = falsingManager;
         mBroadcastDispatcher = broadcastDispatcher;
-        mRemoteInputQuickSettingsDisabler = remoteInputQuickSettingsDisabler;
+        mEntryManager = notificationEntryManager;
         mGutsManager = notificationGutsManager;
         mNotificationLogger = notificationLogger;
         mNotificationInterruptStateProvider = notificationInterruptStateProvider;
@@ -848,7 +838,6 @@
         mScreenLifecycle = screenLifecycle;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mStatusBarStateController = statusBarStateController;
-        mVibratorHelper = vibratorHelper;
         mBubblesManagerOptional = bubblesManagerOptional;
         mBubblesOptional = bubblesOptional;
         mVisualStabilityManager = visualStabilityManager;
@@ -861,20 +850,21 @@
         mPowerManager = powerManager;
         mDozeParameters = dozeParameters;
         mScrimController = scrimController;
-        mKeyguardLiftController = keyguardLiftController;
         mLockscreenWallpaperLazy = lockscreenWallpaperLazy;
+        mLockscreenGestureLogger = lockscreenGestureLogger;
         mScreenPinningRequest = screenPinningRequest;
         mDozeScrimController = dozeScrimController;
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
         mNotificationShadeDepthControllerLazy = notificationShadeDepthControllerLazy;
         mVolumeComponent = volumeComponent;
         mCommandQueue = commandQueue;
-        mStatusBarComponentBuilder = statusBarComponentBuilder;
+        mCollapsedStatusBarFragmentLogger = collapsedStatusBarFragmentLogger;
+        mStatusBarComponentFactory = statusBarComponentFactory;
         mPluginManager = pluginManager;
         mSplitScreenOptional = splitScreenOptional;
         mStatusBarNotificationActivityStarterBuilder = statusBarNotificationActivityStarterBuilder;
         mShadeController = shadeController;
-        mSuperStatusBarViewFactory = superStatusBarViewFactory;
+        mStatusBarWindowView = statusBarWindowView;
         mLightsOutNotifController =  lightsOutNotifController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mKeyguardViewMediatorCallback = viewMediatorCallback;
@@ -884,35 +874,62 @@
         mExtensionController = extensionController;
         mUserInfoControllerImpl = userInfoControllerImpl;
         mIconPolicy = phoneStatusBarPolicy;
-        mDismissCallbackRegistry = dismissCallbackRegistry;
         mDemoModeController = demoModeController;
         mNotificationIconAreaController = notificationIconAreaController;
         mBrightnessSliderFactory = brightnessSliderFactory;
-        mChargingRippleAnimationController = chargingRippleAnimationController;
+        mUnfoldTransitionConfig = unfoldTransitionConfig;
+        mUnfoldLightRevealOverlayAnimation = unfoldLightRevealOverlayAnimation;
+        mUnfoldWallpaperController = unfoldTransitionWallpaperController;
+        mWallpaperController = wallpaperController;
+        mMoveFromCenterAnimation = statusBarUnfoldAnimationController;
         mOngoingCallController = ongoingCallController;
         mAnimationScheduler = animationScheduler;
         mStatusBarLocationPublisher = locationPublisher;
         mStatusBarIconController = statusBarIconController;
         mFeatureFlags = featureFlags;
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
+        mMainHandler = mainHandler;
+        mMainExecutor = delayableExecutor;
+        mMessageRouter = messageRouter;
+        mWallpaperManager = wallpaperManager;
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
+        mTunerService = tunerService;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
         lockscreenShadeTransitionController.setStatusbar(this);
 
         mExpansionChangedListeners = new ArrayList<>();
+        addExpansionChangedListener(
+                (expansion, expanded) -> mScrimController.setRawPanelExpansionFraction(expansion));
+        addExpansionChangedListener(this::onPanelExpansionChanged);
 
         mBubbleExpandListener =
-                (isExpanding, key) -> {
-                    mContext.getMainExecutor().execute(() -> {
-                        mNotificationsController.requestNotificationUpdate("onBubbleExpandChanged");
-                        updateScrimController();
-                    });
-                };
+                (isExpanding, key) -> mContext.getMainExecutor().execute(() -> {
+                    mNotificationsController.requestNotificationUpdate("onBubbleExpandChanged");
+                    updateScrimController();
+                });
 
         mActivityIntentHelper = new ActivityIntentHelper(mContext);
+        mActivityLaunchAnimator = activityLaunchAnimator;
+        mDialogLaunchAnimator = dialogLaunchAnimator;
+
+        // The status bar background may need updating when the ongoing call status changes.
+        mOngoingCallController.addCallback((animate) -> maybeUpdateBarMode());
+
+        // TODO(b/190746471): Find a better home for this.
         DateTimeView.setReceiverHandler(timeTickHandler);
+
+        mMessageRouter.subscribeTo(KeyboardShortcutsMessage.class,
+                data -> toggleKeyboardShortcuts(data.mDeviceId));
+        mMessageRouter.subscribeTo(MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU,
+                id -> dismissKeyboardShortcuts());
+        mMessageRouter.subscribeTo(AnimateExpandSettingsPanelMessage.class,
+                data -> mCommandQueueCallbacks.animateExpandSettingsPanel(data.mSubpanel));
+        mMessageRouter.subscribeTo(MSG_LAUNCH_TRANSITION_TIMEOUT,
+                id -> onLaunchTransitionTimeout());
+
+        dumpManager.registerDumpable(this);
     }
 
     @Override
@@ -929,21 +946,18 @@
 
         mKeyguardIndicationController.init();
 
-        mColorExtractor.addOnColorsChangedListener(this);
-        mStatusBarStateController.addCallback(this,
+        mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
+        mStatusBarStateController.addCallback(mStateListener,
                 SysuiStatusBarStateController.RANK_STATUS_BAR);
 
         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
         mDreamManager = IDreamManager.Stub.asInterface(
                 ServiceManager.checkService(DreamService.DREAM_SERVICE));
 
-        mDisplay = mWindowManager.getDefaultDisplay();
+        mDisplay = mContext.getDisplay();
         mDisplayId = mDisplay.getDisplayId();
         updateDisplaySize();
 
-        mVibrateOnOpening = mContext.getResources().getBoolean(
-                R.bool.config_vibrateOnIconAnimation);
-
         // start old BaseStatusBar.start().
         mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
         mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService(
@@ -957,14 +971,7 @@
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
 
         mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
-        mWallpaperSupported =
-                mContext.getSystemService(WallpaperManager.class).isWallpaperSupported();
-
-        // Connect in to the status bar manager service
-        mCommandQueue.addCallback(this);
-
-        // Listen for demo mode changes
-        mDemoModeController.addCallback(this);
+        mWallpaperSupported = mWallpaperManager.isWallpaperSupported();
 
         RegisterStatusBarResult result = null;
         try {
@@ -991,12 +998,13 @@
         if (containsType(result.mTransientBarTypes, ITYPE_STATUS_BAR)) {
             showTransientUnchecked();
         }
-        onSystemBarAttributesChanged(mDisplayId, result.mAppearance, result.mAppearanceRegions,
-                result.mNavbarColorManagedByIme, result.mBehavior, result.mAppFullscreen);
+        mCommandQueueCallbacks.onSystemBarAttributesChanged(mDisplayId, result.mAppearance,
+                result.mAppearanceRegions, result.mNavbarColorManagedByIme, result.mBehavior,
+                result.mRequestedVisibilities, result.mPackageName);
 
         // StatusBarManagerService has a back up of IME token and it's restored here.
-        setImeWindowStatus(mDisplayId, result.mImeToken, result.mImeWindowVis,
-                result.mImeBackDisposition, result.mShowImeSwitcher);
+        mCommandQueueCallbacks.setImeWindowStatus(mDisplayId, result.mImeToken,
+                result.mImeWindowVis, result.mImeBackDisposition, result.mShowImeSwitcher);
 
         // Set up the initial icon state
         int numIcons = result.mIcons.size();
@@ -1035,7 +1043,13 @@
         // Lastly, call to the icon policy to install/update all the icons.
         mIconPolicy.init();
 
-        mKeyguardStateController.addCallback(this);
+        mKeyguardStateController.addCallback(new KeyguardStateController.Callback() {
+            @Override
+            public void onUnlockedChanged() {
+                updateKeyguardState();
+                logStateToEventlog();
+            }
+        });
         startKeyguard();
 
         mKeyguardUpdateMonitor.registerCallback(mUpdateCallback);
@@ -1047,9 +1061,9 @@
                 mAmbientIndicationContainer);
         updateLightRevealScrimVisibility();
 
-        mConfigurationController.addCallback(this);
+        mConfigurationController.addCallback(mConfigurationListener);
 
-        mBatteryController.observe(mLifecycle, this);
+        mBatteryController.observe(mLifecycle, mBatteryStateChangeCallback);
         mLifecycle.setCurrentState(RESUMED);
 
         // set the initial view visibility
@@ -1060,13 +1074,18 @@
 
         mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener);
 
+        if (mUnfoldTransitionConfig.isEnabled()) {
+            mUnfoldLightRevealOverlayAnimation.get().init();
+            mUnfoldWallpaperController.get().init();
+        }
+
         mPluginManager.addPluginListener(
                 new PluginListener<OverlayPlugin>() {
-                    private ArraySet<OverlayPlugin> mOverlays = new ArraySet<>();
+                    private final ArraySet<OverlayPlugin> mOverlays = new ArraySet<>();
 
                     @Override
                     public void onPluginConnected(OverlayPlugin plugin, Context pluginContext) {
-                        mMainThreadHandler.post(
+                        mMainExecutor.execute(
                                 () -> plugin.setup(getNotificationShadeWindowView(),
                                         getNavigationBarView(),
                                         new Callback(plugin), mDozeParameters));
@@ -1074,7 +1093,7 @@
 
                     @Override
                     public void onPluginDisconnected(OverlayPlugin plugin) {
-                        mMainThreadHandler.post(() -> {
+                        mMainExecutor.execute(() -> {
                             mOverlays.remove(plugin);
                             mNotificationShadeWindowController
                                     .setForcePluginOpen(mOverlays.size() != 0, this);
@@ -1095,7 +1114,7 @@
                             } else {
                                 mOverlays.remove(mPlugin);
                             }
-                            mMainThreadHandler.post(() -> {
+                            mMainExecutor.execute(() -> {
                                 mNotificationShadeWindowController
                                         .setStateListener(b -> mOverlays.forEach(
                                                 o -> o.setCollapseDesired(b)));
@@ -1105,13 +1124,17 @@
                         }
                     }
                 }, OverlayPlugin.class, true /* Allow multiple plugins */);
+
+        mStartingSurfaceOptional.ifPresent(startingSurface -> startingSurface.setSysuiProxy(
+                (requestTopUi, componentTag) -> mMainExecutor.execute(() ->
+                        mNotificationShadeWindowController.setRequestTopUi(
+                                requestTopUi, componentTag))));
     }
 
     // ================================================================================
     // Constructing the view
     // ================================================================================
     protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) {
-        final Context context = mContext;
         updateDisplaySize(); // populates mDisplayMetrics
         updateResources();
         updateTheme();
@@ -1119,26 +1142,25 @@
         inflateStatusBarWindow();
         mNotificationShadeWindowViewController.setService(this, mNotificationShadeWindowController);
         mNotificationShadeWindowView.setOnTouchListener(getStatusBarWindowTouchListener());
+        mWallpaperController.setRootView(mNotificationShadeWindowView);
 
         // TODO: Deal with the ugliness that comes from having some of the statusbar broken out
         // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
-        mStackScrollerController =
-                mNotificationPanelViewController.getNotificationStackScrollLayoutController();
-        mStackScroller = mStackScrollerController.getView();
         NotificationListContainer notifListContainer =
                 mStackScrollerController.getNotificationListContainer();
         mNotificationLogger.setUpWithContainer(notifListContainer);
 
-        inflateShelf();
         mNotificationIconAreaController.setupShelf(mNotificationShelfController);
         mNotificationPanelViewController.addExpansionListener(mWakeUpCoordinator);
         mNotificationPanelViewController.addExpansionListener(
                 this::dispatchPanelExpansionForKeyguardDismiss);
 
+        mUserSwitcherController.init(mNotificationShadeWindowView);
+
         // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
         mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
         mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class);
-        FragmentHostManager.get(mPhoneStatusBarWindow)
+        FragmentHostManager.get(mStatusBarWindowView)
                 .addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
                     CollapsedStatusBarFragment statusBarFragment =
                             (CollapsedStatusBarFragment) fragment;
@@ -1146,29 +1168,46 @@
                     PhoneStatusBarView oldStatusBarView = mStatusBarView;
                     mStatusBarView = (PhoneStatusBarView) statusBarFragment.getView();
                     mStatusBarView.setBar(this);
-                    mStatusBarView.setPanel(mNotificationPanelViewController);
+                    mStatusBarView.setPanelStateChangeListener(
+                            mNotificationPanelViewController.getPanelStateChangeListener());
                     mStatusBarView.setScrimController(mScrimController);
                     mStatusBarView.setExpansionChangedListeners(mExpansionChangedListeners);
                     for (ExpansionChangedListener listener : mExpansionChangedListeners) {
                         sendInitialExpansionAmount(listener);
                     }
 
-                    // CollapsedStatusBarFragment re-inflated PhoneStatusBarView and both of
-                    // mStatusBarView.mExpanded and mStatusBarView.mBouncerShowing are false.
-                    // PhoneStatusBarView's new instance will set to be gone in
-                    // PanelBar.updateVisibility after calling mStatusBarView.setBouncerShowing
-                    // that will trigger PanelBar.updateVisibility. If there is a heads up showing,
-                    // it needs to notify PhoneStatusBarView's new instance to update the correct
-                    // status by calling mNotificationPanel.notifyBarPanelExpansionChanged().
-                    if (mHeadsUpManager.hasPinnedHeadsUp()) {
-                        mNotificationPanelViewController.notifyBarPanelExpansionChanged();
+                    mNotificationPanelViewController.setBar(mStatusBarView);
+
+                    StatusBarMoveFromCenterAnimationController moveFromCenterAnimation = null;
+                    if (mUnfoldTransitionConfig.isEnabled()) {
+                        moveFromCenterAnimation = mMoveFromCenterAnimation.get();
                     }
-                    mStatusBarView.setBouncerShowing(mBouncerShowing);
-                    if (oldStatusBarView != null) {
-                        float fraction = oldStatusBarView.getExpansionFraction();
-                        boolean expanded = oldStatusBarView.isExpanded();
-                        mStatusBarView.panelExpansionChanged(fraction, expanded);
-                    }
+                    mPhoneStatusBarViewController =
+                            new PhoneStatusBarViewController(
+                                    mStatusBarView,
+                                    moveFromCenterAnimation,
+                                    mNotificationPanelViewController.getStatusBarTouchEventHandler()
+                            );
+                    mPhoneStatusBarViewController.init();
+
+                    mBatteryMeterViewController = new BatteryMeterViewController(
+                            mStatusBarView.findViewById(R.id.battery),
+                            mConfigurationController,
+                            mTunerService,
+                            mBroadcastDispatcher,
+                            mMainHandler,
+                            mContext.getContentResolver(),
+                            mBatteryController
+                    );
+                    mBatteryMeterViewController.init();
+
+                    // Ensure we re-propagate panel expansion values to the panel controller and
+                    // any listeners it may have, such as PanelBar. This will also ensure we
+                    // re-display the notification panel if necessary (for example, if
+                    // a heads-up notification was being displayed and should continue being
+                    // displayed).
+                    mNotificationPanelViewController.updatePanelExpansionAndVisibility();
+                    setBouncerShowingForStatusBarComponents(mBouncerShowing);
 
                     HeadsUpAppearanceController oldController = mHeadsUpAppearanceController;
                     if (mHeadsUpAppearanceController != null) {
@@ -1198,19 +1237,21 @@
                                 mStatusBarLocationPublisher,
                                 mNotificationIconAreaController,
                                 mFeatureFlags,
+
+                                () -> Optional.of(this),
                                 mStatusBarIconController,
                                 mKeyguardStateController,
                                 mNetworkController,
                                 mStatusBarStateController,
-                                this,
-                                mCommandQueue
+                                mCommandQueue,
+                                mCollapsedStatusBarFragmentLogger,
+                                mOperatorNameViewControllerFactory
                         ),
                         CollapsedStatusBarFragment.TAG)
                 .commit();
 
         mHeadsUpManager.setup(mVisualStabilityManager);
         mStatusBarTouchableRegionManager.setup(this, mNotificationShadeWindowView);
-        mHeadsUpManager.addListener(this);
         mHeadsUpManager.addListener(mNotificationPanelViewController.getOnHeadsUpChangedListener());
         mHeadsUpManager.addListener(mVisualStabilityManager);
         mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
@@ -1235,7 +1276,7 @@
 
             @Override
             public boolean shouldHideOnTouch() {
-                return !mRemoteInputManager.getController().isRemoteInputActive();
+                return !mRemoteInputManager.isRemoteInputActive();
             }
 
             @Override
@@ -1253,13 +1294,11 @@
         ScrimView notificationsScrim = mNotificationShadeWindowView
                 .findViewById(R.id.scrim_notifications);
         ScrimView scrimInFront = mNotificationShadeWindowView.findViewById(R.id.scrim_in_front);
-        ScrimView scrimForBubble = mBubblesManagerOptional.isPresent()
-                ? mBubblesManagerOptional.get().getScrimForBubble() : null;
 
         mScrimController.setScrimVisibleListener(scrimsVisible -> {
             mNotificationShadeWindowController.setScrimsVisibility(scrimsVisible);
         });
-        mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront, scrimForBubble);
+        mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront);
 
         mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim);
         mLightRevealScrim.setScrimOpaqueChangedListener((opaque) -> {
@@ -1280,6 +1319,7 @@
 
         mNotificationPanelViewController.initDependencies(
                 this,
+                this::makeExpandedInvisible,
                 mNotificationShelfController);
 
         BackDropView backdrop = mNotificationShadeWindowView.findViewById(R.id.backdrop);
@@ -1320,7 +1360,7 @@
                 QS qs = (QS) f;
                 if (qs instanceof QSFragment) {
                     mQSPanelController = ((QSFragment) qs).getQSPanelController();
-                    mQSPanelController.setBrightnessMirror(mBrightnessMirrorController);
+                    ((QSFragment) qs).setBrightnessMirrorController(mBrightnessMirrorController);
                 }
             });
         }
@@ -1351,14 +1391,11 @@
             });
         }
 
-        if (!mPowerManager.isScreenOn()) {
+        if (!mPowerManager.isInteractive()) {
             mBroadcastReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
         }
         mGestureWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
-                "GestureWakeLock");
-        mVibrator = mContext.getSystemService(Vibrator.class);
-        mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
-                mVibrator, context.getResources());
+                "sysui:GestureWakeLock");
 
         // receive broadcasts
         registerBroadcastReceiver();
@@ -1367,13 +1404,19 @@
         if (DEBUG_MEDIA_FAKE_ARTWORK) {
             demoFilter.addAction(ACTION_FAKE_ARTWORK);
         }
-        context.registerReceiverAsUser(mDemoReceiver, UserHandle.ALL, demoFilter,
+        mContext.registerReceiverAsUser(mDemoReceiver, UserHandle.ALL, demoFilter,
                 android.Manifest.permission.DUMP, null);
 
         // listen for USER_SETUP_COMPLETE setting (per-user)
         mDeviceProvisionedController.addCallback(mUserSetupObserver);
         mUserSetupObserver.onUserSetupChanged();
 
+        for (ExpansionChangedListener listener : mExpansionChangedListeners) {
+            // The initial expansion amount comes from mNotificationPanelViewController, so we
+            // should send the amount once we've fully set up that controller.
+            sendInitialExpansionAmount(listener);
+        }
+
         // disable profiling bars, since they overlap and clutter the output on app windows
         ThreadedRenderer.overrideProperty("disableProfileBars", "true");
 
@@ -1414,25 +1457,23 @@
         }
     }
 
+    private void onPanelExpansionChanged(float frac, boolean expanded) {
+        if (frac == 0 || frac == 1) {
+            if (getNavigationBarView() != null) {
+                getNavigationBarView().onStatusBarPanelStateChanged();
+            }
+            if (getNotificationPanelViewController() != null) {
+                getNotificationPanelViewController().updateSystemUiStateFlags();
+            }
+        }
+    }
+
     @NonNull
     @Override
     public Lifecycle getLifecycle() {
         return mLifecycle;
     }
 
-    @Override
-    public void onPowerSaveChanged(boolean isPowerSave) {
-        mHandler.post(mCheckBarModes);
-        if (mDozeServiceHost != null) {
-            mDozeServiceHost.firePowerSaveChanged(isPowerSave);
-        }
-    }
-
-    @Override
-    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
-        // noop
-    }
-
     @VisibleForTesting
     protected void registerBroadcastReceiver() {
         IntentFilter filter = new IntentFilter();
@@ -1448,7 +1489,7 @@
 
     private void setUpPresenter() {
         // Set up the initial notification state.
-        mActivityLaunchAnimator = new ActivityLaunchAnimator(this, mContext);
+        mActivityLaunchAnimator.setCallback(mKeyguardHandler);
         mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider(
                 mNotificationShadeWindowViewController,
                 mStackScrollerController.getNotificationListContainer(),
@@ -1456,17 +1497,37 @@
         );
 
         // TODO: inject this.
-        mPresenter = new StatusBarNotificationPresenter(mContext, mNotificationPanelViewController,
-                mHeadsUpManager, mNotificationShadeWindowView, mStackScrollerController,
-                mDozeScrimController, mScrimController, mNotificationShadeWindowController,
-                mDynamicPrivacyController, mKeyguardStateController,
+        mPresenter = new StatusBarNotificationPresenter(
+                mContext,
+                mNotificationPanelViewController,
+                mHeadsUpManager,
+                mNotificationShadeWindowView,
+                mStackScrollerController,
+                mDozeScrimController,
+                mScrimController,
+                mNotificationShadeWindowController,
+                mDynamicPrivacyController,
+                mKeyguardStateController,
                 mKeyguardIndicationController,
-                this /* statusBar */, mShadeController,
-                mLockscreenShadeTransitionController, mCommandQueue, mInitController,
-                mNotificationInterruptStateProvider);
+                this /* statusBar */,
+                mShadeController,
+                mLockscreenShadeTransitionController,
+                mCommandQueue,
+                mViewHierarchyManager,
+                mLockscreenUserManager,
+                mStatusBarStateController,
+                mEntryManager,
+                mMediaManager,
+                mGutsManager,
+                mKeyguardUpdateMonitor,
+                mLockscreenGestureLogger,
+                mInitController,
+                mNotificationInterruptStateProvider,
+                mRemoteInputManager,
+                mConfigurationController);
 
         mNotificationShelfController.setOnActivatedListener(mPresenter);
-        mRemoteInputManager.getController().addCallback(mNotificationShadeWindowController);
+        mRemoteInputManager.addControllerCallback(mNotificationShadeWindowController);
 
         mNotificationActivityStarter =
                 mStatusBarNotificationActivityStarterBuilder
@@ -1476,7 +1537,7 @@
                         .setNotificationPresenter(mPresenter)
                         .setNotificationPanelViewController(mNotificationPanelViewController)
                         .build();
-        mStackScroller.setNotificationActivityStarter(mNotificationActivityStarter);
+        mStackScrollerController.setNotificationActivityStarter(mNotificationActivityStarter);
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
 
         mNotificationsController.initialize(
@@ -1510,6 +1571,9 @@
                     time, PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:" + why);
             mWakeUpComingFromTouch = true;
             where.getLocationInWindow(mTmpInt2);
+
+            // NOTE, the incoming view can sometimes be the entire container... unsure if
+            // this location is valuable enough
             mWakeUpTouchLocation = new PointF(mTmpInt2[0] + where.getWidth() / 2,
                     mTmpInt2[1] + where.getHeight() / 2);
             mFalsingCollector.onScreenOnFromTouch();
@@ -1539,68 +1603,36 @@
         };
     }
 
-    private void inflateShelf() {
-        mNotificationShelfController = mSuperStatusBarViewFactory
-                .getNotificationShelfController(mStackScroller);
-    }
-
-    @Override
-    public void onDensityOrFontScaleChanged() {
-        // TODO: Remove this.
-        if (mBrightnessMirrorController != null) {
-            mBrightnessMirrorController.onDensityOrFontScaleChanged();
-        }
-        // TODO: Bring these out of StatusBar.
-        mUserInfoControllerImpl.onDensityOrFontScaleChanged();
-        mUserSwitcherController.onDensityOrFontScaleChanged();
-        mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
-        mHeadsUpManager.onDensityOrFontScaleChanged();
-    }
-
-    @Override
-    public void onThemeChanged() {
-        if (mStatusBarKeyguardViewManager != null) {
-            mStatusBarKeyguardViewManager.onThemeChanged();
-        }
-        if (mAmbientIndicationContainer instanceof AutoReinflateContainer) {
-            ((AutoReinflateContainer) mAmbientIndicationContainer).inflateLayout();
-        }
-        mNotificationIconAreaController.onThemeChanged();
-    }
-
-    @Override
-    public void onOverlayChanged() {
-        if (mBrightnessMirrorController != null) {
-            mBrightnessMirrorController.onOverlayChanged();
-        }
-        // We need the new R.id.keyguard_indication_area before recreating
-        // mKeyguardIndicationController
-        mNotificationPanelViewController.onThemeChanged();
-        onThemeChanged();
-    }
-
-    @Override
-    public void onUiModeChanged() {
-        if (mBrightnessMirrorController != null) {
-            mBrightnessMirrorController.onUiModeChanged();
-        }
-    }
-
     private void inflateStatusBarWindow() {
-        mNotificationShadeWindowView = mSuperStatusBarViewFactory.getNotificationShadeWindowView();
-        StatusBarComponent statusBarComponent = mStatusBarComponentBuilder.get()
-                .statusBarWindowView(mNotificationShadeWindowView).build();
+        StatusBarComponent statusBarComponent = mStatusBarComponentFactory.create();
+        mNotificationShadeWindowView = statusBarComponent.getNotificationShadeWindowView();
         mNotificationShadeWindowViewController = statusBarComponent
                 .getNotificationShadeWindowViewController();
         mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowViewController.setupExpandedStatusBar();
         mStatusBarWindowController = statusBarComponent.getStatusBarWindowController();
-        mPhoneStatusBarWindow = mSuperStatusBarViewFactory.getStatusBarWindowView();
         mNotificationPanelViewController = statusBarComponent.getNotificationPanelViewController();
         statusBarComponent.getLockIconViewController().init();
+        mStackScrollerController = statusBarComponent.getNotificationStackScrollLayoutController();
+        mStackScroller = mStackScrollerController.getView();
 
+        mNotificationShelfController = statusBarComponent.getNotificationShelfController();
         mAuthRippleController = statusBarComponent.getAuthRippleController();
         mAuthRippleController.init();
+
+        mHeadsUpManager.addListener(statusBarComponent.getStatusBarHeadsUpChangeListener());
+
+        mHeadsUpManager.addListener(statusBarComponent.getStatusBarHeadsUpChangeListener());
+
+        // Listen for demo mode changes
+        mDemoModeController.addCallback(statusBarComponent.getStatusBarDemoMode());
+
+        if (mCommandQueueCallbacks != null) {
+            mCommandQueue.removeCallback(mCommandQueueCallbacks);
+        }
+        mCommandQueueCallbacks = statusBarComponent.getStatusBarCommandQueueCallbacks();
+        // Connect in to the status bar manager service
+        mCommandQueue.addCallback(mCommandQueueCallbacks);
     }
 
     protected void startKeyguard() {
@@ -1635,13 +1667,13 @@
                     }
                 });
         mStatusBarKeyguardViewManager.registerStatusBar(
-                /* statusBar= */ this, getBouncerContainer(),
+                /* statusBar= */ this,
                 mNotificationPanelViewController, mBiometricUnlockController,
                 mStackScroller, mKeyguardBypassController);
         mKeyguardIndicationController
                 .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
-        mRemoteInputManager.getController().addCallback(mStatusBarKeyguardViewManager);
+        mRemoteInputManager.addControllerCallback(mStatusBarKeyguardViewManager);
         mDynamicPrivacyController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
 
         mLightBarController.setBiometricUnlockController(mBiometricUnlockController);
@@ -1650,7 +1682,7 @@
         Trace.endSection();
     }
 
-    protected View getStatusBarView() {
+    protected PhoneStatusBarView getStatusBarView() {
         return mStatusBarView;
     }
 
@@ -1659,7 +1691,7 @@
     }
 
     public StatusBarWindowView getStatusBarWindow() {
-        return mPhoneStatusBarWindow;
+        return mStatusBarWindowView;
     }
 
     public NotificationShadeWindowViewController getNotificationShadeWindowViewController() {
@@ -1670,8 +1702,8 @@
         return mNotificationPanelViewController;
     }
 
-    protected ViewGroup getBouncerContainer() {
-        return mNotificationShadeWindowView;
+    public ViewGroup getBouncerContainer() {
+        return mNotificationShadeWindowViewController.getBouncerContainer();
     }
 
     public int getStatusBarHeight() {
@@ -1712,7 +1744,7 @@
      * If the user switcher is simple then disable QS during setup because
      * the user intends to use the lock screen user switcher, QS in not needed.
      */
-    private void updateQsExpansionEnabled() {
+    void updateQsExpansionEnabled() {
         final boolean expandEnabled = mDeviceProvisionedController.isDeviceProvisioned()
                 && (mUserSetup || mUserSwitcherController == null
                         || !mUserSwitcherController.isSimpleUserSwitcher())
@@ -1728,22 +1760,6 @@
         return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0;
     }
 
-    public void addQsTile(ComponentName tile) {
-        if (mQSPanelController != null && mQSPanelController.getHost() != null) {
-            mQSPanelController.getHost().addTile(tile);
-        }
-    }
-
-    public void remQsTile(ComponentName tile) {
-        if (mQSPanelController != null && mQSPanelController.getHost() != null) {
-            mQSPanelController.getHost().removeTile(tile);
-        }
-    }
-
-    public void clickTile(ComponentName tile) {
-        mQSPanelController.clickTile(tile);
-    }
-
     /**
      * Request a notification update
      * @param reason why we're requesting a notification update
@@ -1769,102 +1785,10 @@
                 && mFalsingCollector.isReportingEnabled() ? View.VISIBLE : View.INVISIBLE);
     }
 
-    /**
-     * State is one or more of the DISABLE constants from StatusBarManager.
-     */
-    @Override
-    public void disable(int displayId, int state1, int state2, boolean animate) {
-        if (displayId != mDisplayId) {
-            return;
-        }
-        state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
-
-        animate &= mStatusBarWindowState != WINDOW_STATE_HIDDEN;
-        final int old1 = mDisabled1;
-        final int diff1 = state1 ^ old1;
-        mDisabled1 = state1;
-
-        final int old2 = mDisabled2;
-        final int diff2 = state2 ^ old2;
-        mDisabled2 = state2;
-
-        if (DEBUG) {
-            Log.d(TAG, String.format("disable1: 0x%08x -> 0x%08x (diff1: 0x%08x)",
-                old1, state1, diff1));
-            Log.d(TAG, String.format("disable2: 0x%08x -> 0x%08x (diff2: 0x%08x)",
-                old2, state2, diff2));
-        }
-
-        StringBuilder flagdbg = new StringBuilder();
-        flagdbg.append("disable<");
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_EXPAND))                ? 'E' : 'e');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_EXPAND))                ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ICONS))    ? 'I' : 'i');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_NOTIFICATION_ICONS))    ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS))   ? 'A' : 'a');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_NOTIFICATION_ALERTS))   ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_SYSTEM_INFO))           ? 'S' : 's');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_SYSTEM_INFO))           ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_BACK))                  ? 'B' : 'b');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_BACK))                  ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_HOME))                  ? 'H' : 'h');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_HOME))                  ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_RECENT))                ? 'R' : 'r');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_RECENT))                ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_CLOCK))                 ? 'C' : 'c');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_CLOCK))                 ? '!' : ' ');
-        flagdbg.append(0 != ((state1 & StatusBarManager.DISABLE_SEARCH))                ? 'S' : 's');
-        flagdbg.append(0 != ((diff1  & StatusBarManager.DISABLE_SEARCH))                ? '!' : ' ');
-        flagdbg.append("> disable2<");
-        flagdbg.append(0 != ((state2 & StatusBarManager.DISABLE2_QUICK_SETTINGS))       ? 'Q' : 'q');
-        flagdbg.append(0 != ((diff2  & StatusBarManager.DISABLE2_QUICK_SETTINGS))       ? '!' : ' ');
-        flagdbg.append(0 != ((state2 & StatusBarManager.DISABLE2_SYSTEM_ICONS))         ? 'I' : 'i');
-        flagdbg.append(0 != ((diff2  & StatusBarManager.DISABLE2_SYSTEM_ICONS))         ? '!' : ' ');
-        flagdbg.append(0 != ((state2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE))   ? 'N' : 'n');
-        flagdbg.append(0 != ((diff2  & StatusBarManager.DISABLE2_NOTIFICATION_SHADE))   ? '!' : ' ');
-        flagdbg.append('>');
-        Log.d(TAG, flagdbg.toString());
-
-        if ((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) {
-            if ((state1 & StatusBarManager.DISABLE_EXPAND) != 0) {
-                mShadeController.animateCollapsePanels();
-            }
-        }
-
-        if ((diff1 & StatusBarManager.DISABLE_RECENT) != 0) {
-            if ((state1 & StatusBarManager.DISABLE_RECENT) != 0) {
-                // close recents if it's visible
-                mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
-                mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS);
-            }
-        }
-
-        if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
-            if (areNotificationAlertsDisabled()) {
-                mHeadsUpManager.releaseAllImmediately();
-            }
-        }
-
-        if ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) {
-            updateQsExpansionEnabled();
-        }
-
-        if ((diff2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
-            updateQsExpansionEnabled();
-            if ((state2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
-                mShadeController.animateCollapsePanels();
-            }
-        }
-    }
-
     boolean areNotificationAlertsDisabled() {
         return (mDisabled1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
     }
 
-    protected H createHandler() {
-        return new StatusBar.H();
-    }
-
     @Override
     public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade,
             int flags) {
@@ -1878,10 +1802,77 @@
 
     @Override
     public void startActivity(Intent intent, boolean dismissShade,
-            ActivityLaunchAnimator.Controller animationController) {
-        startActivityDismissingKeyguard(intent, false, dismissShade,
+            @Nullable ActivityLaunchAnimator.Controller animationController,
+            boolean showOverLockscreenWhenLocked) {
+        // Make sure that we dismiss the keyguard if it is directly dismissable or when we don't
+        // want to show the activity above it.
+        if (mKeyguardStateController.isUnlocked() || !showOverLockscreenWhenLocked) {
+            startActivityDismissingKeyguard(intent, false, dismissShade,
                 false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */,
                 0 /* flags */, animationController);
+            return;
+        }
+
+        boolean animate =
+                animationController != null && shouldAnimateLaunch(true /* isActivityIntent */,
+                        showOverLockscreenWhenLocked);
+
+        ActivityLaunchAnimator.Controller controller = null;
+        if (animate) {
+            // Wrap the animation controller to dismiss the shade and set
+            // mIsLaunchingActivityOverLockscreen during the animation.
+            ActivityLaunchAnimator.Controller delegate = wrapAnimationController(
+                    animationController, dismissShade);
+            controller = new DelegateLaunchAnimatorController(delegate) {
+                @Override
+                public void onIntentStarted(boolean willAnimate) {
+                    getDelegate().onIntentStarted(willAnimate);
+
+                    if (willAnimate) {
+                        StatusBar.this.mIsLaunchingActivityOverLockscreen = true;
+                    }
+                }
+
+                @Override
+                public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually finishing the
+                    // animation so that we can assume that mIsLaunchingActivityOverLockscreen
+                    // being true means that we will collapse the shade (or at least run the
+                    // post collapse runnables) later on.
+                    StatusBar.this.mIsLaunchingActivityOverLockscreen = false;
+                    getDelegate().onLaunchAnimationEnd(isExpandingFullyAbove);
+                }
+
+                @Override
+                public void onLaunchAnimationCancelled() {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually finishing the
+                    // animation so that we can assume that mIsLaunchingActivityOverLockscreen
+                    // being true means that we will collapse the shade (or at least run the
+                    // post collapse runnables) later on.
+                    StatusBar.this.mIsLaunchingActivityOverLockscreen = false;
+                    getDelegate().onLaunchAnimationCancelled();
+                }
+            };
+        } else if (dismissShade) {
+            // The animation will take care of dismissing the shade at the end of the animation. If
+            // we don't animate, collapse it directly.
+            collapseShade();
+        }
+
+        mActivityLaunchAnimator.startIntentWithAnimation(controller, animate,
+                intent.getPackage(), showOverLockscreenWhenLocked, (adapter) -> TaskStackBuilder
+                        .create(mContext)
+                        .addNextIntent(intent)
+                        .startActivities(getActivityOptions(getDisplayId(), adapter),
+                                UserHandle.CURRENT));
+    }
+
+    /**
+     * Whether we are currently animating an activity launch above the lockscreen (occluding
+     * activity).
+     */
+    public boolean isLaunchingActivityOverLockscreen() {
+        return mIsLaunchingActivityOverLockscreen;
     }
 
     @Override
@@ -1901,6 +1892,7 @@
         mNotificationPanelViewController.setStatusAccessibilityImportance(expanded
                 ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
                 : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+        mNotificationPanelViewController.updateSystemUiStateFlags();
         if (getNavigationBarView() != null) {
             getNavigationBarView().onStatusBarPanelStateChanged();
         }
@@ -1921,70 +1913,6 @@
         logStateToEventlog();
     }
 
-    @Override
-    public void onUnlockedChanged() {
-        updateKeyguardState();
-        logStateToEventlog();
-    }
-
-    @Override
-    public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
-        if (inPinnedMode) {
-            mNotificationShadeWindowController.setHeadsUpShowing(true);
-            mStatusBarWindowController.setForceStatusBarVisible(true);
-            if (mNotificationPanelViewController.isFullyCollapsed()) {
-                // We need to ensure that the touchable region is updated before the window will be
-                // resized, in order to not catch any touches. A layout will ensure that
-                // onComputeInternalInsets will be called and after that we can resize the layout. Let's
-                // make sure that the window stays small for one frame until the touchableRegion is set.
-                mNotificationPanelViewController.getView().requestLayout();
-                mNotificationShadeWindowController.setForceWindowCollapsed(true);
-                mNotificationPanelViewController.getView().post(() -> {
-                    mNotificationShadeWindowController.setForceWindowCollapsed(false);
-                });
-            }
-        } else {
-            boolean bypassKeyguard = mKeyguardBypassController.getBypassEnabled()
-                    && mState == StatusBarState.KEYGUARD;
-            if (!mNotificationPanelViewController.isFullyCollapsed()
-                    || mNotificationPanelViewController.isTracking() || bypassKeyguard) {
-                // We are currently tracking or is open and the shade doesn't need to be kept
-                // open artificially.
-                mNotificationShadeWindowController.setHeadsUpShowing(false);
-                if (bypassKeyguard) {
-                    mStatusBarWindowController.setForceStatusBarVisible(false);
-                }
-            } else {
-                // we need to keep the panel open artificially, let's wait until the animation
-                // is finished.
-                mHeadsUpManager.setHeadsUpGoingAway(true);
-                mNotificationPanelViewController.runAfterAnimationFinished(() -> {
-                    if (!mHeadsUpManager.hasPinnedHeadsUp()) {
-                        mNotificationShadeWindowController.setHeadsUpShowing(false);
-                        mHeadsUpManager.setHeadsUpGoingAway(false);
-                    }
-                    mRemoteInputManager.onPanelCollapsed();
-                });
-            }
-        }
-    }
-
-    @Override
-    public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
-        mNotificationsController.requestNotificationUpdate("onHeadsUpStateChanged");
-        if (mStatusBarStateController.isDozing() && isHeadsUp) {
-            entry.setPulseSuppressed(false);
-            mDozeServiceHost.fireNotificationPulse(entry);
-            if (mDozeServiceHost.isPulsing()) {
-                mDozeScrimController.cancelPendingPulseTimeout();
-            }
-        }
-        if (!isHeadsUp && !mHeadsUpManager.hasNotifications()) {
-            // There are no longer any notifications to show.  We should end the pulse now.
-            mDozeScrimController.pulseOutNow();
-        }
-    }
-
     public void setPanelExpanded(boolean isExpanded) {
         if (mPanelExpanded != isExpanded) {
             mNotificationLogger.onPanelExpandedChanged(isExpanded);
@@ -2017,11 +1945,6 @@
         return mNotificationPanelViewController.hideStatusBarIconsWhenExpanded();
     }
 
-    @Override
-    public void onColorsChanged(ColorExtractor extractor, int which) {
-        updateTheme();
-    }
-
     @Nullable
     public View getAmbientIndicationContainer() {
         return mAmbientIndicationContainer;
@@ -2057,7 +1980,7 @@
      *
      * @param animate should the change of the icons be animated.
      */
-    private void updateHideIconsForBouncer(boolean animate) {
+    void updateHideIconsForBouncer(boolean animate) {
         boolean hideBecauseApp = mTopHidesStatusBar && mIsOccluded
                 && (mStatusBarWindowHidden || mBouncerShowing);
         boolean hideBecauseKeyguard = !mPanelExpanded && !mIsOccluded && mBouncerShowing;
@@ -2068,7 +1991,7 @@
                 // We're delaying the showing, since most of the time the fullscreen app will
                 // hide the icons again and we don't want them to fade in and out immediately again.
                 mWereIconsJustHidden = true;
-                mHandler.postDelayed(() -> {
+                mMainExecutor.executeDelayed(() -> {
                     mWereIconsJustHidden = false;
                     mCommandQueue.recomputeDisableFlags(mDisplayId, true);
                 }, 500);
@@ -2111,51 +2034,28 @@
      *
      * Note: This method must be called *before* dismissing the keyguard.
      */
-    public boolean shouldAnimateLaunch(boolean isActivityIntent) {
+    public boolean shouldAnimateLaunch(boolean isActivityIntent, boolean showOverLockscreen) {
         // TODO(b/184121838): Support launch animations when occluded.
         if (isOccluded()) {
             return false;
         }
 
-        // Always animate if we are unlocked.
-        if (!mKeyguardStateController.isShowing()) {
+        // Always animate if we are not showing the keyguard or if we animate over the lockscreen
+        // (without unlocking it).
+        if (showOverLockscreen || !mKeyguardStateController.isShowing()) {
             return true;
         }
 
-        // If we are locked, only animate if remote unlock animations are enabled. We also don't
-        // animate non-activity launches as they can break the animation.
+        // If we are locked and have to dismiss the keyguard, only animate if remote unlock
+        // animations are enabled. We also don't animate non-activity launches as they can break the
+        // animation.
         // TODO(b/184121838): Support non activity launches on the lockscreen.
         return isActivityIntent && KeyguardService.sEnableRemoteKeyguardGoingAwayAnimation;
     }
 
-    @Override
-    public boolean isOnKeyguard() {
-        return mKeyguardStateController.isShowing();
-    }
-
-    @Override
-    public void hideKeyguardWithAnimation(IRemoteAnimationRunner runner) {
-        // We post to the main thread for 2 reasons:
-        //   1. KeyguardViewMediator is not thread-safe.
-        //   2. To ensure that ViewMediatorCallback#keyguardDonePending is called before
-        //      ViewMediatorCallback#readyForKeyguardDone. The wrong order could occur when doing
-        //      dismissKeyguardThenExecute { hideKeyguardWithAnimation(runner) }.
-        mMainThreadHandler.post(() -> mKeyguardViewMediator.hideWithAnimation(runner));
-    }
-
-    @Override
-    public void setBlursDisabledForAppLaunch(boolean disabled) {
-        mKeyguardViewMediator.setBlursDisabledForAppLaunch(disabled);
-    }
-
-    @Override
-    public int getBackgroundColor(TaskInfo task) {
-        if (!mStartingSurfaceOptional.isPresent()) {
-            Log.w(TAG, "No starting surface, defaulting to SystemBGColor");
-            return SplashscreenContentDrawer.getSystemBGColor();
-        }
-
-        return mStartingSurfaceOptional.get().getBackgroundColor(task);
+    /** Whether we should animate an activity launch. */
+    public boolean shouldAnimateLaunch(boolean isActivityIntent) {
+        return shouldAnimateLaunch(isActivityIntent, false /* showOverLockscreen */);
     }
 
     public boolean isDeviceInVrMode() {
@@ -2171,38 +2071,19 @@
         mState = state;
     }
 
-    @VisibleForTesting
-    void setUserSetupForTest(boolean userSetup) {
-        mUserSetup = userSetup;
+    static class KeyboardShortcutsMessage {
+        final int mDeviceId;
+
+        KeyboardShortcutsMessage(int deviceId) {
+            mDeviceId = deviceId;
+        }
     }
 
-    /**
-     * All changes to the status bar and notifications funnel through here and are batched.
-     */
-    protected class H extends Handler {
-        @Override
-        public void handleMessage(Message m) {
-            switch (m.what) {
-                case MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU:
-                    toggleKeyboardShortcuts(m.arg1);
-                    break;
-                case MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU:
-                    dismissKeyboardShortcuts();
-                    break;
-                // End old BaseStatusBar.H handling.
-                case MSG_OPEN_NOTIFICATION_PANEL:
-                    animateExpandNotificationsPanel();
-                    break;
-                case MSG_OPEN_SETTINGS_PANEL:
-                    animateExpandSettingsPanel((String) m.obj);
-                    break;
-                case MSG_CLOSE_PANELS:
-                    mShadeController.animateCollapsePanels();
-                    break;
-                case MSG_LAUNCH_TRANSITION_TIMEOUT:
-                    onLaunchTransitionTimeout();
-                    break;
-            }
+    static class AnimateExpandSettingsPanelMessage {
+        final String mSubpanel;
+
+        AnimateExpandSettingsPanelMessage(String subpanel) {
+            mSubpanel = subpanel;
         }
     }
 
@@ -2226,59 +2107,6 @@
         mHeadsUpManager.releaseAllImmediately();
     }
 
-    /**
-     * Called for system navigation gestures. First action opens the panel, second opens
-     * settings. Down action closes the entire panel.
-     */
-    @Override
-    public void handleSystemKey(int key) {
-        if (SPEW) Log.d(TAG, "handleNavigationKey: " + key);
-        if (!mCommandQueue.panelsEnabled() || !mKeyguardUpdateMonitor.isDeviceInteractive()
-                || mKeyguardStateController.isShowing() && !mKeyguardStateController.isOccluded()) {
-            return;
-        }
-
-        // Panels are not available in setup
-        if (!mUserSetup) return;
-
-        if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP == key) {
-            mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_UP);
-            mNotificationPanelViewController.collapse(
-                    false /* delayed */, 1.0f /* speedUpFactor */);
-        } else if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN == key) {
-            mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_DOWN);
-            if (mNotificationPanelViewController.isFullyCollapsed()) {
-                if (mVibrateOnOpening) {
-                    mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
-                }
-                mNotificationPanelViewController.expand(true /* animate */);
-                mStackScroller.setWillExpand(true);
-                mHeadsUpManager.unpinAll(true /* userUnpinned */);
-                mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN, 1);
-            } else if (!mNotificationPanelViewController.isInSettings()
-                    && !mNotificationPanelViewController.isExpanding()) {
-                mNotificationPanelViewController.flingSettings(0 /* velocity */,
-                        NotificationPanelView.FLING_EXPAND);
-                mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN_QS, 1);
-            }
-        }
-
-    }
-
-    @Override
-    public void showPinningEnterExitToast(boolean entering) {
-        if (getNavigationBarView() != null) {
-            getNavigationBarView().showPinningEnterExitToast(entering);
-        }
-    }
-
-    @Override
-    public void showPinningEscapeToast() {
-        if (getNavigationBarView() != null) {
-            getNavigationBarView().showPinningEscapeToast();
-        }
-    }
-
     void makeExpandedVisible(boolean force) {
         if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible);
         if (!force && (mExpandedVisible || !mCommandQueue.panelsEnabled())) {
@@ -2297,42 +2125,17 @@
     }
 
     public void postAnimateCollapsePanels() {
-        mHandler.post(mShadeController::animateCollapsePanels);
+        mMainExecutor.execute(mShadeController::animateCollapsePanels);
     }
 
     public void postAnimateForceCollapsePanels() {
-        mHandler.post(() -> mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE,
+        mMainExecutor.execute(
+                () -> mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE,
                 true /* force */));
     }
 
     public void postAnimateOpenPanels() {
-        mHandler.sendEmptyMessage(MSG_OPEN_SETTINGS_PANEL);
-    }
-
-    @Override
-    public void togglePanel() {
-        if (mPanelExpanded) {
-            mShadeController.animateCollapsePanels();
-        } else {
-            animateExpandNotificationsPanel();
-        }
-    }
-
-    @Override
-    public void animateCollapsePanels(int flags, boolean force) {
-        mShadeController.animateCollapsePanels(flags, force, false /* delayed */,
-                1.0f /* speedUpFactor */);
-    }
-
-    /**
-     * Called by {@link ShadeController} when it calls
-     * {@link ShadeController#animateCollapsePanels(int, boolean, boolean, float)}.
-     */
-    void postHideRecentApps() {
-        if (!mHandler.hasMessages(MSG_HIDE_RECENT_APPS)) {
-            mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
-            mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS);
-        }
+        mMessageRouter.sendMessage(MSG_OPEN_SETTINGS_PANEL);
     }
 
     public boolean isExpandedVisible() {
@@ -2358,39 +2161,10 @@
         }
     }
 
-    @Override
-    public void animateExpandNotificationsPanel() {
-        if (SPEW) Log.d(TAG, "animateExpand: mExpandedVisible=" + mExpandedVisible);
-        if (!mCommandQueue.panelsEnabled()) {
-            return ;
-        }
-
-        mNotificationPanelViewController.expandWithoutQs();
-
-        if (false) postStartTracing();
-    }
-
-    @Override
-    public void animateExpandSettingsPanel(@Nullable String subPanel) {
-        if (SPEW) Log.d(TAG, "animateExpand: mExpandedVisible=" + mExpandedVisible);
-        if (!mCommandQueue.panelsEnabled()) {
-            return;
-        }
-
-        // Settings are not available in setup
-        if (!mUserSetup) return;
-
-        if (subPanel != null) {
-            mQSPanelController.openDetails(subPanel);
-        }
-        mNotificationPanelViewController.expandWithQs();
-
-        if (false) postStartTracing();
-    }
-
     public void animateCollapseQuickSettings() {
         if (mState == StatusBarState.SHADE) {
-            mStatusBarView.collapsePanel(true, false /* delayed */, 1.0f /* speedUpFactor */);
+            mNotificationPanelViewController.collapsePanel(
+                    true, false /* delayed */, 1.0f /* speedUpFactor */);
         }
     }
 
@@ -2403,7 +2177,7 @@
         }
 
         // Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868)
-        mStatusBarView.collapsePanel(/*animate=*/ false, false /* delayed*/,
+        mNotificationPanelViewController.collapsePanel(/*animate=*/ false, false /* delayed*/,
                 1.0f /* speedUpFactor */);
 
         mNotificationPanelViewController.closeQs();
@@ -2437,7 +2211,10 @@
         }
     }
 
-    public boolean interceptTouchEvent(MotionEvent event) {
+    /** Called when a touch event occurred on {@link PhoneStatusBarView}. */
+    public void onTouchEvent(MotionEvent event) {
+        // TODO(b/202981994): Move this touch debugging to a central location. (Right now, it's
+        //   split between NotificationPanelViewController and here.)
         if (DEBUG_GESTURES) {
             if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
                 EventLog.writeEvent(EventLogTags.SYSUI_STATUSBAR_TOUCH,
@@ -2467,13 +2244,8 @@
             final boolean upOrCancel =
                     event.getAction() == MotionEvent.ACTION_UP ||
                     event.getAction() == MotionEvent.ACTION_CANCEL;
-            if (upOrCancel && !mExpandedVisible) {
-                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
-            } else {
-                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true);
-            }
+            setInteracting(StatusBarManager.WINDOW_STATUS_BAR, !upOrCancel || mExpandedVisible);
         }
-        return false;
     }
 
     boolean isSameStatusBarState(int state) {
@@ -2488,88 +2260,23 @@
         return mBiometricUnlockController;
     }
 
-    @Override // CommandQueue
-    public void setWindowState(
-            int displayId, @WindowType int window, @WindowVisibleState int state) {
-        if (displayId != mDisplayId) {
-            return;
-        }
-        boolean showing = state == WINDOW_STATE_SHOWING;
-        if (mNotificationShadeWindowView != null
-                && window == StatusBarManager.WINDOW_STATUS_BAR
-                && mStatusBarWindowState != state) {
-            mStatusBarWindowState = state;
-            if (DEBUG_WINDOW_STATE) Log.d(TAG, "Status bar " + windowStateToString(state));
-            if (mStatusBarView != null) {
-                if (!showing && mState == StatusBarState.SHADE) {
-                    mStatusBarView.collapsePanel(false /* animate */, false /* delayed */,
-                            1.0f /* speedUpFactor */);
-                }
-                mStatusBarWindowHidden = state == WINDOW_STATE_HIDDEN;
-                updateHideIconsForBouncer(false /* animate */);
-            }
-        }
-
-        updateBubblesVisibility();
-    }
-
-    @Override
-    public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
-            AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, boolean isFullscreen) {
-        if (displayId != mDisplayId) {
-            return;
-        }
-        boolean barModeChanged = false;
-        if (mAppearance != appearance) {
-            mAppearance = appearance;
-            barModeChanged = updateBarMode(barMode(mTransientShown, appearance));
-        }
-        mLightBarController.onStatusBarAppearanceChanged(appearanceRegions, barModeChanged,
-                mStatusBarMode, navbarColorManagedByIme);
-
-        updateBubblesVisibility();
-        mStatusBarStateController.setFullscreenState(isFullscreen);
-    }
-
-    @Override
-    public void showTransient(int displayId, @InternalInsetsType int[] types) {
-        if (displayId != mDisplayId) {
-            return;
-        }
-        if (!containsType(types, ITYPE_STATUS_BAR)) {
-            return;
-        }
-        showTransientUnchecked();
-    }
-
-    private void showTransientUnchecked() {
+    void showTransientUnchecked() {
         if (!mTransientShown) {
             mTransientShown = true;
             mNoAnimationOnNextBarModeChange = true;
-            handleTransientChanged();
+            maybeUpdateBarMode();
         }
     }
 
-    @Override
-    public void abortTransient(int displayId, @InternalInsetsType int[] types) {
-        if (displayId != mDisplayId) {
-            return;
-        }
-        if (!containsType(types, ITYPE_STATUS_BAR)) {
-            return;
-        }
-        clearTransient();
-    }
 
-    private void clearTransient() {
+    void clearTransient() {
         if (mTransientShown) {
             mTransientShown = false;
-            handleTransientChanged();
+            maybeUpdateBarMode();
         }
     }
 
-    private void handleTransientChanged() {
+    private void maybeUpdateBarMode() {
         final int barMode = barMode(mTransientShown, mAppearance);
         if (updateBarMode(barMode)) {
             mLightBarController.onStatusBarModeChanged(barMode);
@@ -2587,9 +2294,11 @@
         return false;
     }
 
-    private static @TransitionMode int barMode(boolean isTransient, int appearance) {
+    private @TransitionMode int barMode(boolean isTransient, int appearance) {
         final int lightsOutOpaque = APPEARANCE_LOW_PROFILE_BARS | APPEARANCE_OPAQUE_STATUS_BARS;
-        if (isTransient) {
+        if (mOngoingCallController.hasOngoingCall() && mIsFullscreen) {
+            return MODE_SEMI_TRANSPARENT;
+        } else if (isTransient) {
             return MODE_SEMI_TRANSPARENT;
         } else if ((appearance & lightsOutOpaque) == lightsOutOpaque) {
             return MODE_LIGHTS_OUT;
@@ -2604,8 +2313,7 @@
         }
     }
 
-    @Override
-    public void showWirelessChargingAnimation(int batteryLevel) {
+    protected void showWirelessChargingAnimation(int batteryLevel) {
         showChargingAnimation(batteryLevel, UNKNOWN_BATTERY_LEVEL, 0);
     }
 
@@ -2626,11 +2334,6 @@
                 }, false, sUiEventLogger).show(animationDelay);
     }
 
-    @Override
-    public void onRecentsAnimationStateChanged(boolean running) {
-        setInteracting(StatusBarManager.WINDOW_NAVIGATION_BAR, running);
-    }
-
     protected BarTransitions getStatusBarTransitions() {
         return mNotificationShadeWindowViewController.getBarTransitions();
     }
@@ -2650,13 +2353,11 @@
     }
 
     /** Temporarily hides Bubbles if the status bar is hidden. */
-    private void updateBubblesVisibility() {
-        if (mBubblesOptional.isPresent()) {
-            mBubblesOptional.get().onStatusBarVisibilityChanged(
-                    mStatusBarMode != MODE_LIGHTS_OUT
-                            && mStatusBarMode != MODE_LIGHTS_OUT_TRANSPARENT
-                            && !mStatusBarWindowHidden);
-        }
+    void updateBubblesVisibility() {
+        mBubblesOptional.ifPresent(bubbles -> bubbles.onStatusBarVisibilityChanged(
+                mStatusBarMode != MODE_LIGHTS_OUT
+                        && mStatusBarMode != MODE_LIGHTS_OUT_TRANSPARENT
+                        && !mStatusBarWindowHidden));
     }
 
     void checkBarMode(@TransitionMode int mode, @WindowVisibleState int windowState,
@@ -2740,7 +2441,7 @@
             mNotificationPanelViewController.dump(fd, pw, args);
         }
         pw.println("  mStackScroller: ");
-        if (mStackScroller instanceof Dumpable) {
+        if (mStackScroller != null) {
             pw.print  ("      ");
             ((Dumpable) mStackScroller).dump(fd, pw, args);
         }
@@ -2773,19 +2474,6 @@
 
         mNotificationsController.dump(fd, pw, args, DUMPTRUCK);
 
-        if (DUMPTRUCK) {
-            if (false) {
-                pw.println("see the logcat for a dump of the views we have created.");
-                // must happen on ui thread
-                mHandler.post(() -> {
-                    mStatusBarView.getLocationOnScreen(mAbsPos);
-                    Log.d(TAG, "mStatusBarView: ----- (" + mAbsPos[0] + "," + mAbsPos[1] +
-                            ") " + mStatusBarView.getWidth() + "x" + getStatusBarHeight());
-                    mStatusBarView.debug();
-                });
-            }
-        }
-
         if (DEBUG_GESTURES) {
             pw.print("  status bar gestures: ");
             mGestureRec.dump(fd, pw, args);
@@ -2816,7 +2504,7 @@
         pw.println("   Insecure camera: " + CameraIntents.getInsecureCameraIntent(mContext));
         pw.println("   Secure camera: " + CameraIntents.getSecureCameraIntent(mContext));
         pw.println("   Override package: "
-                + String.valueOf(CameraIntents.getOverrideCameraPackage(mContext)));
+                + CameraIntents.getOverrideCameraPackage(mContext));
     }
 
     public static void dumpBarTransitions(
@@ -2877,7 +2565,7 @@
         startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, 0);
     }
 
-    private void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+    void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
             final boolean dismissShade, final boolean disallowEnterPictureInPictureWhileLaunching,
             final Callback callback, int flags,
             @Nullable ActivityLaunchAnimator.Controller animationController) {
@@ -2891,7 +2579,8 @@
                 animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch(
                         true /* isActivityIntent */);
         ActivityLaunchAnimator.Controller animController =
-                animate ? wrapAnimationController(animationController, dismissShade) : null;
+                animationController != null ? wrapAnimationController(animationController,
+                        dismissShade) : null;
 
         // If we animate, we will dismiss the shade only once the animation is done. This is taken
         // care of by the StatusBarLaunchAnimationController.
@@ -2922,7 +2611,7 @@
                             options.setRotationAnimationHint(
                                     WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
                         }
-                        if (intent.getAction() == Settings.Panel.ACTION_VOLUME) {
+                        if (Settings.Panel.ACTION_VOLUME.equals(intent.getAction())) {
                             // Settings Panel is implemented as activity(not a dialog), so
                             // underlying app is paused and may enter picture-in-picture mode
                             // as a result.
@@ -2962,7 +2651,7 @@
     private ActivityLaunchAnimator.Controller wrapAnimationController(
             ActivityLaunchAnimator.Controller animationController, boolean dismissShade) {
         View rootView = animationController.getLaunchContainer().getRootView();
-        if (rootView == mSuperStatusBarViewFactory.getStatusBarWindowView()) {
+        if (rootView == mStatusBarWindowView) {
             // We are animating a view in the status bar. We have to make sure that the status bar
             // window matches the full screen during the animation and that we are expanding the
             // view below the other status bar text.
@@ -3021,7 +2710,7 @@
                             && mStatusBarKeyguardViewManager.isOccluded()) {
                         mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
                     } else {
-                        AsyncTask.execute(runnable);
+                        mMainExecutor.execute(runnable);
                     }
                 }
                 if (dismissShade) {
@@ -3033,7 +2722,7 @@
 
                         // Do it after DismissAction has been processed to conserve the needed
                         // ordering.
-                        mHandler.post(mShadeController::runPostCollapseRunnables);
+                        mMainExecutor.execute(mShadeController::runPostCollapseRunnables);
                     }
                 } else if (StatusBar.this.isInLaunchTransition()
                         && mNotificationPanelViewController.isLaunchTransitionFinished()) {
@@ -3042,7 +2731,7 @@
                     // finished,
                     // so nobody will call readyForKeyguardDone anymore. Post it such that
                     // keyguardDonePending gets called first.
-                    mHandler.post(mStatusBarKeyguardViewManager::readyForKeyguardDone);
+                    mMainExecutor.execute(mStatusBarKeyguardViewManager::readyForKeyguardDone);
                 }
                 return deferred;
             }
@@ -3063,9 +2752,7 @@
             String action = intent.getAction();
             if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
                 KeyboardShortcuts.dismiss();
-                if (mRemoteInputManager.getController() != null) {
-                    mRemoteInputManager.getController().closeRemoteInputs();
-                }
+                mRemoteInputManager.closeRemoteInputs();
                 if (mBubblesOptional.isPresent() && mBubblesOptional.get().isStackExpanded()) {
                     mBubblesOptional.get().collapseStack();
                 }
@@ -3083,8 +2770,8 @@
                     mNotificationShadeWindowController.setNotTouchable(false);
                 }
                 if (mBubblesOptional.isPresent() && mBubblesOptional.get().isStackExpanded()) {
-                    // Post to main thread handler, since updating the UI.
-                    mMainThreadHandler.post(() -> mBubblesOptional.get().collapseStack());
+                    // Post to main thread, since updating the UI.
+                    mMainExecutor.execute(() -> mBubblesOptional.get().collapseStack());
                 }
                 finishBarAnimations();
                 resetUserExpandedStates();
@@ -3145,20 +2832,6 @@
             action.onDismiss();
         }
     }
-
-    @Override
-    public void onConfigChanged(Configuration newConfig) {
-        updateResources();
-        updateDisplaySize(); // populates mDisplayMetrics
-
-        if (DEBUG) {
-            Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
-        }
-
-        mViewHierarchyManager.updateRowStates();
-        mScreenPinningRequest.onConfigurationChanged();
-    }
-
     /**
      * Notify the shade controller that the current user changed
      *
@@ -3306,43 +2979,12 @@
                 | ((currentlyInsecure ? 1 : 0) << 12);
     }
 
-    //
-    // tracing
-    //
-
-    void postStartTracing() {
-        mHandler.postDelayed(mStartTracing, 3000);
-    }
-
-    void vibrate() {
-        android.os.Vibrator vib = (android.os.Vibrator)mContext.getSystemService(
-                Context.VIBRATOR_SERVICE);
-        vib.vibrate(250, VIBRATION_ATTRIBUTES);
-    }
-
-    final Runnable mStartTracing = new Runnable() {
-        @Override
-        public void run() {
-            vibrate();
-            SystemClock.sleep(250);
-            Log.d(TAG, "startTracing");
-            android.os.Debug.startMethodTracing("/data/statusbar-traces/trace");
-            mHandler.postDelayed(mStopTracing, 10000);
-        }
-    };
-
-    final Runnable mStopTracing = () -> {
-        android.os.Debug.stopMethodTracing();
-        Log.d(TAG, "stopTracing");
-        vibrate();
-    };
-
     @Override
     public void postQSRunnableDismissingKeyguard(final Runnable runnable) {
-        mHandler.post(() -> {
+        mMainExecutor.execute(() -> {
             mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
-            executeRunnableDismissingKeyguard(() -> mHandler.post(runnable), null, false, false,
-                    false);
+            executeRunnableDismissingKeyguard(
+                    () -> mMainExecutor.execute(runnable), null, false, false, false);
         });
     }
 
@@ -3354,7 +2996,7 @@
     @Override
     public void postStartActivityDismissingKeyguard(final PendingIntent intent,
             @Nullable ActivityLaunchAnimator.Controller animationController) {
-        mHandler.post(() -> startPendingIntentDismissingKeyguard(intent,
+        mMainExecutor.execute(() -> startPendingIntentDismissingKeyguard(intent,
                 null /* intentSentUiThreadCallback */, animationController));
     }
 
@@ -3366,7 +3008,7 @@
     @Override
     public void postStartActivityDismissingKeyguard(Intent intent, int delay,
             @Nullable ActivityLaunchAnimator.Controller animationController) {
-        mHandler.postDelayed(
+        mMainExecutor.executeDelayed(
                 () ->
                         startActivityDismissingKeyguard(intent, true /* onlyProvisioned */,
                                 true /* dismissShade */,
@@ -3377,82 +3019,6 @@
                 delay);
     }
 
-    @Override
-    public List<String> demoCommands() {
-        List<String> s = new ArrayList<>();
-        s.add(DemoMode.COMMAND_BARS);
-        s.add(DemoMode.COMMAND_CLOCK);
-        s.add(DemoMode.COMMAND_OPERATOR);
-        return s;
-    }
-
-    @Override
-    public void onDemoModeStarted() {
-        // Must send this message to any view that we delegate to via dispatchDemoCommandToView
-        dispatchDemoModeStartedToView(R.id.clock);
-        dispatchDemoModeStartedToView(R.id.operator_name);
-    }
-
-    @Override
-    public void onDemoModeFinished() {
-        dispatchDemoModeFinishedToView(R.id.clock);
-        dispatchDemoModeFinishedToView(R.id.operator_name);
-        checkBarModes();
-    }
-
-    @Override
-    public void dispatchDemoCommand(String command, @NonNull Bundle args) {
-        if (command.equals(COMMAND_CLOCK)) {
-            dispatchDemoCommandToView(command, args, R.id.clock);
-        }
-        if (command.equals(COMMAND_BARS)) {
-            String mode = args.getString("mode");
-            int barMode = "opaque".equals(mode) ? MODE_OPAQUE :
-                    "translucent".equals(mode) ? MODE_TRANSLUCENT :
-                    "semi-transparent".equals(mode) ? MODE_SEMI_TRANSPARENT :
-                    "transparent".equals(mode) ? MODE_TRANSPARENT :
-                    "warning".equals(mode) ? MODE_WARNING :
-                    -1;
-            if (barMode != -1) {
-                boolean animate = true;
-                if (mNotificationShadeWindowController != null
-                        && mNotificationShadeWindowViewController.getBarTransitions() != null) {
-                    mNotificationShadeWindowViewController.getBarTransitions().transitionTo(
-                            barMode, animate);
-                }
-                mNavigationBarController.transitionTo(mDisplayId, barMode, animate);
-            }
-        }
-        if (command.equals(COMMAND_OPERATOR)) {
-            dispatchDemoCommandToView(command, args, R.id.operator_name);
-        }
-    }
-
-    //TODO: these should have controllers, and this method should be removed
-    private void dispatchDemoCommandToView(String command, Bundle args, int id) {
-        if (mStatusBarView == null) return;
-        View v = mStatusBarView.findViewById(id);
-        if (v instanceof DemoModeCommandReceiver) {
-            ((DemoModeCommandReceiver) v).dispatchDemoCommand(command, args);
-        }
-    }
-
-    private void dispatchDemoModeStartedToView(int id) {
-        if (mStatusBarView == null) return;
-        View v = mStatusBarView.findViewById(id);
-        if (v instanceof DemoModeCommandReceiver) {
-            ((DemoModeCommandReceiver) v).onDemoModeStarted();
-        }
-    }
-
-    private void dispatchDemoModeFinishedToView(int id) {
-        if (mStatusBarView == null) return;
-        View v = mStatusBarView.findViewById(id);
-        if (v instanceof DemoModeCommandReceiver) {
-            ((DemoModeCommandReceiver) v).onDemoModeFinished();
-        }
-    }
-
     public void showKeyguard() {
         mStatusBarStateController.setKeyguardRequested(true);
         mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
@@ -3512,7 +3078,7 @@
             mNotificationPanelViewController.cancelAnimation();
             onLaunchTransitionFadingEnded();
         }
-        mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+        mMessageRouter.cancelMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
         if (mUserSwitcherController != null && mUserSwitcherController.useFullscreenUserSwitcher()) {
             mStatusBarStateController.setState(StatusBarState.FULLSCREEN_USER_SWITCHER);
         } else if (!mPulseExpansionHandler.isWakingToShadeLocked()) {
@@ -3553,7 +3119,7 @@
      */
     public void fadeKeyguardAfterLaunchTransition(final Runnable beforeFading,
             Runnable endRunnable) {
-        mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+        mMessageRouter.cancelMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
         mLaunchTransitionEndRunnable = endRunnable;
         Runnable hideRunnable = () -> {
             mKeyguardStateController.setLaunchTransitionFadingAway(true);
@@ -3583,8 +3149,20 @@
     public void fadeKeyguardWhilePulsing() {
         mNotificationPanelViewController.fadeOut(0, FADE_KEYGUARD_DURATION_PULSING,
                 ()-> {
-                hideKeyguard();
-                mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+                Runnable finishFading = () -> {
+                    mCallingFadingAwayAfterReveal = false;
+                    hideKeyguard();
+                    mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+                };
+                if (mLightRevealScrim.getRevealAmount() != 1.0f) {
+                    mCallingFadingAwayAfterReveal = true;
+                    // We're still revealing the Light reveal, let's only go to keyguard once
+                    // that has finished and nothing moves anymore.
+                    // Going there introduces lots of jank
+                    mLightRevealScrim.setFullyRevealedRunnable(finishFading);
+                } else {
+                    finishFading.run();
+                }
             }).start();
     }
 
@@ -3593,7 +3171,7 @@
      */
     public void animateKeyguardUnoccluding() {
         mNotificationPanelViewController.setExpandedFraction(0f);
-        animateExpandNotificationsPanel();
+        mCommandQueueCallbacks.animateExpandNotificationsPanel();
         mScrimController.setUnocclusionAnimationRunning(true);
     }
 
@@ -3603,8 +3181,8 @@
      * because the launched app crashed or something else went wrong.
      */
     public void startLaunchTransitionTimeout() {
-        mHandler.sendEmptyMessageDelayed(MSG_LAUNCH_TRANSITION_TIMEOUT,
-                LAUNCH_TRANSITION_TIMEOUT_MS);
+        mMessageRouter.sendMessageDelayed(
+                MSG_LAUNCH_TRANSITION_TIMEOUT, LAUNCH_TRANSITION_TIMEOUT_MS);
     }
 
     private void onLaunchTransitionTimeout() {
@@ -3659,7 +3237,7 @@
         if (mQSPanelController != null) {
             mQSPanelController.refreshAllTiles();
         }
-        mHandler.removeMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
+        mMessageRouter.cancelMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
         releaseGestureWakeLock();
         mNotificationPanelViewController.onAffordanceLaunchEnded();
         mNotificationPanelViewController.cancelAnimation();
@@ -3718,16 +3296,16 @@
      * Switches theme from light to dark and vice-versa.
      */
     protected void updateTheme() {
-
         // Lock wallpaper defines the color of the majority of the views, hence we'll use it
         // to set our default theme.
         final boolean lockDarkText = mColorExtractor.getNeutralColors().supportsDarkText();
         final int themeResId = lockDarkText ? R.style.Theme_SystemUI_LightWallpaper
                 : R.style.Theme_SystemUI;
-        if (mContext.getThemeResId() != themeResId) {
-            mContext.setTheme(themeResId);
-            mConfigurationController.notifyThemeChanged();
+        if (mContext.getThemeResId() == themeResId) {
+            return;
         }
+        mContext.setTheme(themeResId);
+        mConfigurationController.notifyThemeChanged();
     }
 
     private void updateDozingState() {
@@ -3765,7 +3343,7 @@
 
     /**
      * While IME is active and a BACK event is detected, check with
-     * {@link StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme(KeyEvent)} to see if the event
+     * {@link StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event
      * should be handled before routing to IME, in order to prevent the user having to hit back
      * twice to exit bouncer.
      */
@@ -3887,71 +3465,6 @@
         mNotificationPanelViewController.collapseWithDuration(duration);
     }
 
-    @Override
-    public void onStatePreChange(int oldState, int newState) {
-        // If we're visible and switched to SHADE_LOCKED (the user dragged
-        // down on the lockscreen), clear notification LED, vibration,
-        // ringing.
-        // Other transitions are covered in handleVisibleToUserChanged().
-        if (mVisible && (newState == StatusBarState.SHADE_LOCKED
-                || mStatusBarStateController.goingToFullShade())) {
-            clearNotificationEffects();
-        }
-        if (newState == StatusBarState.KEYGUARD) {
-            mRemoteInputManager.onPanelCollapsed();
-            maybeEscalateHeadsUp();
-        }
-    }
-
-    @Override
-    public void onStateChanged(int newState) {
-        mState = newState;
-        updateReportRejectedTouchVisibility();
-        mDozeServiceHost.updateDozing();
-        updateTheme();
-        mNavigationBarController.touchAutoDim(mDisplayId);
-        Trace.beginSection("StatusBar#updateKeyguardState");
-        if (mState == StatusBarState.KEYGUARD && mStatusBarView != null) {
-            mStatusBarView.removePendingHideExpandedRunnables();
-        }
-        updateDozingState();
-        checkBarModes();
-        updateScrimController();
-        mPresenter.updateMediaMetaData(false, mState != StatusBarState.KEYGUARD);
-        updateKeyguardState();
-        Trace.endSection();
-    }
-
-    @Override
-    public void onDozeAmountChanged(float linear, float eased) {
-        if (mFeatureFlags.useNewLockscreenAnimations()
-                && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)
-                && !mBiometricUnlockController.isWakeAndUnlock()) {
-            mLightRevealScrim.setRevealAmount(1f - linear);
-        }
-    }
-
-    @Override
-    public void onDozingChanged(boolean isDozing) {
-        Trace.beginSection("StatusBar#updateDozing");
-        mDozing = isDozing;
-
-        // Collapse the notification panel if open
-        boolean dozingAnimated = mDozeServiceHost.getDozingRequested()
-                && mDozeParameters.shouldControlScreenOff();
-        mNotificationPanelViewController.resetViews(dozingAnimated);
-
-        updateQsExpansionEnabled();
-        mKeyguardViewMediator.setDozing(mDozing);
-
-        mNotificationsController.requestNotificationUpdate("onDozingChanged");
-        updateDozingState();
-        mDozeServiceHost.updateDozing();
-        updateScrimController();
-        updateReportRejectedTouchVisibility();
-        Trace.endSection();
-    }
-
     /**
      * Updates the light reveal effect to reflect the reason we're waking or sleeping (for example,
      * from the power button).
@@ -4052,7 +3565,7 @@
         mBouncerShowing = bouncerShowing;
         mKeyguardBypassController.setBouncerShowing(bouncerShowing);
         mPulseExpansionHandler.setBouncerShowing(bouncerShowing);
-        if (mStatusBarView != null) mStatusBarView.setBouncerShowing(bouncerShowing);
+        setBouncerShowingForStatusBarComponents(bouncerShowing);
         updateHideIconsForBouncer(true /* animate */);
         mCommandQueue.recomputeDisableFlags(mDisplayId, true /* animate */);
         updateScrimController();
@@ -4062,6 +3575,23 @@
     }
 
     /**
+     * Propagate the bouncer state to status bar components.
+     *
+     * Separate from {@link #setBouncerShowing} because we sometimes re-create the status bar and
+     * should update only the status bar components.
+     */
+    private void setBouncerShowingForStatusBarComponents(boolean bouncerShowing) {
+        int importance = bouncerShowing
+                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+        if (mPhoneStatusBarViewController != null) {
+            mPhoneStatusBarViewController.setImportantForAccessibility(importance);
+        }
+        mNotificationPanelViewController.setImportantForAccessibility(importance);
+        mNotificationPanelViewController.setBouncerShowing(bouncerShowing);
+    }
+
+    /**
      * Collapses the notification shade if it is tracking or expanded.
      */
     public void collapseShade() {
@@ -4092,7 +3622,8 @@
 
                 // This gets executed before we will show Keyguard, so post it in order that the state
                 // is correct.
-                mHandler.post(() -> onCameraLaunchGestureDetected(mLastCameraLaunchSource));
+                mMainExecutor.execute(() -> mCommandQueueCallbacks.onCameraLaunchGestureDetected(
+                        mLastCameraLaunchSource));
             }
 
             if (mLaunchEmergencyActionOnFinishedGoingToSleep) {
@@ -4100,7 +3631,8 @@
 
                 // This gets executed before we will show Keyguard, so post it in order that the
                 // state is correct.
-                mHandler.post(() -> onEmergencyActionLaunchGestureDetected());
+                mMainExecutor.execute(
+                        () -> mCommandQueueCallbacks.onEmergencyActionLaunchGestureDetected());
             }
             updateIsKeyguard();
         }
@@ -4213,36 +3745,6 @@
         return mWakefulnessLifecycle.getWakefulness();
     }
 
-    private void vibrateForCameraGesture() {
-        // Make sure to pass -1 for repeat so VibratorService doesn't stop us when going to sleep.
-        mVibrator.vibrate(mCameraLaunchGestureVibrationEffect, VIBRATION_ATTRIBUTES);
-    }
-
-    private static VibrationEffect getCameraGestureVibrationEffect(Vibrator vibrator,
-            Resources resources) {
-        if (vibrator.areAllPrimitivesSupported(
-                VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
-                VibrationEffect.Composition.PRIMITIVE_CLICK)) {
-            return VibrationEffect.startComposition()
-                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE)
-                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 50)
-                    .compose();
-        }
-        if (vibrator.hasAmplitudeControl()) {
-            return VibrationEffect.createWaveform(
-                    CAMERA_LAUNCH_GESTURE_VIBRATION_TIMINGS,
-                    CAMERA_LAUNCH_GESTURE_VIBRATION_AMPLITUDES,
-                    /* repeat= */ -1);
-        }
-
-        int[] pattern = resources.getIntArray(R.array.config_cameraLaunchGestureVibePattern);
-        long[] timings = new long[pattern.length];
-        for (int i = 0; i < pattern.length; i++) {
-            timings[i] = pattern[i];
-        }
-        return VibrationEffect.createWaveform(timings, /* repeat= */ -1);
-    }
-
     /**
      * @return true if the screen is currently fully off, i.e. has finished turning off and has
      * since not started turning on.
@@ -4251,139 +3753,11 @@
         return mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_OFF;
     }
 
-    @Override
-    public void showScreenPinningRequest(int taskId) {
-        if (mKeyguardStateController.isShowing()) {
-            // Don't allow apps to trigger this from keyguard.
-            return;
-        }
-        // Show screen pinning request, since this comes from an app, show 'no thanks', button.
-        showScreenPinningRequest(taskId, true);
-    }
-
     public void showScreenPinningRequest(int taskId, boolean allowCancel) {
         mScreenPinningRequest.showPrompt(taskId, allowCancel);
     }
 
-    @Override
-    public void appTransitionCancelled(int displayId) {
-        if (displayId == mDisplayId) {
-            mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.onAppTransitionFinished());
-        }
-    }
-
-    @Override
-    public void appTransitionFinished(int displayId) {
-        if (displayId == mDisplayId) {
-            mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.onAppTransitionFinished());
-        }
-    }
-
-    @Override
-    public void onCameraLaunchGestureDetected(int source) {
-        mLastCameraLaunchSource = source;
-        if (isGoingToSleep()) {
-            if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Finish going to sleep before launching camera");
-            mLaunchCameraOnFinishedGoingToSleep = true;
-            return;
-        }
-        if (!mNotificationPanelViewController.canCameraGestureBeLaunched()) {
-            if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Can't launch camera right now");
-            return;
-        }
-        if (!mDeviceInteractive) {
-            mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_CAMERA_LAUNCH,
-                    "com.android.systemui:CAMERA_GESTURE");
-        }
-        vibrateForCameraGesture();
-
-        if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) {
-            Log.v(TAG, "Camera launch");
-            mKeyguardUpdateMonitor.onCameraLaunched();
-        }
-
-        if (!mStatusBarKeyguardViewManager.isShowing()) {
-            final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
-            startActivityDismissingKeyguard(cameraIntent,
-                    false /* onlyProvisioned */, true /* dismissShade */,
-                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
-                    null /* animationController */);
-        } else {
-            if (!mDeviceInteractive) {
-                // Avoid flickering of the scrim when we instant launch the camera and the bouncer
-                // comes on.
-                mGestureWakeLock.acquire(LAUNCH_TRANSITION_TIMEOUT_MS + 1000L);
-            }
-            if (isWakingUpOrAwake()) {
-                if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Launching camera");
-                if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
-                    mStatusBarKeyguardViewManager.reset(true /* hide */);
-                }
-                mNotificationPanelViewController.launchCamera(
-                        mDeviceInteractive /* animate */, source);
-                updateScrimController();
-            } else {
-                // We need to defer the camera launch until the screen comes on, since otherwise
-                // we will dismiss us too early since we are waiting on an activity to be drawn and
-                // incorrectly get notified because of the screen on event (which resumes and pauses
-                // some activities)
-                if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Deferring until screen turns on");
-                mLaunchCameraWhenFinishedWaking = true;
-            }
-        }
-    }
-
-    @Override
-    public void onEmergencyActionLaunchGestureDetected() {
-        Intent emergencyIntent = getEmergencyActionIntent();
-
-        if (emergencyIntent == null) {
-            Log.wtf(TAG, "Couldn't find an app to process the emergency intent.");
-            return;
-        }
-
-        if (isGoingToSleep()) {
-            mLaunchEmergencyActionOnFinishedGoingToSleep = true;
-            return;
-        }
-
-        if (!mDeviceInteractive) {
-            mPowerManager.wakeUp(SystemClock.uptimeMillis(),
-                    PowerManager.WAKE_REASON_GESTURE,
-                    "com.android.systemui:EMERGENCY_GESTURE");
-        }
-        // TODO(b/169087248) Possibly add haptics here for emergency action. Currently disabled for
-        // app-side haptic experimentation.
-
-        if (!mStatusBarKeyguardViewManager.isShowing()) {
-            startActivityDismissingKeyguard(emergencyIntent,
-                    false /* onlyProvisioned */, true /* dismissShade */,
-                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
-                    null /* animationController */);
-            return;
-        }
-
-        if (!mDeviceInteractive) {
-            // Avoid flickering of the scrim when we instant launch the camera and the bouncer
-            // comes on.
-            mGestureWakeLock.acquire(LAUNCH_TRANSITION_TIMEOUT_MS + 1000L);
-        }
-
-        if (isWakingUpOrAwake()) {
-            if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
-                mStatusBarKeyguardViewManager.reset(true /* hide */);
-            }
-            mContext.startActivityAsUser(emergencyIntent, UserHandle.CURRENT);
-            return;
-        }
-        // We need to defer the emergency action launch until the screen comes on, since otherwise
-        // we will dismiss us too early since we are waiting on an activity to be drawn and
-        // incorrectly get notified because of the screen on event (which resumes and pauses
-        // some activities)
-        mLaunchEmergencyActionWhenFinishedWaking = true;
-    }
-
-    private @Nullable Intent getEmergencyActionIntent() {
+    @Nullable Intent getEmergencyActionIntent() {
         Intent emergencyIntent = new Intent(EmergencyGesture.ACTION_LAUNCH_EMERGENCY);
         PackageManager pm = mContext.getPackageManager();
         List<ResolveInfo> emergencyActivities = pm.queryIntentActivities(emergencyIntent,
@@ -4441,16 +3815,11 @@
         return true;
     }
 
-    private boolean isGoingToSleep() {
+    boolean isGoingToSleep() {
         return mWakefulnessLifecycle.getWakefulness()
                 == WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
     }
 
-    private boolean isWakingUpOrAwake() {
-        return mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
-                || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING;
-    }
-
     public void notifyBiometricAuthModeChanged() {
         mDozeServiceHost.updateDozing();
         updateScrimController();
@@ -4477,7 +3846,11 @@
         mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview);
 
         if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
-            mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED);
+            if (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED) {
+                mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE);
+            } else {
+                mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED);
+            }
         } else if (mBouncerShowing) {
             // Bouncer needs the front scrim when it's on top of an activity,
             // tapping on a notification, editing QS or being dismissed by
@@ -4506,8 +3879,6 @@
             mScrimController.transitionTo(ScrimState.AOD);
         } else if (mIsKeyguard && !unlocking) {
             mScrimController.transitionTo(ScrimState.KEYGUARD);
-        } else if (mBubblesOptional.isPresent() && mBubblesOptional.get().isStackExpanded()) {
-            mScrimController.transitionTo(ScrimState.BUBBLE_EXPANDED, mUnlockScrimCallback);
         } else {
             mScrimController.transitionTo(ScrimState.UNLOCKED, mUnlockScrimCallback);
         }
@@ -4604,10 +3975,6 @@
         mNotificationsController.setNotificationSnoozed(sbn, snoozeOption);
     }
 
-    @Override
-    public void toggleSplitScreen() {
-        toggleSplitScreenMode(-1 /* metricsDockAction */, -1 /* metricsUndockAction */);
-    }
 
     public void awakenDreams() {
         mUiBgExecutor.execute(() -> {
@@ -4619,46 +3986,6 @@
         });
     }
 
-    @Override
-    public void preloadRecentApps() {
-        int msg = MSG_PRELOAD_RECENT_APPS;
-        mHandler.removeMessages(msg);
-        mHandler.sendEmptyMessage(msg);
-    }
-
-    @Override
-    public void cancelPreloadRecentApps() {
-        int msg = MSG_CANCEL_PRELOAD_RECENT_APPS;
-        mHandler.removeMessages(msg);
-        mHandler.sendEmptyMessage(msg);
-    }
-
-    @Override
-    public void dismissKeyboardShortcutsMenu() {
-        int msg = MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU;
-        mHandler.removeMessages(msg);
-        mHandler.sendEmptyMessage(msg);
-    }
-
-    @Override
-    public void toggleKeyboardShortcutsMenu(int deviceId) {
-        int msg = MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU;
-        mHandler.removeMessages(msg);
-        mHandler.obtainMessage(msg, deviceId, 0).sendToTarget();
-    }
-
-    @Override
-    public void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) {
-        mTopHidesStatusBar = topAppHidesStatusBar;
-        if (!topAppHidesStatusBar && mWereIconsJustHidden) {
-            // Immediately update the icon hidden state, since that should only apply if we're
-            // staying fullscreen.
-            mWereIconsJustHidden = false;
-            mCommandQueue.recomputeDisableFlags(mDisplayId, true);
-        }
-        updateHideIconsForBouncer(true /* animate */);
-    }
-
     protected void toggleKeyboardShortcuts(int deviceId) {
         KeyboardShortcuts.toggle(mContext, deviceId);
     }
@@ -4778,7 +4105,7 @@
     }
 
     private void postOnUiThread(Runnable runnable) {
-        mMainThreadHandler.post(runnable);
+        mMainExecutor.execute(runnable);
     }
 
     /**
@@ -4920,34 +4247,19 @@
         }
         return mStatusBarKeyguardViewManager.isSecure();
     }
-
-    @Override
-    public void showAssistDisclosure() {
-        mAssistManagerLazy.get().showDisclosure();
-    }
-
     public NotificationPanelViewController getPanelController() {
         return mNotificationPanelViewController;
     }
-
-    @Override
-    public void startAssist(Bundle args) {
-        mAssistManagerLazy.get().startAssist(args);
-    }
     // End Extra BaseStatusBarMethods.
 
     public NotificationGutsManager getGutsManager() {
         return mGutsManager;
     }
 
-    private boolean isTransientShown() {
+    boolean isTransientShown() {
         return mTransientShown;
     }
 
-    @Override
-    public void suppressAmbientDisplay(boolean suppressed) {
-        mDozeServiceHost.setDozeSuppressed(suppressed);
-    }
 
     public void addExpansionChangedListener(@NonNull ExpansionChangedListener listener) {
         mExpansionChangedListeners.add(listener);
@@ -4955,9 +4267,10 @@
     }
 
     private void sendInitialExpansionAmount(ExpansionChangedListener expansionChangedListener) {
-        if (mStatusBarView != null) {
-            expansionChangedListener.onExpansionChanged(mStatusBarView.getExpansionFraction(),
-                    mStatusBarView.isExpanded());
+        if (mNotificationPanelViewController != null) {
+            expansionChangedListener.onExpansionChanged(
+                    mNotificationPanelViewController.getExpandedFraction(),
+                    mNotificationPanelViewController.isExpanded());
         }
     }
 
@@ -4973,4 +4286,285 @@
 
         mLightRevealScrim.setAlpha(mScrimController.getState().getMaxLightRevealScrimAlpha());
     }
+
+    private final KeyguardUpdateMonitorCallback mUpdateCallback =
+            new KeyguardUpdateMonitorCallback() {
+                @Override
+                public void onDreamingStateChanged(boolean dreaming) {
+                    if (dreaming) {
+                        maybeEscalateHeadsUp();
+                    }
+                }
+
+                // TODO: (b/145659174) remove when moving to NewNotifPipeline. Replaced by
+                //  KeyguardCoordinator
+                @Override
+                public void onStrongAuthStateChanged(int userId) {
+                    super.onStrongAuthStateChanged(userId);
+                    mNotificationsController.requestNotificationUpdate("onStrongAuthStateChanged");
+                }
+            };
+
+
+    private final FalsingManager.FalsingBeliefListener mFalsingBeliefListener =
+            new FalsingManager.FalsingBeliefListener() {
+                @Override
+                public void onFalse() {
+                    // Hides quick settings, bouncer, and quick-quick settings.
+                    mStatusBarKeyguardViewManager.reset(true);
+                }
+            };
+
+    // Notifies StatusBarKeyguardViewManager every time the keyguard transition is over,
+    // this animation is tied to the scrim for historic reasons.
+    // TODO: notify when keyguard has faded away instead of the scrim.
+    private final ScrimController.Callback mUnlockScrimCallback = new ScrimController
+            .Callback() {
+        @Override
+        public void onFinished() {
+            if (mStatusBarKeyguardViewManager == null) {
+                Log.w(TAG, "Tried to notify keyguard visibility when "
+                        + "mStatusBarKeyguardViewManager was null");
+                return;
+            }
+            if (mKeyguardStateController.isKeyguardFadingAway() && !mCallingFadingAwayAfterReveal) {
+                mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+            }
+        }
+
+        @Override
+        public void onCancelled() {
+            onFinished();
+        }
+    };
+
+    private final DeviceProvisionedListener mUserSetupObserver = new DeviceProvisionedListener() {
+        @Override
+        public void onUserSetupChanged() {
+            final boolean userSetup = mDeviceProvisionedController.isUserSetup(
+                    mDeviceProvisionedController.getCurrentUser());
+            Log.d(TAG, "mUserSetupObserver - DeviceProvisionedListener called for user "
+                    + mDeviceProvisionedController.getCurrentUser());
+            if (MULTIUSER_DEBUG) {
+                Log.d(TAG, String.format("User setup changed: userSetup=%s mUserSetup=%s",
+                        userSetup, mUserSetup));
+            }
+
+            if (userSetup != mUserSetup) {
+                mUserSetup = userSetup;
+                if (!mUserSetup && mStatusBarView != null) {
+                    animateCollapseQuickSettings();
+                }
+                if (mNotificationPanelViewController != null) {
+                    mNotificationPanelViewController.setUserSetupComplete(mUserSetup);
+                }
+                updateQsExpansionEnabled();
+            }
+        }
+    };
+
+    private final BroadcastReceiver mWallpaperChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (!mWallpaperSupported) {
+                // Receiver should not have been registered at all...
+                Log.wtf(TAG, "WallpaperManager not supported");
+                return;
+            }
+            WallpaperInfo info = mWallpaperManager.getWallpaperInfo(UserHandle.USER_CURRENT);
+            mWallpaperController.onWallpaperInfoUpdated(info);
+
+            final boolean deviceSupportsAodWallpaper = mContext.getResources().getBoolean(
+                    com.android.internal.R.bool.config_dozeSupportsAodWallpaper);
+            // If WallpaperInfo is null, it must be ImageWallpaper.
+            final boolean supportsAmbientMode = deviceSupportsAodWallpaper
+                    && (info != null && info.supportsAmbientMode());
+
+            mNotificationShadeWindowController.setWallpaperSupportsAmbientMode(supportsAmbientMode);
+            mScrimController.setWallpaperSupportsAmbientMode(supportsAmbientMode);
+            mKeyguardViewMediator.setWallpaperSupportsAmbientMode(supportsAmbientMode);
+        }
+    };
+
+    BroadcastReceiver mTaskbarChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mBubblesOptional.ifPresent(bubbles -> bubbles.onTaskbarChanged(intent.getExtras()));
+        }
+    };
+
+    private final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
+        @Override
+        public void onConfigChanged(Configuration newConfig) {
+            updateResources();
+            updateDisplaySize(); // populates mDisplayMetrics
+
+            if (DEBUG) {
+                Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
+            }
+
+            mViewHierarchyManager.updateRowStates();
+            mScreenPinningRequest.onConfigurationChanged();
+        }
+
+        @Override
+        public void onDensityOrFontScaleChanged() {
+            // TODO: Remove this.
+            if (mBrightnessMirrorController != null) {
+                mBrightnessMirrorController.onDensityOrFontScaleChanged();
+            }
+            // TODO: Bring these out of StatusBar.
+            mUserInfoControllerImpl.onDensityOrFontScaleChanged();
+            mUserSwitcherController.onDensityOrFontScaleChanged();
+            mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
+            mHeadsUpManager.onDensityOrFontScaleChanged();
+        }
+
+        @Override
+        public void onThemeChanged() {
+            if (mBrightnessMirrorController != null) {
+                mBrightnessMirrorController.onOverlayChanged();
+            }
+            // We need the new R.id.keyguard_indication_area before recreating
+            // mKeyguardIndicationController
+            mNotificationPanelViewController.onThemeChanged();
+
+            if (mStatusBarKeyguardViewManager != null) {
+                mStatusBarKeyguardViewManager.onThemeChanged();
+            }
+            if (mAmbientIndicationContainer instanceof AutoReinflateContainer) {
+                ((AutoReinflateContainer) mAmbientIndicationContainer).inflateLayout();
+            }
+            mNotificationIconAreaController.onThemeChanged();
+        }
+
+        @Override
+        public void onUiModeChanged() {
+            if (mBrightnessMirrorController != null) {
+                mBrightnessMirrorController.onUiModeChanged();
+            }
+        }
+    };
+
+    private StatusBarStateController.StateListener mStateListener =
+            new StatusBarStateController.StateListener() {
+                @Override
+                public void onStatePreChange(int oldState, int newState) {
+                    // If we're visible and switched to SHADE_LOCKED (the user dragged
+                    // down on the lockscreen), clear notification LED, vibration,
+                    // ringing.
+                    // Other transitions are covered in handleVisibleToUserChanged().
+                    if (mVisible && (newState == StatusBarState.SHADE_LOCKED
+                            || mStatusBarStateController.goingToFullShade())) {
+                        clearNotificationEffects();
+                    }
+                    if (newState == StatusBarState.KEYGUARD) {
+                        mRemoteInputManager.onPanelCollapsed();
+                        maybeEscalateHeadsUp();
+                    }
+                }
+
+                @Override
+                public void onStateChanged(int newState) {
+                    mState = newState;
+                    updateReportRejectedTouchVisibility();
+                    mDozeServiceHost.updateDozing();
+                    updateTheme();
+                    mNavigationBarController.touchAutoDim(mDisplayId);
+                    Trace.beginSection("StatusBar#updateKeyguardState");
+                    if (mState == StatusBarState.KEYGUARD && mStatusBarView != null) {
+                        mNotificationPanelViewController.cancelPendingPanelCollapse();
+                    }
+                    updateDozingState();
+                    checkBarModes();
+                    updateScrimController();
+                    mPresenter.updateMediaMetaData(false, mState != StatusBarState.KEYGUARD);
+                    updateKeyguardState();
+                    Trace.endSection();
+                }
+
+                @Override
+                public void onDozeAmountChanged(float linear, float eased) {
+                    if (mFeatureFlags.useNewLockscreenAnimations()
+                            && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)
+                            && !mBiometricUnlockController.isWakeAndUnlock()) {
+                        mLightRevealScrim.setRevealAmount(1f - linear);
+                    }
+
+                    mDialogLaunchAnimator.onDozeAmountChanged(linear);
+                }
+
+                @Override
+                public void onDozingChanged(boolean isDozing) {
+                    Trace.beginSection("StatusBar#updateDozing");
+                    mDozing = isDozing;
+
+                    // Collapse the notification panel if open
+                    boolean dozingAnimated = mDozeServiceHost.getDozingRequested()
+                            && mDozeParameters.shouldControlScreenOff();
+                    mNotificationPanelViewController.resetViews(dozingAnimated);
+
+                    updateQsExpansionEnabled();
+                    mKeyguardViewMediator.setDozing(mDozing);
+
+                    mNotificationsController.requestNotificationUpdate("onDozingChanged");
+                    updateDozingState();
+                    mDozeServiceHost.updateDozing();
+                    updateScrimController();
+                    updateReportRejectedTouchVisibility();
+                    Trace.endSection();
+                }
+
+                @Override
+                public void onFullscreenStateChanged(boolean isFullscreen) {
+                    mIsFullscreen = isFullscreen;
+                    maybeUpdateBarMode();
+                }
+            };
+
+    private final BatteryController.BatteryStateChangeCallback mBatteryStateChangeCallback =
+            new BatteryController.BatteryStateChangeCallback() {
+                @Override
+                public void onPowerSaveChanged(boolean isPowerSave) {
+                    mMainExecutor.execute(mCheckBarModes);
+                    if (mDozeServiceHost != null) {
+                        mDozeServiceHost.firePowerSaveChanged(isPowerSave);
+                    }
+                }
+            };
+
+    private final ActivityLaunchAnimator.Callback mKeyguardHandler =
+            new ActivityLaunchAnimator.Callback() {
+                @Override
+                public boolean isOnKeyguard() {
+                    return mKeyguardStateController.isShowing();
+                }
+
+                @Override
+                public void hideKeyguardWithAnimation(IRemoteAnimationRunner runner) {
+                    // We post to the main thread for 2 reasons:
+                    //   1. KeyguardViewMediator is not thread-safe.
+                    //   2. To ensure that ViewMediatorCallback#keyguardDonePending is called before
+                    //      ViewMediatorCallback#readyForKeyguardDone. The wrong order could occur
+                    //      when doing
+                    //      dismissKeyguardThenExecute { hideKeyguardWithAnimation(runner) }.
+                    mMainExecutor.execute(() -> mKeyguardViewMediator.hideWithAnimation(runner));
+                }
+
+                @Override
+                public void setBlursDisabledForAppLaunch(boolean disabled) {
+                    mKeyguardViewMediator.setBlursDisabledForAppLaunch(disabled);
+                }
+
+                @Override
+                public int getBackgroundColor(TaskInfo task) {
+                    if (!mStartingSurfaceOptional.isPresent()) {
+                        Log.w(TAG, "No starting surface, defaulting to SystemBGColor");
+                        return SplashscreenContentDrawer.getSystemBGColor();
+                    }
+
+                    return mStartingSurfaceOptional.get().getBackgroundColor(task);
+                }
+            };
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarCommandQueueCallbacks.java
new file mode 100644
index 0000000..bb1daa2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarCommandQueueCallbacks.java
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.app.StatusBarManager.windowStateToString;
+import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.InsetsState.containsType;
+
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
+
+import android.annotation.Nullable;
+import android.app.StatusBarManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.media.AudioAttributes;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.util.Log;
+import android.util.Slog;
+import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
+import android.view.KeyEvent;
+import android.view.WindowInsetsController.Appearance;
+import android.view.WindowInsetsController.Behavior;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.view.AppearanceRegion;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.R;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.camera.CameraIntents;
+import com.android.systemui.dagger.qualifiers.DisplayId;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.DisableFlagsLogger;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+/** */
+@StatusBarComponent.StatusBarScope
+public class StatusBarCommandQueueCallbacks implements CommandQueue.Callbacks {
+    private final StatusBar mStatusBar;
+    private final Context mContext;
+    private final ShadeController mShadeController;
+    private final CommandQueue mCommandQueue;
+    private final NotificationPanelViewController mNotificationPanelViewController;
+    private final Optional<LegacySplitScreen> mSplitScreenOptional;
+    private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
+    private final MetricsLogger mMetricsLogger;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final KeyguardStateController mKeyguardStateController;
+    private final HeadsUpManager mHeadsUpManager;
+    private final WakefulnessLifecycle mWakefulnessLifecycle;
+    private final DeviceProvisionedController mDeviceProvisionedController;
+    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private final AssistManager mAssistManager;
+    private final DozeServiceHost mDozeServiceHost;
+    private final SysuiStatusBarStateController mStatusBarStateController;
+    private final NotificationShadeWindowView mNotificationShadeWindowView;
+    private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
+    private final PowerManager mPowerManager;
+    private final VibratorHelper mVibratorHelper;
+    private final Optional<Vibrator> mVibratorOptional;
+    private final LightBarController mLightBarController;
+    private final DisableFlagsLogger mDisableFlagsLogger;
+    private final int mDisplayId;
+    private final boolean mVibrateOnOpening;
+    private final VibrationEffect mCameraLaunchGestureVibrationEffect;
+
+
+    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+            .build();
+
+    @Inject
+    StatusBarCommandQueueCallbacks(
+            StatusBar statusBar,
+            Context context,
+            @Main Resources resources,
+            ShadeController shadeController,
+            CommandQueue commandQueue,
+            NotificationPanelViewController notificationPanelViewController,
+            Optional<LegacySplitScreen> splitScreenOptional,
+            RemoteInputQuickSettingsDisabler remoteInputQuickSettingsDisabler,
+            MetricsLogger metricsLogger,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            KeyguardStateController keyguardStateController,
+            HeadsUpManager headsUpManager,
+            WakefulnessLifecycle wakefulnessLifecycle,
+            DeviceProvisionedController deviceProvisionedController,
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            AssistManager assistManager,
+            DozeServiceHost dozeServiceHost,
+            SysuiStatusBarStateController statusBarStateController,
+            NotificationShadeWindowView notificationShadeWindowView,
+            NotificationStackScrollLayoutController notificationStackScrollLayoutController,
+            PowerManager powerManager,
+            VibratorHelper vibratorHelper,
+            Optional<Vibrator> vibratorOptional,
+            LightBarController lightBarController,
+            DisableFlagsLogger disableFlagsLogger,
+            @DisplayId int displayId) {
+
+        mStatusBar = statusBar;
+        mContext = context;
+        mShadeController = shadeController;
+        mCommandQueue = commandQueue;
+        mNotificationPanelViewController = notificationPanelViewController;
+        mSplitScreenOptional = splitScreenOptional;
+        mRemoteInputQuickSettingsDisabler = remoteInputQuickSettingsDisabler;
+        mMetricsLogger = metricsLogger;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mKeyguardStateController = keyguardStateController;
+        mHeadsUpManager = headsUpManager;
+        mWakefulnessLifecycle = wakefulnessLifecycle;
+        mDeviceProvisionedController = deviceProvisionedController;
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mAssistManager = assistManager;
+        mDozeServiceHost = dozeServiceHost;
+        mStatusBarStateController = statusBarStateController;
+        mNotificationShadeWindowView = notificationShadeWindowView;
+        mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
+        mPowerManager = powerManager;
+        mVibratorHelper = vibratorHelper;
+        mVibratorOptional = vibratorOptional;
+        mLightBarController = lightBarController;
+        mDisableFlagsLogger = disableFlagsLogger;
+        mDisplayId = displayId;
+
+        mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
+        mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
+                mVibratorOptional, resources);
+    }
+
+    @Override
+    public void abortTransient(int displayId, @InternalInsetsType int[] types) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        if (!containsType(types, ITYPE_STATUS_BAR)) {
+            return;
+        }
+        mStatusBar.clearTransient();
+    }
+
+    @Override
+    public void addQsTile(ComponentName tile) {
+        QSPanelController qsPanelController = mStatusBar.getQSPanelController();
+        if (qsPanelController != null && qsPanelController.getHost() != null) {
+            qsPanelController.getHost().addTile(tile);
+        }
+    }
+
+    @Override
+    public void remQsTile(ComponentName tile) {
+        QSPanelController qsPanelController = mStatusBar.getQSPanelController();
+        if (qsPanelController != null && qsPanelController.getHost() != null) {
+            qsPanelController.getHost().removeTile(tile);
+        }
+    }
+
+    @Override
+    public void clickTile(ComponentName tile) {
+        QSPanelController qsPanelController = mStatusBar.getQSPanelController();
+        if (qsPanelController != null) {
+            qsPanelController.clickTile(tile);
+        }
+    }
+
+    @Override
+    public void animateCollapsePanels(int flags, boolean force) {
+        mShadeController.animateCollapsePanels(flags, force, false /* delayed */,
+                1.0f /* speedUpFactor */);
+    }
+
+    @Override
+    public void animateExpandNotificationsPanel() {
+        if (StatusBar.SPEW) {
+            Log.d(StatusBar.TAG,
+                    "animateExpand: mExpandedVisible=" + mStatusBar.isExpandedVisible());
+        }
+        if (!mCommandQueue.panelsEnabled()) {
+            return;
+        }
+
+        mNotificationPanelViewController.expandWithoutQs();
+    }
+
+    @Override
+    public void animateExpandSettingsPanel(@Nullable String subPanel) {
+        if (StatusBar.SPEW) {
+            Log.d(StatusBar.TAG,
+                    "animateExpand: mExpandedVisible=" + mStatusBar.isExpandedVisible());
+        }
+        if (!mCommandQueue.panelsEnabled()) {
+            return;
+        }
+
+        // Settings are not available in setup
+        if (!mDeviceProvisionedController.isCurrentUserSetup()) return;
+
+
+        QSPanelController qsPanelController = mStatusBar.getQSPanelController();
+        if (subPanel != null && qsPanelController != null) {
+            qsPanelController.openDetails(subPanel);
+        }
+        mNotificationPanelViewController.expandWithQs();
+    }
+
+    @Override
+    public void appTransitionCancelled(int displayId) {
+        if (displayId == mDisplayId) {
+            mSplitScreenOptional.ifPresent(LegacySplitScreen::onAppTransitionFinished);
+        }
+    }
+
+    @Override
+    public void appTransitionFinished(int displayId) {
+        if (displayId == mDisplayId) {
+            mSplitScreenOptional.ifPresent(LegacySplitScreen::onAppTransitionFinished);
+        }
+    }
+
+    @Override
+    public void dismissKeyboardShortcutsMenu() {
+        mStatusBar.resendMessage(StatusBar.MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU);
+    }
+    /**
+     * State is one or more of the DISABLE constants from StatusBarManager.
+     */
+    @Override
+    public void disable(int displayId, int state1, int state2, boolean animate) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+
+        int state2BeforeAdjustment = state2;
+        state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
+        Log.d(StatusBar.TAG,
+                mDisableFlagsLogger.getDisableFlagsString(
+                        /* old= */ new DisableFlagsLogger.DisableState(
+                                mStatusBar.getDisabled1(), mStatusBar.getDisabled2()),
+                        /* new= */ new DisableFlagsLogger.DisableState(
+                                state1, state2BeforeAdjustment),
+                        /* newStateAfterLocalModification= */ new DisableFlagsLogger.DisableState(
+                                state1, state2)));
+
+        final int old1 = mStatusBar.getDisabled1();
+        final int diff1 = state1 ^ old1;
+        mStatusBar.setDisabled1(state1);
+
+        final int old2 = mStatusBar.getDisabled2();
+        final int diff2 = state2 ^ old2;
+        mStatusBar.setDisabled2(state2);
+
+        if ((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) {
+            if ((state1 & StatusBarManager.DISABLE_EXPAND) != 0) {
+                mShadeController.animateCollapsePanels();
+            }
+        }
+
+        if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
+            if (mStatusBar.areNotificationAlertsDisabled()) {
+                mHeadsUpManager.releaseAllImmediately();
+            }
+        }
+
+        if ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) {
+            mStatusBar.updateQsExpansionEnabled();
+        }
+
+        if ((diff2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
+            mStatusBar.updateQsExpansionEnabled();
+            if ((state2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
+                mShadeController.animateCollapsePanels();
+            }
+        }
+    }
+
+    /**
+     * Called for system navigation gestures. First action opens the panel, second opens
+     * settings. Down action closes the entire panel.
+     */
+    @Override
+    public void handleSystemKey(int key) {
+        if (StatusBar.SPEW) {
+            Log.d(StatusBar.TAG, "handleNavigationKey: " + key);
+        }
+        if (!mCommandQueue.panelsEnabled() || !mKeyguardUpdateMonitor.isDeviceInteractive()
+                || mKeyguardStateController.isShowing() && !mKeyguardStateController.isOccluded()) {
+            return;
+        }
+
+        // Panels are not available in setup
+        if (!mDeviceProvisionedController.isCurrentUserSetup()) return;
+
+        if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP == key) {
+            mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_UP);
+            mNotificationPanelViewController.collapse(
+                    false /* delayed */, 1.0f /* speedUpFactor */);
+        } else if (KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN == key) {
+            mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_DOWN);
+            if (mNotificationPanelViewController.isFullyCollapsed()) {
+                if (mVibrateOnOpening) {
+                    mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+                }
+                mNotificationPanelViewController.expand(true /* animate */);
+                mNotificationStackScrollLayoutController.setWillExpand(true);
+                mHeadsUpManager.unpinAll(true /* userUnpinned */);
+                mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN, 1);
+            } else if (!mNotificationPanelViewController.isInSettings()
+                    && !mNotificationPanelViewController.isExpanding()) {
+                mNotificationPanelViewController.flingSettings(0 /* velocity */,
+                        NotificationPanelView.FLING_EXPAND);
+                mMetricsLogger.count(NotificationPanelView.COUNTER_PANEL_OPEN_QS, 1);
+            }
+        }
+
+    }
+
+    @Override
+    public void onCameraLaunchGestureDetected(int source) {
+        mStatusBar.setLastCameraLaunchSource(source);
+        if (mStatusBar.isGoingToSleep()) {
+            if (StatusBar.DEBUG_CAMERA_LIFT) {
+                Slog.d(StatusBar.TAG, "Finish going to sleep before launching camera");
+            }
+            mStatusBar.setLaunchCameraOnFinishedGoingToSleep(true);
+            return;
+        }
+        if (!mNotificationPanelViewController.canCameraGestureBeLaunched()) {
+            if (StatusBar.DEBUG_CAMERA_LIFT) {
+                Slog.d(StatusBar.TAG, "Can't launch camera right now");
+            }
+            return;
+        }
+        if (!mStatusBar.isDeviceInteractive()) {
+            mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_CAMERA_LAUNCH,
+                    "com.android.systemui:CAMERA_GESTURE");
+        }
+        vibrateForCameraGesture();
+
+        if (source == StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) {
+            Log.v(StatusBar.TAG, "Camera launch");
+            mKeyguardUpdateMonitor.onCameraLaunched();
+        }
+
+        if (!mStatusBarKeyguardViewManager.isShowing()) {
+            final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
+            mStatusBar.startActivityDismissingKeyguard(cameraIntent,
+                    false /* onlyProvisioned */, true /* dismissShade */,
+                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
+                    null /* animationController */);
+        } else {
+            if (!mStatusBar.isDeviceInteractive()) {
+                // Avoid flickering of the scrim when we instant launch the camera and the bouncer
+                // comes on.
+                mStatusBar.acquireGestureWakeLock(StatusBar.LAUNCH_TRANSITION_TIMEOUT_MS + 1000L);
+            }
+            if (isWakingUpOrAwake()) {
+                if (StatusBar.DEBUG_CAMERA_LIFT) {
+                    Slog.d(StatusBar.TAG, "Launching camera");
+                }
+                if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+                    mStatusBarKeyguardViewManager.reset(true /* hide */);
+                }
+                mNotificationPanelViewController.launchCamera(
+                        mStatusBar.isDeviceInteractive() /* animate */, source);
+                mStatusBar.updateScrimController();
+            } else {
+                // We need to defer the camera launch until the screen comes on, since otherwise
+                // we will dismiss us too early since we are waiting on an activity to be drawn and
+                // incorrectly get notified because of the screen on event (which resumes and pauses
+                // some activities)
+                if (StatusBar.DEBUG_CAMERA_LIFT) {
+                    Slog.d(StatusBar.TAG, "Deferring until screen turns on");
+                }
+                mStatusBar.setLaunchCameraOnFinishedWaking(true);
+            }
+        }
+    }
+
+    @Override
+    public void onEmergencyActionLaunchGestureDetected() {
+        Intent emergencyIntent = mStatusBar.getEmergencyActionIntent();
+
+        if (emergencyIntent == null) {
+            Log.wtf(StatusBar.TAG, "Couldn't find an app to process the emergency intent.");
+            return;
+        }
+
+        if (isGoingToSleep()) {
+            mStatusBar.setLaunchEmergencyActionOnFinishedGoingToSleep(true);
+            return;
+        }
+
+        if (!mStatusBar.isDeviceInteractive()) {
+            mPowerManager.wakeUp(SystemClock.uptimeMillis(),
+                    PowerManager.WAKE_REASON_GESTURE,
+                    "com.android.systemui:EMERGENCY_GESTURE");
+        }
+        // TODO(b/169087248) Possibly add haptics here for emergency action. Currently disabled for
+        // app-side haptic experimentation.
+
+        if (!mStatusBarKeyguardViewManager.isShowing()) {
+            mStatusBar.startActivityDismissingKeyguard(emergencyIntent,
+                    false /* onlyProvisioned */, true /* dismissShade */,
+                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
+                    null /* animationController */);
+            return;
+        }
+
+        if (!mStatusBar.isDeviceInteractive()) {
+            // Avoid flickering of the scrim when we instant launch the camera and the bouncer
+            // comes on.
+            mStatusBar.acquireGestureWakeLock(StatusBar.LAUNCH_TRANSITION_TIMEOUT_MS + 1000L);
+        }
+
+        if (isWakingUpOrAwake()) {
+            if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+                mStatusBarKeyguardViewManager.reset(true /* hide */);
+            }
+            mContext.startActivityAsUser(emergencyIntent, UserHandle.CURRENT);
+            return;
+        }
+        // We need to defer the emergency action launch until the screen comes on, since otherwise
+        // we will dismiss us too early since we are waiting on an activity to be drawn and
+        // incorrectly get notified because of the screen on event (which resumes and pauses
+        // some activities)
+        mStatusBar.setLaunchEmergencyActionOnFinishedWaking(true);
+    }
+
+    @Override
+    public void onRecentsAnimationStateChanged(boolean running) {
+        mStatusBar.setInteracting(StatusBarManager.WINDOW_NAVIGATION_BAR, running);
+    }
+
+
+    @Override
+    public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
+            AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
+            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        boolean barModeChanged = mStatusBar.setAppearance(appearance);
+
+        mLightBarController.onStatusBarAppearanceChanged(appearanceRegions, barModeChanged,
+                mStatusBar.getBarMode(), navbarColorManagedByIme);
+
+        mStatusBar.updateBubblesVisibility();
+        mStatusBarStateController.setSystemBarAttributes(
+                appearance, behavior, requestedVisibilities, packageName);
+    }
+
+    @Override
+    public void showTransient(int displayId, @InternalInsetsType int[] types) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        if (!containsType(types, ITYPE_STATUS_BAR)) {
+            return;
+        }
+        mStatusBar.showTransientUnchecked();
+    }
+
+    @Override
+    public void toggleKeyboardShortcutsMenu(int deviceId) {
+        mStatusBar.resendMessage(new StatusBar.KeyboardShortcutsMessage(deviceId));
+    }
+
+    @Override
+    public void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) {
+        mStatusBar.setTopHidesStatusBar(topAppHidesStatusBar);
+        if (!topAppHidesStatusBar && mStatusBar.getWereIconsJustHidden()) {
+            // Immediately update the icon hidden state, since that should only apply if we're
+            // staying fullscreen.
+            mStatusBar.setWereIconsJustHidden(false);
+            mCommandQueue.recomputeDisableFlags(mDisplayId, true);
+        }
+        mStatusBar.updateHideIconsForBouncer(true /* animate */);
+    }
+
+    @Override
+    public void setWindowState(
+            int displayId, @StatusBarManager.WindowType int window,
+            @StatusBarManager.WindowVisibleState int state) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        boolean showing = state == WINDOW_STATE_SHOWING;
+        if (mNotificationShadeWindowView != null
+                && window == StatusBarManager.WINDOW_STATUS_BAR
+                && !mStatusBar.isSameStatusBarState(state)) {
+            mStatusBar.setWindowState(state);
+            if (StatusBar.DEBUG_WINDOW_STATE) {
+                Log.d(StatusBar.TAG, "Status bar " + windowStateToString(state));
+            }
+            if (mStatusBar.getStatusBarView() != null) {
+                if (!showing && mStatusBarStateController.getState() == StatusBarState.SHADE) {
+                    mNotificationPanelViewController.collapsePanel(
+                            false /* animate */, false /* delayed */, 1.0f /* speedUpFactor */);
+                }
+
+                mStatusBar.updateHideIconsForBouncer(false /* animate */);
+            }
+        }
+
+        mStatusBar.updateBubblesVisibility();
+    }
+
+    @Override
+    public void showAssistDisclosure() {
+        mAssistManager.showDisclosure();
+    }
+
+    @Override
+    public void showPinningEnterExitToast(boolean entering) {
+        if (mStatusBar.getNavigationBarView() != null) {
+            mStatusBar.getNavigationBarView().showPinningEnterExitToast(entering);
+        }
+    }
+
+    @Override
+    public void showPinningEscapeToast() {
+        if (mStatusBar.getNavigationBarView() != null) {
+            mStatusBar.getNavigationBarView().showPinningEscapeToast();
+        }
+    }
+
+    @Override
+    public void showScreenPinningRequest(int taskId) {
+        if (mKeyguardStateController.isShowing()) {
+            // Don't allow apps to trigger this from keyguard.
+            return;
+        }
+        // Show screen pinning request, since this comes from an app, show 'no thanks', button.
+        mStatusBar.showScreenPinningRequest(taskId, true);
+    }
+
+    @Override
+    public void showWirelessChargingAnimation(int batteryLevel) {
+        mStatusBar.showWirelessChargingAnimation(batteryLevel);
+    }
+
+    @Override
+    public void startAssist(Bundle args) {
+        mAssistManager.startAssist(args);
+    }
+
+    @Override
+    public void suppressAmbientDisplay(boolean suppressed) {
+        mDozeServiceHost.setDozeSuppressed(suppressed);
+    }
+
+    @Override
+    public void togglePanel() {
+        if (mStatusBar.isPanelExpanded()) {
+            mShadeController.animateCollapsePanels();
+        } else {
+            animateExpandNotificationsPanel();
+        }
+    }
+
+    @Override
+    public void toggleSplitScreen() {
+        mStatusBar.toggleSplitScreenMode(-1 /* metricsDockAction */, -1 /* metricsUndockAction */);
+    }
+
+    private boolean isGoingToSleep() {
+        return mWakefulnessLifecycle.getWakefulness()
+                == WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
+    }
+
+    private boolean isWakingUpOrAwake() {
+        return mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
+                || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING;
+    }
+
+    private void vibrateForCameraGesture() {
+        // Make sure to pass -1 for repeat so VibratorService doesn't stop us when going to sleep.
+        mVibratorOptional.ifPresent(
+                v -> v.vibrate(mCameraLaunchGestureVibrationEffect, VIBRATION_ATTRIBUTES));
+    }
+
+    private static VibrationEffect getCameraGestureVibrationEffect(
+            Optional<Vibrator> vibratorOptional, Resources resources) {
+        if (vibratorOptional.isPresent() && vibratorOptional.get().areAllPrimitivesSupported(
+                VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+                VibrationEffect.Composition.PRIMITIVE_CLICK)) {
+            return VibrationEffect.startComposition()
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE)
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 50)
+                    .compose();
+        }
+        if (vibratorOptional.isPresent() && vibratorOptional.get().hasAmplitudeControl()) {
+            return VibrationEffect.createWaveform(
+                    StatusBar.CAMERA_LAUNCH_GESTURE_VIBRATION_TIMINGS,
+                    StatusBar.CAMERA_LAUNCH_GESTURE_VIBRATION_AMPLITUDES,
+                    /* repeat= */ -1);
+        }
+
+        int[] pattern = resources.getIntArray(R.array.config_cameraLaunchGestureVibePattern);
+        long[] timings = new long[pattern.length];
+        for (int i = 0; i < pattern.length; i++) {
+            timings[i] = pattern[i];
+        }
+        return VibrationEffect.createWaveform(timings, /* repeat= */ -1);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
index fe1f63a..5bfb236 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
@@ -19,11 +19,10 @@
 import android.content.Context
 import android.content.res.Resources
 import android.graphics.Rect
-import android.util.Log
+import android.util.LruCache
 import android.util.Pair
 import android.view.DisplayCutout
 import android.view.View.LAYOUT_DIRECTION_RTL
-import android.view.WindowManager
 import android.view.WindowMetrics
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.Dumpable
@@ -38,6 +37,7 @@
 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
 import com.android.systemui.util.leak.RotationUtils.Rotation
+import com.android.systemui.util.leak.RotationUtils.getResourcesForRotation
 import java.io.FileDescriptor
 import java.io.PrintWriter
 import java.lang.Math.max
@@ -61,14 +61,18 @@
 class StatusBarContentInsetsProvider @Inject constructor(
     val context: Context,
     val configurationController: ConfigurationController,
-    val windowManager: WindowManager,
     val dumpManager: DumpManager
 ) : CallbackController<StatusBarContentInsetsChangedListener>,
         ConfigurationController.ConfigurationListener,
         Dumpable {
-    // Indexed by @Rotation
-    private val insetsByCorner = arrayOfNulls<Rect>(4)
+
+    // Limit cache size as potentially we may connect large number of displays
+    // (e.g. network displays)
+    private val insetsCache = LruCache<CacheKey, Rect>(MAX_CACHE_SIZE)
     private val listeners = mutableSetOf<StatusBarContentInsetsChangedListener>()
+    private val isPrivacyDotEnabled: Boolean by lazy(LazyThreadSafetyMode.PUBLICATION) {
+        context.resources.getBoolean(R.bool.config_enablePrivacyDot)
+    }
 
     init {
         configurationController.addCallback(this)
@@ -87,16 +91,16 @@
         clearCachedInsets()
     }
 
-    override fun onOverlayChanged() {
+    override fun onThemeChanged() {
         clearCachedInsets()
     }
 
-    private fun clearCachedInsets() {
-        insetsByCorner[0] = null
-        insetsByCorner[1] = null
-        insetsByCorner[2] = null
-        insetsByCorner[3] = null
+    override fun onMaxBoundsChanged() {
+        notifyInsetsChanged()
+    }
 
+    private fun clearCachedInsets() {
+        insetsCache.evictAll()
         notifyInsetsChanged()
     }
 
@@ -111,10 +115,10 @@
      * dot in the coordinates relative to the given rotation.
      */
     fun getBoundingRectForPrivacyChipForRotation(@Rotation rotation: Int): Rect {
-        var insets = insetsByCorner[rotation]
-        val rotatedResources = RotationUtils.getResourcesForRotation(rotation, context)
+        var insets = insetsCache[getCacheKey(rotation = rotation)]
+        val rotatedResources = getResourcesForRotation(rotation, context)
         if (insets == null) {
-            insets = getAndSetInsetsForRotation(rotation, rotatedResources)
+            insets = getStatusBarContentInsetsForRotation(rotation, rotatedResources)
         }
 
         val dotWidth = rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
@@ -129,24 +133,16 @@
      * Calculates the necessary left and right locations for the status bar contents invariant of
      * the current device rotation, in the target rotation's coordinates
      */
-    fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Rect {
-        var insets = insetsByCorner[rotation]
-        if (insets == null) {
-            val rotatedResources = RotationUtils.getResourcesForRotation(rotation, context)
-            insets = getAndSetInsetsForRotation(rotation, rotatedResources)
-        }
-
-        return insets
-    }
-
-    private fun getAndSetInsetsForRotation(
-        @Rotation rot: Int,
-        rotatedResources: Resources
+    @JvmOverloads
+    fun getStatusBarContentInsetsForRotation(
+        @Rotation rotation: Int,
+        rotatedResources: Resources = getResourcesForRotation(rotation, context)
     ): Rect {
-        val insets = getCalculatedInsetsForRotation(rot, rotatedResources)
-        insetsByCorner[rot] = insets
-
-        return insets
+        val key = getCacheKey(rotation = rotation)
+        return insetsCache[key] ?: getCalculatedInsetsForRotation(rotation, rotatedResources)
+            .also {
+                insetsCache.put(key, it)
+            }
     }
 
     private fun getCalculatedInsetsForRotation(
@@ -159,8 +155,9 @@
         val isRtl = rotatedResources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL
         val roundedCornerPadding = rotatedResources
                 .getDimensionPixelSize(R.dimen.rounded_corner_content_padding)
-        val minDotWidth = rotatedResources
-                .getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding)
+        val minDotWidth = if (isPrivacyDotEnabled)
+                rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding)
+            else 0
 
         val minLeft: Int
         val minRight: Int
@@ -176,17 +173,34 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowManager.maximumWindowMetrics,
+                context.resources.configuration.windowConfiguration.maxBounds,
                 rotatedResources.getDimensionPixelSize(R.dimen.status_bar_height),
                 minLeft,
                 minRight)
     }
 
-    override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
-        insetsByCorner.forEachIndexed { index, rect ->
-            pw.println("${RotationUtils.toString(index)} -> $rect")
-        }
+    fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int {
+        val res = rotation?.let { it -> getResourcesForRotation(it, context) } ?: context.resources
+        return res.getDimensionPixelSize(R.dimen.status_bar_padding_top)
     }
+
+    override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+        insetsCache.snapshot().forEach { (key, rect) ->
+            pw.println("$key -> $rect")
+        }
+        pw.println(insetsCache)
+    }
+
+    private fun getCacheKey(@Rotation rotation: Int): CacheKey =
+        CacheKey(
+            uniqueDisplayId = context.display.uniqueId,
+            rotation = rotation
+        )
+
+    private data class CacheKey(
+        val uniqueDisplayId: String,
+        @Rotation val rotation: Int
+    )
 }
 
 interface StatusBarContentInsetsChangedListener {
@@ -194,10 +208,9 @@
 }
 
 private const val TAG = "StatusBarInsetsProvider"
+private const val MAX_CACHE_SIZE = 16
 
-private fun getRotationZeroDisplayBounds(wm: WindowMetrics, @Rotation exactRotation: Int): Rect {
-    val bounds = wm.bounds
-
+private fun getRotationZeroDisplayBounds(bounds: Rect, @Rotation exactRotation: Int): Rect {
     if (exactRotation == ROTATION_NONE || exactRotation == ROTATION_UPSIDE_DOWN) {
         return bounds
     }
@@ -243,7 +256,7 @@
     @Rotation currentRotation: Int,
     @Rotation targetRotation: Int,
     displayCutout: DisplayCutout?,
-    windowMetrics: WindowMetrics,
+    maxBounds: Rect,
     statusBarHeight: Int,
     minLeft: Int,
     minRight: Int
@@ -254,16 +267,15 @@
     val right = if (isRtl) paddingStart else paddingEnd
      */
 
-    val rotZeroBounds = getRotationZeroDisplayBounds(windowMetrics, currentRotation)
-    val currentBounds = windowMetrics.bounds
+    val rotZeroBounds = getRotationZeroDisplayBounds(maxBounds, currentRotation)
 
     val sbLeftRight = getStatusBarLeftRight(
             displayCutout,
             statusBarHeight,
             rotZeroBounds.right,
             rotZeroBounds.bottom,
-            currentBounds.width(),
-            currentBounds.height(),
+            maxBounds.width(),
+            maxBounds.height(),
             minLeft,
             minRight,
             targetRotation,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
new file mode 100644
index 0000000..e642b2e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.DisplayId;
+import com.android.systemui.demomode.DemoMode;
+import com.android.systemui.demomode.DemoModeCommandReceiver;
+import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/** */
+@StatusBarComponent.StatusBarScope
+public class StatusBarDemoMode implements DemoMode {
+    private final StatusBar mStatusBar;
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
+    private final NotificationShadeWindowViewController mNotificationShadeWindowViewController;
+    private final NavigationBarController mNavigationBarController;
+    private final int mDisplayId;
+
+    @Inject
+    StatusBarDemoMode(
+            StatusBar statusBar,
+            NotificationShadeWindowController notificationShadeWindowController,
+            NotificationShadeWindowViewController notificationShadeWindowViewController,
+            NavigationBarController navigationBarController,
+            @DisplayId int displayId) {
+        mStatusBar = statusBar;
+        mNotificationShadeWindowController = notificationShadeWindowController;
+        mNotificationShadeWindowViewController = notificationShadeWindowViewController;
+        mNavigationBarController = navigationBarController;
+        mDisplayId = displayId;
+    }
+
+    @Override
+    public List<String> demoCommands() {
+        List<String> s = new ArrayList<>();
+        s.add(DemoMode.COMMAND_BARS);
+        s.add(DemoMode.COMMAND_CLOCK);
+        s.add(DemoMode.COMMAND_OPERATOR);
+        return s;
+    }
+
+    @Override
+    public void onDemoModeStarted() {
+        // Must send this message to any view that we delegate to via dispatchDemoCommandToView
+        dispatchDemoModeStartedToView(R.id.clock);
+        dispatchDemoModeStartedToView(R.id.operator_name);
+    }
+
+    @Override
+    public void onDemoModeFinished() {
+        dispatchDemoModeFinishedToView(R.id.clock);
+        dispatchDemoModeFinishedToView(R.id.operator_name);
+        mStatusBar.checkBarModes();
+    }
+
+    @Override
+    public void dispatchDemoCommand(String command, @NonNull Bundle args) {
+        if (command.equals(COMMAND_CLOCK)) {
+            dispatchDemoCommandToView(command, args, R.id.clock);
+        }
+        if (command.equals(COMMAND_BARS)) {
+            String mode = args.getString("mode");
+            int barMode = "opaque".equals(mode) ? MODE_OPAQUE :
+                    "translucent".equals(mode) ? MODE_TRANSLUCENT :
+                            "semi-transparent".equals(mode) ? MODE_SEMI_TRANSPARENT :
+                                    "transparent".equals(mode) ? MODE_TRANSPARENT :
+                                            "warning".equals(mode) ? MODE_WARNING :
+                                                    -1;
+            if (barMode != -1) {
+                boolean animate = true;
+                if (mNotificationShadeWindowController != null
+                        && mNotificationShadeWindowViewController.getBarTransitions() != null) {
+                    mNotificationShadeWindowViewController.getBarTransitions().transitionTo(
+                            barMode, animate);
+                }
+                mNavigationBarController.transitionTo(mDisplayId, barMode, animate);
+            }
+        }
+        if (command.equals(COMMAND_OPERATOR)) {
+            dispatchDemoCommandToView(command, args, R.id.operator_name);
+        }
+    }
+
+    private void dispatchDemoModeStartedToView(int id) {
+        View statusBarView = mStatusBar.getStatusBarView();
+        if (statusBarView == null) return;
+        View v = statusBarView.findViewById(id);
+        if (v instanceof DemoModeCommandReceiver) {
+            ((DemoModeCommandReceiver) v).onDemoModeStarted();
+        }
+    }
+
+    //TODO: these should have controllers, and this method should be removed
+    private void dispatchDemoCommandToView(String command, Bundle args, int id) {
+        View statusBarView = mStatusBar.getStatusBarView();
+        if (statusBarView == null) return;
+        View v = statusBarView.findViewById(id);
+        if (v instanceof DemoModeCommandReceiver) {
+            ((DemoModeCommandReceiver) v).dispatchDemoCommand(command, args);
+        }
+    }
+
+    private void dispatchDemoModeFinishedToView(int id) {
+        View statusBarView = mStatusBar.getStatusBarView();
+        if (statusBarView == null) return;
+        View v = statusBarView.findViewById(id);
+        if (v instanceof DemoModeCommandReceiver) {
+            ((DemoModeCommandReceiver) v).onDemoModeFinished();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
new file mode 100644
index 0000000..ca877af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.init.NotificationsController;
+import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import javax.inject.Inject;
+
+/** Ties the {@link StatusBar} to {@link com.android.systemui.statusbar.policy.HeadsUpManager}. */
+@StatusBarComponent.StatusBarScope
+public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener {
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
+    private final StatusBarWindowController mStatusBarWindowController;
+    private final NotificationPanelViewController mNotificationPanelViewController;
+    private final KeyguardBypassController mKeyguardBypassController;
+    private final HeadsUpManagerPhone mHeadsUpManager;
+    private final StatusBarStateController mStatusBarStateController;
+    private final NotificationRemoteInputManager mNotificationRemoteInputManager;
+    private final NotificationsController mNotificationsController;
+    private final DozeServiceHost mDozeServiceHost;
+    private final DozeScrimController mDozeScrimController;
+
+    @Inject
+    StatusBarHeadsUpChangeListener(
+            NotificationShadeWindowController notificationShadeWindowController,
+            StatusBarWindowController statusBarWindowController,
+            NotificationPanelViewController notificationPanelViewController,
+            KeyguardBypassController keyguardBypassController,
+            HeadsUpManagerPhone headsUpManager,
+            StatusBarStateController statusBarStateController,
+            NotificationRemoteInputManager notificationRemoteInputManager,
+            NotificationsController notificationsController,
+            DozeServiceHost dozeServiceHost,
+            DozeScrimController dozeScrimController) {
+
+        mNotificationShadeWindowController = notificationShadeWindowController;
+        mStatusBarWindowController = statusBarWindowController;
+        mNotificationPanelViewController = notificationPanelViewController;
+        mKeyguardBypassController = keyguardBypassController;
+        mHeadsUpManager = headsUpManager;
+        mStatusBarStateController = statusBarStateController;
+        mNotificationRemoteInputManager = notificationRemoteInputManager;
+        mNotificationsController = notificationsController;
+        mDozeServiceHost = dozeServiceHost;
+        mDozeScrimController = dozeScrimController;
+    }
+
+    @Override
+    public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+        if (inPinnedMode) {
+            mNotificationShadeWindowController.setHeadsUpShowing(true);
+            mStatusBarWindowController.setForceStatusBarVisible(true);
+            if (mNotificationPanelViewController.isFullyCollapsed()) {
+                // We need to ensure that the touchable region is updated before the
+                //window will be
+                // resized, in order to not catch any touches. A layout will ensure that
+                // onComputeInternalInsets will be called and after that we can
+                //resize the layout. Let's
+                // make sure that the window stays small for one frame until the
+                //touchableRegion is set.
+                mNotificationPanelViewController.getView().requestLayout();
+                mNotificationShadeWindowController.setForceWindowCollapsed(true);
+                mNotificationPanelViewController.getView().post(() -> {
+                    mNotificationShadeWindowController.setForceWindowCollapsed(false);
+                });
+            }
+        } else {
+            boolean bypassKeyguard = mKeyguardBypassController.getBypassEnabled()
+                    && mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
+            if (!mNotificationPanelViewController.isFullyCollapsed()
+                    || mNotificationPanelViewController.isTracking()
+                    || bypassKeyguard) {
+                // We are currently tracking or is open and the shade doesn't need to
+                //be kept
+                // open artificially.
+                mNotificationShadeWindowController.setHeadsUpShowing(false);
+                if (bypassKeyguard) {
+                    mStatusBarWindowController.setForceStatusBarVisible(false);
+                }
+            } else {
+                // we need to keep the panel open artificially, let's wait until the
+                //animation
+                // is finished.
+                mHeadsUpManager.setHeadsUpGoingAway(true);
+                mNotificationPanelViewController.runAfterAnimationFinished(() -> {
+                    if (!mHeadsUpManager.hasPinnedHeadsUp()) {
+                        mNotificationShadeWindowController.setHeadsUpShowing(false);
+                        mHeadsUpManager.setHeadsUpGoingAway(false);
+                    }
+                    mNotificationRemoteInputManager.onPanelCollapsed();
+                });
+            }
+        }
+    }
+
+    @Override
+    public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
+        mNotificationsController.requestNotificationUpdate("onHeadsUpStateChanged");
+        if (mStatusBarStateController.isDozing() && isHeadsUp) {
+            entry.setPulseSuppressed(false);
+            mDozeServiceHost.fireNotificationPulse(entry);
+            if (mDozeServiceHost.isPulsing()) {
+                mDozeScrimController.cancelPendingPulseTimeout();
+            }
+        }
+        if (!isHeadsUp && !mHeadsUpManager.hasNotifications()) {
+            // There are no longer any notifications to show.  We should end the
+            //pulse now.
+            mDozeScrimController.pulseOutNow();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 2c75534..48fe774 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -35,10 +35,11 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.demomode.DemoModeCommandReceiver;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarMobileView;
 import com.android.systemui.statusbar.StatusBarWifiView;
@@ -50,6 +51,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.inject.Inject;
+
 public interface StatusBarIconController {
 
     /**
@@ -213,6 +216,20 @@
             icons.setColor(mColor);
             return icons;
         }
+
+        @SysUISingleton
+        public static class Factory {
+            private final FeatureFlags mFeatureFlags;
+
+            @Inject
+            public Factory(FeatureFlags featureFlags) {
+                mFeatureFlags = featureFlags;
+            }
+
+            public TintedIconManager create(ViewGroup group) {
+                return new TintedIconManager(group, mFeatureFlags);
+            }
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 75900a2..88a7dc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -23,6 +23,7 @@
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Log;
 import android.view.ViewGroup;
 
 import com.android.internal.statusbar.StatusBarIcon;
@@ -32,6 +33,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusIconDisplayable;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
@@ -71,7 +73,8 @@
     public StatusBarIconControllerImpl(
             Context context,
             CommandQueue commandQueue,
-            DemoModeController demoModeController) {
+            DemoModeController demoModeController,
+            DumpManager dumpManager) {
         super(context.getResources().getStringArray(
                 com.android.internal.R.array.config_statusBarIcons));
         Dependency.get(ConfigurationController.class).addCallback(this);
@@ -83,11 +86,19 @@
         commandQueue.addCallback(this);
         Dependency.get(TunerService.class).addTunable(this, ICON_HIDE_LIST);
         demoModeController.addCallback(this);
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     /** */
     @Override
     public void addIconGroup(IconManager group) {
+        for (IconManager existingIconManager : mIconGroups) {
+            if (existingIconManager.mGroup == group.mGroup) {
+                Log.e(TAG, "Adding new IconManager for the same ViewGroup. This could cause "
+                        + "unexpected results.");
+            }
+        }
+
         mIconGroups.add(group);
         List<Slot> allSlots = getSlots();
         for (int i = 0; i < allSlots.size(); i++) {
@@ -103,6 +114,14 @@
         }
     }
 
+    private void refreshIconGroups() {
+        for (int i = mIconGroups.size() - 1; i >= 0; --i) {
+            IconManager group = mIconGroups.get(i);
+            removeIconGroup(group);
+            addIconGroup(group);
+        }
+    }
+
     /** */
     @Override
     public void removeIconGroup(IconManager group) {
@@ -457,5 +476,6 @@
     @Override
     public void onDensityOrFontScaleChanged() {
         loadDimens();
+        refreshIconGroups();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 1e3e47b..07489a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -74,6 +74,8 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /**
  * Manages creating, showing, hiding and resetting the keyguard within the status bar. Calls back
  * via {@link ViewMediatorCallback} to poke the wake lock and report that the keyguard is done,
@@ -113,11 +115,13 @@
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private final KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
     private KeyguardMessageAreaController mKeyguardMessageAreaController;
+    private final Lazy<ShadeController> mShadeController;
     private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() {
         @Override
         public void onFullyShown() {
             updateStates();
-            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mContainer, "BOUNCER_VISIBLE");
+            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(),
+                    mStatusBar.getBouncerContainer(), "BOUNCER_VISIBLE");
         }
 
         @Override
@@ -171,7 +175,6 @@
     private NotificationPanelViewController mNotificationPanelViewController;
     private BiometricUnlockController mBiometricUnlockController;
 
-    private ViewGroup mContainer;
     private View mNotificationContainer;
 
     protected KeyguardBouncer mBouncer;
@@ -244,7 +247,8 @@
             KeyguardBouncer.Factory keyguardBouncerFactory,
             WakefulnessLifecycle wakefulnessLifecycle,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
-            KeyguardMessageAreaController.Factory keyguardMessageAreaFactory) {
+            KeyguardMessageAreaController.Factory keyguardMessageAreaFactory,
+            Lazy<ShadeController> shadeController) {
         mContext = context;
         mViewMediatorCallback = callback;
         mLockPatternUtils = lockPatternUtils;
@@ -261,18 +265,19 @@
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
         mKeyguardMessageAreaFactory = keyguardMessageAreaFactory;
+        mShadeController = shadeController;
     }
 
     @Override
     public void registerStatusBar(StatusBar statusBar,
-            ViewGroup container,
             NotificationPanelViewController notificationPanelViewController,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
             KeyguardBypassController bypassController) {
         mStatusBar = statusBar;
-        mContainer = container;
         mBiometricUnlockController = biometricUnlockController;
+
+        ViewGroup container = mStatusBar.getBouncerContainer();
         mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
         mNotificationPanelViewController = notificationPanelViewController;
         notificationPanelViewController.addExpansionListener(this);
@@ -351,7 +356,8 @@
         } else if (mPulsing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) {
             // Panel expanded while pulsing but didn't translate the bouncer (because we are
             // unlocked.) Let's simply wake-up to dismiss the lock screen.
-            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mContainer, "BOUNCER_VISIBLE");
+            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mStatusBar.getBouncerContainer(),
+                    "BOUNCER_VISIBLE");
         }
     }
 
@@ -637,6 +643,18 @@
                         });
                 return;
             }
+
+            if (mStatusBar.isLaunchingActivityOverLockscreen()) {
+                mOccluded = true;
+
+                // When isLaunchingActivityOverLockscreen() is true, we know for sure that the post
+                // collapse runnables will be run.
+                mShadeController.get().addPostCollapseAction(() -> {
+                    mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+                    reset(true /* hideBouncerWhenShowing */);
+                });
+                return;
+            }
         } else if (!occluded && mOccluded && mShowing) {
             SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
                     SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__SHOWN);
@@ -816,7 +834,7 @@
     }
 
     public void onKeyguardFadedAway() {
-        mContainer.postDelayed(() -> mNotificationShadeWindowController
+        mNotificationContainer.postDelayed(() -> mNotificationShadeWindowController
                         .setKeyguardFadingAway(false), 100);
         ViewGroupFadeHelper.reset(mNotificationPanelViewController.getView());
         mStatusBar.finishKeyguardFadingAway();
@@ -941,10 +959,6 @@
     };
 
     protected void updateStates() {
-        if (mContainer == null ) {
-            return;
-        }
-        int vis = mContainer.getSystemUiVisibility();
         boolean showing = mShowing;
         boolean occluded = mOccluded;
         boolean bouncerShowing = mBouncer.isShowing();
@@ -956,9 +970,9 @@
                 (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
                 || mFirstUpdate) {
             if (bouncerDismissible || !showing || remoteInputActive) {
-                mContainer.setSystemUiVisibility(vis & ~View.STATUS_BAR_DISABLE_BACK);
+                mBouncer.setBackButtonEnabled(true);
             } else {
-                mContainer.setSystemUiVisibility(vis | View.STATUS_BAR_DISABLE_BACK);
+                mBouncer.setBackButtonEnabled(false);
             }
         }
 
@@ -973,6 +987,9 @@
             mStatusBar.setBouncerShowing(bouncerShowing);
         }
 
+        if (occluded != mLastOccluded || mFirstUpdate) {
+            mKeyguardUpdateManager.onKeyguardOccludedChanged(occluded);
+        }
         if ((showing && !occluded) != (mLastShowing && !mLastOccluded) || mFirstUpdate) {
             mKeyguardUpdateManager.onKeyguardVisibilityChanged(showing && !occluded);
         }
@@ -1022,11 +1039,11 @@
                 if (delay == 0) {
                     mMakeNavigationBarVisibleRunnable.run();
                 } else {
-                    mContainer.postOnAnimationDelayed(mMakeNavigationBarVisibleRunnable,
+                    mNotificationContainer.postOnAnimationDelayed(mMakeNavigationBarVisibleRunnable,
                             delay);
                 }
             } else {
-                mContainer.removeCallbacks(mMakeNavigationBarVisibleRunnable);
+                mNotificationContainer.removeCallbacks(mMakeNavigationBarVisibleRunnable);
                 mStatusBar.getNotificationShadeWindowView().getWindowInsetsController()
                         .hide(navigationBars());
             }
@@ -1129,7 +1146,6 @@
     public void showBouncerMessage(String message, ColorStateList colorState) {
         if (isShowingAlternateAuth()) {
             if (mKeyguardMessageAreaController != null) {
-                mKeyguardMessageAreaController.setNextMessageColor(colorState);
                 mKeyguardMessageAreaController.setMessage(message);
             }
         } else {
@@ -1352,4 +1368,4 @@
         void requestUdfps(boolean requestUdfps, int color);
 
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index 14e513a..32aae6c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.statusbar.phone
 
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.LaunchAnimator
 
 /**
  * A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right
@@ -22,7 +23,7 @@
         delegate.onLaunchAnimationStart(isExpandingFullyAbove)
         statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
         if (!isExpandingFullyAbove) {
-            statusBar.collapsePanelWithDuration(ActivityLaunchAnimator.ANIMATION_DURATION.toInt())
+            statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt())
         }
     }
 
@@ -33,7 +34,7 @@
     }
 
     override fun onLaunchAnimationProgress(
-        state: ActivityLaunchAnimator.State,
+        state: LaunchAnimator.State,
         progress: Float,
         linearProgress: Float
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt
new file mode 100644
index 0000000..8af03aa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.phone
+
+import android.view.View
+import android.view.WindowManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
+import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator.ViewCenterProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import javax.inject.Inject
+
+@SysUISingleton
+class StatusBarMoveFromCenterAnimationController @Inject constructor(
+    private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
+    private val windowManager: WindowManager
+) {
+
+    private lateinit var moveFromCenterAnimator: UnfoldMoveFromCenterAnimator
+
+    fun init(viewsToAnimate: Array<View>, viewCenterProvider: ViewCenterProvider) {
+        moveFromCenterAnimator = UnfoldMoveFromCenterAnimator(windowManager,
+            viewCenterProvider = viewCenterProvider)
+
+        unfoldTransitionProgressProvider.addCallback(object : TransitionProgressListener {
+            override fun onTransitionStarted() {
+                moveFromCenterAnimator.updateDisplayProperties()
+
+                viewsToAnimate.forEach {
+                    moveFromCenterAnimator.registerViewForAnimation(it)
+                }
+            }
+
+            override fun onTransitionFinished() {
+                moveFromCenterAnimator.onTransitionFinished()
+                moveFromCenterAnimator.clearRegisteredViews()
+            }
+
+            override fun onTransitionProgress(progress: Float) {
+                moveFromCenterAnimator.onTransitionProgress(progress)
+            }
+        })
+    }
+
+    fun onStatusBarWidthChanged() {
+        moveFromCenterAnimator.updateViewPositions()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 9a6dd38..dba3b24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -52,15 +52,14 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationClickNotifier;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -72,6 +71,7 @@
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
 import com.android.systemui.statusbar.policy.HeadsUpUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -230,12 +230,11 @@
         mLogger.logStartingActivityFromClick(sbn.getKey());
 
         final NotificationEntry entry = row.getEntry();
-        RemoteInputController controller = mRemoteInputManager.getController();
-        if (controller.isRemoteInputActive(entry)
+        if (mRemoteInputManager.isRemoteInputActive(entry)
                 && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
             // We have an active remote input typed and the user clicked on the notification.
             // this was probably unintentional, so we're closing the edit text instead.
-            controller.closeRemoteInputs();
+            mRemoteInputManager.closeRemoteInputs();
             return;
         }
         Notification notification = sbn.getNotification();
@@ -265,8 +264,7 @@
             @Override
             public boolean onDismiss() {
                 return handleNotificationClickAfterKeyguardDismissed(
-                        entry, row, controller, intent,
-                        isActivityIntent, animate, showOverLockscreen);
+                        entry, row, intent, isActivityIntent, animate, showOverLockscreen);
             }
 
             @Override
@@ -286,7 +284,6 @@
     private boolean handleNotificationClickAfterKeyguardDismissed(
             NotificationEntry entry,
             ExpandableNotificationRow row,
-            RemoteInputController controller,
             PendingIntent intent,
             boolean isActivityIntent,
             boolean animate,
@@ -294,8 +291,7 @@
         mLogger.logHandleClickAfterKeyguardDismissed(entry.getKey());
 
         final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed(
-                entry, row, controller, intent,
-                isActivityIntent, animate);
+                entry, row, intent, isActivityIntent, animate);
 
         if (showOverLockscreen) {
             mShadeController.addPostCollapseAction(runnable);
@@ -315,7 +311,6 @@
     private void handleNotificationClickAfterPanelCollapsed(
             NotificationEntry entry,
             ExpandableNotificationRow row,
-            RemoteInputController controller,
             PendingIntent intent,
             boolean isActivityIntent,
             boolean animate) {
@@ -354,7 +349,8 @@
         if (!TextUtils.isEmpty(entry.remoteInputText)) {
             remoteInputText = entry.remoteInputText;
         }
-        if (!TextUtils.isEmpty(remoteInputText) && !controller.isSpinning(notificationKey)) {
+        if (!TextUtils.isEmpty(remoteInputText)
+                && !mRemoteInputManager.isSpinning(notificationKey)) {
             fillInIntent = new Intent().putExtra(Notification.EXTRA_REMOTE_INPUT_DRAFT,
                     remoteInputText.toString());
         }
@@ -407,6 +403,53 @@
         mIsCollapsingToShowActivityOverLockscreen = false;
     }
 
+    /**
+     * Called when a notification is dropped on proper target window.
+     * Intent that is included in this entry notification,
+     * will be sent by {@link ExpandableNotificationRowDragController}
+     *
+     * @param entry notification entry that is dropped.
+     */
+    @Override
+    public void onDragSuccess(NotificationEntry entry) {
+        // this method is not responsible for intent sending.
+        // will focus follow operation only after drag-and-drop that notification.
+        NotificationVisibility.NotificationLocation location =
+                NotificationLogger.getNotificationLocation(entry);
+        final NotificationVisibility nv = NotificationVisibility.obtain(entry.getKey(),
+                entry.getRanking().getRank(), getVisibleNotificationsCount(), true, location);
+
+        // retrieve the group summary to remove with this entry before we tell NMS the
+        // notification was clicked to avoid a race condition
+        final boolean shouldAutoCancel = shouldAutoCancel(entry.getSbn());
+        final NotificationEntry summaryToRemove = shouldAutoCancel
+                ? mOnUserInteractionCallback.getGroupSummaryToDismiss(entry) : null;
+
+        String notificationKey = entry.getKey();
+        // inform NMS that the notification was clicked
+        mClickNotifier.onNotificationClick(notificationKey, nv);
+
+        if (shouldAutoCancel || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
+                notificationKey)) {
+            // Immediately remove notification from visually showing.
+            // We have to post the removal to the UI thread for synchronization.
+            mMainThreadHandler.post(() -> {
+                final Runnable removeNotification = () ->
+                        mOnUserInteractionCallback.onDismiss(
+                                entry, REASON_CLICK, summaryToRemove);
+                if (mPresenter.isCollapsing()) {
+                    // To avoid lags we're only performing the remove
+                    // after the shade is collapsed
+                    mShadeController.addPostCollapseAction(removeNotification);
+                } else {
+                    removeNotification.run();
+                }
+            });
+        }
+
+        mIsCollapsingToShowActivityOverLockscreen = false;
+    }
+
     private void expandBubbleStackOnMainThread(NotificationEntry entry) {
         if (!mBubblesManagerOptional.isPresent()) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 8821de0..c655964 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -25,7 +25,6 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
-import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.service.vr.IVrManager;
 import android.service.vr.IVrStateCallbacks;
@@ -47,7 +46,6 @@
 import com.android.systemui.R;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -65,7 +63,6 @@
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -84,28 +81,18 @@
         ConfigurationController.ConfigurationListener,
         NotificationRowBinderImpl.BindRowCallback,
         CommandQueue.Callbacks {
-
-    private final LockscreenGestureLogger mLockscreenGestureLogger =
-            Dependency.get(LockscreenGestureLogger.class);
-
     private static final String TAG = "StatusBarNotificationPresenter";
 
     private final ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
     private final KeyguardStateController mKeyguardStateController;
-    private final NotificationViewHierarchyManager mViewHierarchyManager =
-            Dependency.get(NotificationViewHierarchyManager.class);
-    private final NotificationLockscreenUserManager mLockscreenUserManager =
-            Dependency.get(NotificationLockscreenUserManager.class);
-    private final SysuiStatusBarStateController mStatusBarStateController =
-            (SysuiStatusBarStateController) Dependency.get(StatusBarStateController.class);
-    private final NotificationEntryManager mEntryManager =
-            Dependency.get(NotificationEntryManager.class);
-    private final NotificationMediaManager mMediaManager =
-            Dependency.get(NotificationMediaManager.class);
-    private final VisualStabilityManager mVisualStabilityManager =
-            Dependency.get(VisualStabilityManager.class);
-    private final NotificationGutsManager mGutsManager =
-            Dependency.get(NotificationGutsManager.class);
+    private final NotificationViewHierarchyManager mViewHierarchyManager;
+    private final NotificationLockscreenUserManager mLockscreenUserManager;
+    private final SysuiStatusBarStateController mStatusBarStateController;
+    private final NotificationEntryManager mEntryManager;
+    private final NotificationMediaManager mMediaManager;
+    private final NotificationGutsManager mGutsManager;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final LockscreenGestureLogger mLockscreenGestureLogger;
 
     private final NotificationPanelViewController mNotificationPanel;
     private final HeadsUpManagerPhone mHeadsUpManager;
@@ -144,8 +131,18 @@
             ShadeController shadeController,
             LockscreenShadeTransitionController shadeTransitionController,
             CommandQueue commandQueue,
+            NotificationViewHierarchyManager notificationViewHierarchyManager,
+            NotificationLockscreenUserManager lockscreenUserManager,
+            SysuiStatusBarStateController sysuiStatusBarStateController,
+            NotificationEntryManager notificationEntryManager,
+            NotificationMediaManager notificationMediaManager,
+            NotificationGutsManager notificationGutsManager,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            LockscreenGestureLogger lockscreenGestureLogger,
             InitController initController,
-            NotificationInterruptStateProvider notificationInterruptStateProvider) {
+            NotificationInterruptStateProvider notificationInterruptStateProvider,
+            NotificationRemoteInputManager remoteInputManager,
+            ConfigurationController configurationController) {
         mKeyguardStateController = keyguardStateController;
         mNotificationPanel = panel;
         mHeadsUpManager = headsUp;
@@ -156,6 +153,14 @@
         mShadeController = shadeController;
         mShadeTransitionController = shadeTransitionController;
         mCommandQueue = commandQueue;
+        mViewHierarchyManager = notificationViewHierarchyManager;
+        mLockscreenUserManager = lockscreenUserManager;
+        mStatusBarStateController = sysuiStatusBarStateController;
+        mEntryManager = notificationEntryManager;
+        mMediaManager = notificationMediaManager;
+        mGutsManager = notificationGutsManager;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mLockscreenGestureLogger = lockscreenGestureLogger;
         mAboveShelfObserver = new AboveShelfObserver(stackScrollerController.getView());
         mNotificationShadeWindowController = notificationShadeWindowController;
         mAboveShelfObserver.setListener(statusBarWindow.findViewById(
@@ -176,13 +181,9 @@
                 Slog.e(TAG, "Failed to register VR mode state listener: " + e);
             }
         }
-        NotificationRemoteInputManager remoteInputManager =
-                Dependency.get(NotificationRemoteInputManager.class);
         remoteInputManager.setUpWithCallback(
                 Dependency.get(NotificationRemoteInputManager.Callback.class),
                 mNotificationPanel.createRemoteInputDelegate());
-        remoteInputManager.getController().addCallback(
-                Dependency.get(NotificationShadeWindowController.class));
 
         initController.addPostInitTask(() -> {
             NotificationEntryListener notificationEntryListener = new NotificationEntryListener() {
@@ -222,14 +223,14 @@
 
             onUserSwitched(mLockscreenUserManager.getCurrentUserId());
         });
-        Dependency.get(ConfigurationController.class).addCallback(this);
+        configurationController.addCallback(this);
     }
 
     @Override
     public void onDensityOrFontScaleChanged() {
         MessagingMessage.dropCache();
         MessagingGroup.dropCache();
-        if (!Dependency.get(KeyguardUpdateMonitor.class).isSwitchingUser()) {
+        if (!mKeyguardUpdateMonitor.isSwitchingUser()) {
             updateNotificationsOnDensityOrFontScaleChanged();
         } else {
             mReinflateNotificationsOnUserSwitched = true;
@@ -238,7 +239,7 @@
 
     @Override
     public void onUiModeChanged() {
-        if (!Dependency.get(KeyguardUpdateMonitor.class).isSwitchingUser()) {
+        if (!mKeyguardUpdateMonitor.isSwitchingUser()) {
             updateNotificationOnUiModeChanged();
         } else {
             mDispatchUiModeChangeOnUserSwitched = true;
@@ -246,7 +247,7 @@
     }
 
     @Override
-    public void onOverlayChanged() {
+    public void onThemeChanged() {
         onDensityOrFontScaleChanged();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index fe52281..fbd9ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -25,12 +25,12 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
-import com.android.systemui.statusbar.policy.NetworkControllerImpl;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkControllerImpl;
 import com.android.systemui.statusbar.policy.SecurityController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
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 d3d9063..eb405e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -83,7 +83,7 @@
             }
 
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 initResources();
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
index f33ff27..ac43b67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
@@ -16,5 +16,6 @@
 package com.android.systemui.statusbar.phone;
 
 public interface StatusBarWindowCallback {
-    void onStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean bouncerShowing);
+    void onStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean bouncerShowing,
+            boolean isDozing);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
index 9a25a70..235a8e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.view.ViewRootImpl.INSETS_LAYOUT_GENERALIZATION;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
@@ -34,13 +36,13 @@
 import android.util.Log;
 import android.view.Gravity;
 import android.view.IWindowManager;
+import android.view.Surface;
 import android.view.ViewGroup;
 import android.view.WindowManager;
 
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 
 import javax.inject.Inject;
 
@@ -55,14 +57,13 @@
     private final Context mContext;
     private final WindowManager mWindowManager;
     private final IWindowManager mIWindowManager;
-    private final SuperStatusBarViewFactory mSuperStatusBarViewFactory;
     private final StatusBarContentInsetsProvider mContentInsetsProvider;
     private final Resources mResources;
     private int mBarHeight = -1;
     private final State mCurrentState = new State();
 
-    private ViewGroup mStatusBarView;
-    private ViewGroup mLaunchAnimationContainer;
+    private final ViewGroup mStatusBarView;
+    private final ViewGroup mLaunchAnimationContainer;
     private WindowManager.LayoutParams mLp;
     private final WindowManager.LayoutParams mLpChanged;
 
@@ -71,15 +72,14 @@
             Context context,
             WindowManager windowManager,
             IWindowManager iWindowManager,
-            SuperStatusBarViewFactory superStatusBarViewFactory,
+            StatusBarWindowView statusBarWindowView,
             StatusBarContentInsetsProvider contentInsetsProvider,
             @Main Resources resources) {
         mContext = context;
         mWindowManager = windowManager;
         mIWindowManager = iWindowManager;
         mContentInsetsProvider = contentInsetsProvider;
-        mSuperStatusBarViewFactory = superStatusBarViewFactory;
-        mStatusBarView = mSuperStatusBarViewFactory.getStatusBarWindowView();
+        mStatusBarView = statusBarWindowView;
         mLaunchAnimationContainer = mStatusBarView.findViewById(
                 R.id.status_bar_launch_animation_container);
         mLpChanged = new WindowManager.LayoutParams();
@@ -118,21 +118,7 @@
         // Now that the status bar window encompasses the sliding panel and its
         // translucent backdrop, the entire thing is made TRANSLUCENT and is
         // hardware-accelerated.
-        mLp = new WindowManager.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT,
-                mBarHeight,
-                WindowManager.LayoutParams.TYPE_STATUS_BAR,
-                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
-                        | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
-                PixelFormat.TRANSLUCENT);
-        mLp.privateFlags |= PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
-        mLp.token = new Binder();
-        mLp.gravity = Gravity.TOP;
-        mLp.setFitInsetsTypes(0 /* types */);
-        mLp.setTitle("StatusBar");
-        mLp.packageName = mContext.getPackageName();
-        mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+        mLp = getBarLayoutParams(mContext.getDisplay().getRotation());
 
         mWindowManager.addView(mStatusBarView, mLp);
         mLpChanged.copyFrom(mLp);
@@ -141,6 +127,63 @@
         calculateStatusBarLocationsForAllRotations();
     }
 
+    private WindowManager.LayoutParams getBarLayoutParams(int rotation) {
+        WindowManager.LayoutParams lp = getBarLayoutParamsForRotation(rotation);
+        lp.paramsForRotation = new WindowManager.LayoutParams[4];
+        for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) {
+            lp.paramsForRotation[rot] = getBarLayoutParamsForRotation(rot);
+        }
+        return lp;
+    }
+
+    private WindowManager.LayoutParams getBarLayoutParamsForRotation(int rotation) {
+        int height = mBarHeight;
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            Rect displayBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+            int defaultAndUpsideDownHeight;
+            int theOtherHeight;
+            if (displayBounds.width() > displayBounds.height()) {
+                defaultAndUpsideDownHeight = mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.status_bar_height_landscape);
+                theOtherHeight = mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.status_bar_height_portrait);
+            } else {
+                defaultAndUpsideDownHeight = mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.status_bar_height_portrait);
+                theOtherHeight = mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.status_bar_height_landscape);
+            }
+            switch (rotation) {
+                case ROTATION_UNDEFINED:
+                case Surface.ROTATION_0:
+                case Surface.ROTATION_180:
+                    height = defaultAndUpsideDownHeight;
+                    break;
+                case Surface.ROTATION_90:
+                case Surface.ROTATION_270:
+                    height = theOtherHeight;
+                    break;
+            }
+        }
+        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.MATCH_PARENT,
+                height,
+                WindowManager.LayoutParams.TYPE_STATUS_BAR,
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+                        | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+                PixelFormat.TRANSLUCENT);
+        lp.privateFlags |= PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
+        lp.token = new Binder();
+        lp.gravity = Gravity.TOP;
+        lp.setFitInsetsTypes(0 /* types */);
+        lp.setTitle("StatusBar");
+        lp.packageName = mContext.getPackageName();
+        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+        return lp;
+
+    }
+
     private void calculateStatusBarLocationsForAllRotations() {
         Rect[] bounds = new Rect[4];
         bounds[0] = mContentInsetsProvider
@@ -166,6 +209,21 @@
     }
 
     /**
+     * Sets whether an ongoing process requires the status bar to be forced visible.
+     *
+     * This method is separate from {@link this#setForceStatusBarVisible} because the ongoing
+     * process **takes priority**. For example, if {@link this#setForceStatusBarVisible} is set to
+     * false but this method is set to true, then the status bar **will** be visible.
+     *
+     * TODO(b/195839150): We should likely merge this method and
+     * {@link this#setForceStatusBarVisible} together and use some sort of ranking system instead.
+     */
+    public void setOngoingProcessRequiresStatusBarVisible(boolean visible) {
+        mCurrentState.mOngoingProcessRequiresStatusBarVisible = visible;
+        apply(mCurrentState);
+    }
+
+    /**
      * Return the container in which we should run launch animations started from the status bar and
      * expanding into the opening window.
      *
@@ -205,10 +263,14 @@
     private static class State {
         boolean mForceStatusBarVisible;
         boolean mIsLaunchAnimationRunning;
+        boolean mOngoingProcessRequiresStatusBarVisible;
     }
 
     private void applyForceStatusBarVisibleFlag(State state) {
-        if (state.mForceStatusBarVisible || state.mIsLaunchAnimationRunning) {
+        if (state.mForceStatusBarVisible
+                || state.mIsLaunchAnimationRunning
+                // Don't force-show the status bar if the user has already dismissed it.
+                || state.mOngoingProcessRequiresStatusBarVisible) {
             mLpChanged.privateFlags |= PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
         } else {
             mLpChanged.privateFlags &= ~PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 1e98c75..9415d50 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -30,19 +30,24 @@
 
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.animation.DialogListener;
+import com.android.systemui.animation.ListenableDialog;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+import java.util.LinkedHashSet;
+import java.util.Set;
+
 
 /**
  * Base class for dialogs that should appear over panels and keyguard.
  * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast,
  * and dismisses itself when it receives the broadcast.
  */
-public class SystemUIDialog extends AlertDialog {
-
+public class SystemUIDialog extends AlertDialog implements ListenableDialog {
     private final Context mContext;
     private final DismissReceiver mDismissReceiver;
+    private final Set<DialogListener> mDialogListeners = new LinkedHashSet<>();
 
     public SystemUIDialog(Context context) {
         this(context, R.style.Theme_SystemUI_Dialog);
@@ -72,6 +77,43 @@
         mDismissReceiver.unregister();
     }
 
+    @Override
+    public void addListener(DialogListener listener) {
+        mDialogListeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(DialogListener listener) {
+        mDialogListeners.remove(listener);
+    }
+
+    @Override
+    public void dismiss() {
+        super.dismiss();
+
+        for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) {
+            listener.onDismiss();
+        }
+    }
+
+    @Override
+    public void hide() {
+        super.hide();
+
+        for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) {
+            listener.onHide();
+        }
+    }
+
+    @Override
+    public void show() {
+        super.show();
+
+        for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) {
+            listener.onShow();
+        }
+    }
+
     public void setShowForAllUsers(boolean show) {
         setShowForAllUsers(this, show);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt
new file mode 100644
index 0000000..6a49a6d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt
@@ -0,0 +1,36 @@
+package com.android.systemui.statusbar.phone
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import com.android.systemui.animation.HostDialogProvider
+
+/** An implementation of [HostDialogProvider] to be used when animating SysUI dialogs. */
+class SystemUIHostDialogProvider : HostDialogProvider {
+    override fun createHostDialog(
+        context: Context,
+        theme: Int,
+        onCreateCallback: () -> Unit,
+        dismissOverride: (() -> Unit) -> Unit
+    ): Dialog {
+        return SystemUIHostDialog(context, theme, onCreateCallback, dismissOverride)
+    }
+
+    private class SystemUIHostDialog(
+        context: Context,
+        theme: Int,
+        private val onCreateCallback: () -> Unit,
+        private val dismissOverride: (() -> Unit) -> Unit
+    ) : SystemUIDialog(context, theme) {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            onCreateCallback()
+        }
+
+        override fun dismiss() {
+            dismissOverride {
+                super.dismiss()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java
index 0c5502b..26ba31c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java
@@ -43,11 +43,6 @@
     @VisibleForTesting
     final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
         @Override
-        public void onOverlayChanged() {
-            mView.updateColor();
-        }
-
-        @Override
         public void onUiModeChanged() {
             mView.updateColor();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 143aaba..fdc0ec5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -4,10 +4,10 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.content.Context
-import android.content.res.Configuration
 import android.database.ContentObserver
 import android.os.Handler
 import android.provider.Settings
+import android.view.Surface
 import android.view.View
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
@@ -226,6 +226,12 @@
             return false
         }
 
+        // If animations are disabled system-wide, don't play this one either.
+        if (Settings.Global.getString(
+                context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") {
+            return false
+        }
+
         // We only play the unlocked screen off animation if we are... unlocked.
         if (statusBarStateControllerImpl.state != StatusBarState.SHADE) {
             return false
@@ -239,10 +245,11 @@
             return false
         }
 
-        // If we're not allowed to rotate the keyguard, then only do the screen off animation if
-        // we're in portrait. Otherwise, AOD will animate in sideways, which looks weird.
+        // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
+        // portrait. If we're in another orientation, disable the screen off animation so we don't
+        // animate in the keyguard AOD UI sideways or upside down.
         if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
-                context.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
+            context.display.rotation != Surface.ROTATION_0) {
             return false
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarComponent.java
index fb25ae3..418f588 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarComponent.java
@@ -20,9 +20,15 @@
 
 import com.android.keyguard.LockIconViewController;
 import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.statusbar.NotificationShelfController;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController;
+import com.android.systemui.statusbar.phone.SplitShadeHeaderController;
+import com.android.systemui.statusbar.phone.StatusBarCommandQueueCallbacks;
+import com.android.systemui.statusbar.phone.StatusBarDemoMode;
+import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener;
 import com.android.systemui.statusbar.phone.StatusBarWindowController;
 
 import java.lang.annotation.Documented;
@@ -30,7 +36,6 @@
 
 import javax.inject.Scope;
 
-import dagger.BindsInstance;
 import dagger.Subcomponent;
 
 /**
@@ -42,11 +47,9 @@
     /**
      * Builder for {@link StatusBarComponent}.
      */
-    @Subcomponent.Builder
-    interface Builder {
-        @BindsInstance Builder statusBarWindowView(
-                NotificationShadeWindowView notificationShadeWindowView);
-        StatusBarComponent build();
+    @Subcomponent.Factory
+    interface Factory {
+        StatusBarComponent create();
     }
 
     /**
@@ -58,6 +61,21 @@
     @interface StatusBarScope {}
 
     /**
+     * Creates a {@link NotificationShadeWindowView}/
+     * @return
+     */
+    @StatusBarScope
+    NotificationShadeWindowView getNotificationShadeWindowView();
+
+    /** */
+    @StatusBarScope
+    NotificationShelfController getNotificationShelfController();
+
+    /** */
+    @StatusBarScope
+    NotificationStackScrollLayoutController getNotificationStackScrollLayoutController();
+
+    /**
      * Creates a NotificationShadeWindowViewController.
      */
     @StatusBarScope
@@ -86,4 +104,28 @@
      */
     @StatusBarScope
     AuthRippleController getAuthRippleController();
+
+    /**
+     * Creates a StatusBarDemoMode.
+     */
+    @StatusBarScope
+    StatusBarDemoMode getStatusBarDemoMode();
+
+    /**
+     * Creates a StatusBarHeadsUpChangeListener.
+     */
+    @StatusBarScope
+    StatusBarHeadsUpChangeListener getStatusBarHeadsUpChangeListener();
+
+    /**
+     * Creates a StatusBarCommandQueueCallbacks.
+     */
+    @StatusBarScope
+    StatusBarCommandQueueCallbacks getStatusBarCommandQueueCallbacks();
+
+    /**
+     * Creates a SplitShadeHeaderController.
+     */
+    @StatusBarScope
+    SplitShadeHeaderController getSplitShadeHeaderController();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index b6e8bd8..5689707 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -18,26 +18,28 @@
 
 import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
 
+import android.app.WallpaperManager;
 import android.content.Context;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.util.DisplayMetrics;
 
-import androidx.annotation.Nullable;
-
 import com.android.internal.logging.MetricsLogger;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.InitController;
-import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
+import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.demomode.DemoModeController;
-import com.android.systemui.keyguard.DismissCallbackRegistry;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.ScreenLifecycle;
@@ -49,7 +51,6 @@
 import com.android.systemui.settings.brightness.BrightnessSlider;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -58,13 +59,13 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
+import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.charging.WiredChargingRippleController;
+import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
@@ -74,15 +75,16 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragmentLogger;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.DozeScrimController;
 import com.android.systemui.statusbar.phone.DozeServiceHost;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
-import com.android.systemui.statusbar.phone.KeyguardLiftController;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LightsOutNotifController;
+import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
 import com.android.systemui.statusbar.phone.LockscreenWallpaper;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.PhoneStatusBarPolicy;
@@ -92,9 +94,10 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
+import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy;
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
+import com.android.systemui.statusbar.phone.StatusBarWindowView;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -102,10 +105,15 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.ExtensionController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
+import com.android.systemui.unfold.UnfoldTransitionWallpaperController;
+import com.android.systemui.unfold.config.UnfoldTransitionConfig;
+import com.android.systemui.util.WallpaperController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.concurrency.MessageRouter;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.wmshell.BubblesManager;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -116,7 +124,6 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Named;
-import javax.inject.Provider;
 
 import dagger.Lazy;
 import dagger.Module;
@@ -138,7 +145,6 @@
             LightBarController lightBarController,
             AutoHideController autoHideController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            StatusBarSignalPolicy signalPolicy,
             PulseExpansionHandler pulseExpansionHandler,
             NotificationWakeUpCoordinator notificationWakeUpCoordinator,
             KeyguardBypassController keyguardBypassController,
@@ -149,7 +155,7 @@
             FalsingManager falsingManager,
             FalsingCollector falsingCollector,
             BroadcastDispatcher broadcastDispatcher,
-            RemoteInputQuickSettingsDisabler remoteInputQuickSettingsDisabler,
+            NotificationEntryManager notificationEntryManager,
             NotificationGutsManager notificationGutsManager,
             NotificationLogger notificationLogger,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
@@ -168,20 +174,18 @@
             ScreenLifecycle screenLifecycle,
             WakefulnessLifecycle wakefulnessLifecycle,
             SysuiStatusBarStateController statusBarStateController,
-            VibratorHelper vibratorHelper,
             Optional<BubblesManager> bubblesManagerOptional,
             Optional<Bubbles> bubblesOptional,
             VisualStabilityManager visualStabilityManager,
             DeviceProvisionedController deviceProvisionedController,
             NavigationBarController navigationBarController,
-            AccessibilityFloatingMenuController accessibilityFloatingMenuController,
             Lazy<AssistManager> assistManagerLazy,
             ConfigurationController configurationController,
             NotificationShadeWindowController notificationShadeWindowController,
             DozeParameters dozeParameters,
             ScrimController scrimController,
-            @Nullable KeyguardLiftController keyguardLiftController,
             Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
+            LockscreenGestureLogger lockscreenGestureLogger,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
             DozeServiceHost dozeServiceHost,
             PowerManager powerManager,
@@ -189,14 +193,15 @@
             DozeScrimController dozeScrimController,
             VolumeComponent volumeComponent,
             CommandQueue commandQueue,
-            Provider<StatusBarComponent.Builder> statusBarComponentBuilder,
+            CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger,
+            StatusBarComponent.Factory statusBarComponentFactory,
             PluginManager pluginManager,
             Optional<LegacySplitScreen> splitScreenOptional,
             LightsOutNotifController lightsOutNotifController,
             StatusBarNotificationActivityStarter.Builder
                     statusBarNotificationActivityStarterBuilder,
             ShadeController shadeController,
-            SuperStatusBarViewFactory superStatusBarViewFactory,
+            StatusBarWindowView statusBarWindowView,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             ViewMediatorCallback viewMediatorCallback,
             InitController initController,
@@ -205,15 +210,19 @@
             KeyguardDismissUtil keyguardDismissUtil,
             ExtensionController extensionController,
             UserInfoControllerImpl userInfoControllerImpl,
+            OperatorNameViewController.Factory operatorNameViewControllerFactory,
             PhoneStatusBarPolicy phoneStatusBarPolicy,
             KeyguardIndicationController keyguardIndicationController,
             DemoModeController demoModeController,
             Lazy<NotificationShadeDepthController> notificationShadeDepthController,
-            DismissCallbackRegistry dismissCallbackRegistry,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
             NotificationIconAreaController notificationIconAreaController,
             BrightnessSlider.Factory brightnessSliderFactory,
-            WiredChargingRippleController chargingRippleAnimationController,
+            UnfoldTransitionConfig unfoldTransitionConfig,
+            Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
+            Lazy<UnfoldTransitionWallpaperController> unfoldTransitionWallpaperController,
+            Lazy<StatusBarMoveFromCenterAnimationController> statusBarMoveFromCenterAnimation,
+            WallpaperController wallpaperController,
             OngoingCallController ongoingCallController,
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
@@ -221,15 +230,22 @@
             LockscreenShadeTransitionController transitionController,
             FeatureFlags featureFlags,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
+            @Main Handler mainHandler,
+            @Main DelayableExecutor delayableExecutor,
+            @Main MessageRouter messageRouter,
+            WallpaperManager wallpaperManager,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
-            Optional<StartingSurface> startingSurfaceOptional) {
+            Optional<StartingSurface> startingSurfaceOptional,
+            TunerService tunerService,
+            DumpManager dumpManager,
+            ActivityLaunchAnimator activityLaunchAnimator,
+            DialogLaunchAnimator dialogLaunchAnimator) {
         return new StatusBar(
                 context,
                 notificationsController,
                 lightBarController,
                 autoHideController,
                 keyguardUpdateMonitor,
-                signalPolicy,
                 pulseExpansionHandler,
                 notificationWakeUpCoordinator,
                 keyguardBypassController,
@@ -240,7 +256,7 @@
                 falsingManager,
                 falsingCollector,
                 broadcastDispatcher,
-                remoteInputQuickSettingsDisabler,
+                notificationEntryManager,
                 notificationGutsManager,
                 notificationLogger,
                 notificationInterruptStateProvider,
@@ -259,20 +275,18 @@
                 screenLifecycle,
                 wakefulnessLifecycle,
                 statusBarStateController,
-                vibratorHelper,
                 bubblesManagerOptional,
                 bubblesOptional,
                 visualStabilityManager,
                 deviceProvisionedController,
                 navigationBarController,
-                accessibilityFloatingMenuController,
                 assistManagerLazy,
                 configurationController,
                 notificationShadeWindowController,
                 dozeParameters,
                 scrimController,
-                keyguardLiftController,
                 lockscreenWallpaperLazy,
+                lockscreenGestureLogger,
                 biometricUnlockControllerLazy,
                 dozeServiceHost,
                 powerManager,
@@ -280,13 +294,14 @@
                 dozeScrimController,
                 volumeComponent,
                 commandQueue,
-                statusBarComponentBuilder,
+                collapsedStatusBarFragmentLogger,
+                statusBarComponentFactory,
                 pluginManager,
                 splitScreenOptional,
                 lightsOutNotifController,
                 statusBarNotificationActivityStarterBuilder,
                 shadeController,
-                superStatusBarViewFactory,
+                statusBarWindowView,
                 statusBarKeyguardViewManager,
                 viewMediatorCallback,
                 initController,
@@ -295,15 +310,19 @@
                 keyguardDismissUtil,
                 extensionController,
                 userInfoControllerImpl,
+                operatorNameViewControllerFactory,
                 phoneStatusBarPolicy,
                 keyguardIndicationController,
-                dismissCallbackRegistry,
                 demoModeController,
                 notificationShadeDepthController,
                 statusBarTouchableRegionManager,
                 notificationIconAreaController,
                 brightnessSliderFactory,
-                chargingRippleAnimationController,
+                unfoldTransitionConfig,
+                unfoldLightRevealOverlayAnimation,
+                unfoldTransitionWallpaperController,
+                statusBarMoveFromCenterAnimation,
+                wallpaperController,
                 ongoingCallController,
                 animationScheduler,
                 locationPublisher,
@@ -311,7 +330,15 @@
                 transitionController,
                 featureFlags,
                 keyguardUnlockAnimationController,
+                mainHandler,
+                delayableExecutor,
+                messageRouter,
+                wallpaperManager,
                 unlockedScreenOffAnimationController,
-                startingSurfaceOptional);
+                startingSurfaceOptional,
+                tunerService,
+                dumpManager,
+                activityLaunchAnimator,
+                dialogLaunchAnimator);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 27d71ed..2791678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -17,19 +17,91 @@
 package com.android.systemui.statusbar.phone.dagger;
 
 import android.annotation.Nullable;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
 
 import com.android.keyguard.LockIconView;
 import com.android.systemui.R;
+import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.biometrics.AuthRippleView;
+import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.NotificationShelfController;
+import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.phone.NotificationPanelView;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
+import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 import com.android.systemui.statusbar.phone.TapAgainView;
+import com.android.systemui.util.InjectionInflationController;
+
+import javax.inject.Named;
 
 import dagger.Module;
 import dagger.Provides;
 
 @Module
 public abstract class StatusBarViewModule {
+
+    public static final String SPLIT_SHADE_HEADER = "split_shade_header";
+
+    /** */
+    @Provides
+    @StatusBarComponent.StatusBarScope
+    public static NotificationShadeWindowView providesNotificationShadeWindowView(
+            InjectionInflationController injectionInflationController,
+            Context context) {
+        NotificationShadeWindowView notificationShadeWindowView = (NotificationShadeWindowView)
+                injectionInflationController.injectable(
+                        LayoutInflater.from(context)).inflate(R.layout.super_notification_shade,
+                        /* root= */ null);
+        if (notificationShadeWindowView == null) {
+            throw new IllegalStateException(
+                    "R.layout.super_notification_shade could not be properly inflated");
+        }
+
+        return notificationShadeWindowView;
+    }
+
+    /** */
+    @Provides
+    @StatusBarComponent.StatusBarScope
+    public static NotificationStackScrollLayout providesNotificationStackScrollLayout(
+            NotificationShadeWindowView notificationShadeWindowView) {
+        return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller);
+    }
+
+    /** */
+    @Provides
+    @StatusBarComponent.StatusBarScope
+    public static NotificationShelf providesNotificationShelf(LayoutInflater layoutInflater,
+            NotificationStackScrollLayout notificationStackScrollLayout) {
+        NotificationShelf view = (NotificationShelf) layoutInflater.inflate(
+                R.layout.status_bar_notification_shelf, notificationStackScrollLayout, false);
+
+        if (view == null) {
+            throw new IllegalStateException(
+                    "R.layout.status_bar_notification_shelf could not be properly inflated");
+        }
+        return view;
+    }
+
+    /** */
+    @Provides
+    @StatusBarComponent.StatusBarScope
+    public static NotificationShelfController providesStatusBarWindowView(
+            NotificationShelfComponent.Builder notificationShelfComponentBuilder,
+            NotificationShelf notificationShelf) {
+        NotificationShelfComponent component = notificationShelfComponentBuilder
+                .notificationShelf(notificationShelf)
+                .build();
+        NotificationShelfController notificationShelfController =
+                component.getNotificationShelfController();
+        notificationShelfController.init();
+
+        return notificationShelfController;
+    }
+
     /** */
     @Provides
     @StatusBarComponent.StatusBarScope
@@ -57,8 +129,32 @@
 
     /** */
     @Provides
+    @Named(SPLIT_SHADE_HEADER)
+    @StatusBarComponent.StatusBarScope
+    public static View getSlitShadeStatusBarView(
+            NotificationShadeWindowView notificationShadeWindowView) {
+        return notificationShadeWindowView.findViewById(R.id.split_shade_status_bar);
+    }
+
+    /** */
+    @Provides
+    @StatusBarComponent.StatusBarScope
+    static BatteryMeterView getBatteryMeterView(@Named(SPLIT_SHADE_HEADER) View view) {
+        return view.findViewById(R.id.batteryRemainingIcon);
+    }
+
+    /** */
+    @Provides
     @StatusBarComponent.StatusBarScope
     public static TapAgainView getTapAgainView(NotificationPanelView npv) {
         return npv.getTapAgainView();
     }
+
+    /** */
+    @Provides
+    @StatusBarComponent.StatusBarScope
+    public static NotificationsQuickSettingsContainer getNotificationsQuickSettingsContainer(
+            NotificationShadeWindowView notificationShadeWindowView) {
+        return notificationShadeWindowView.findViewById(R.id.notification_container_parent);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 80a0a98..3806d9a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -24,20 +24,27 @@
 import android.content.Intent
 import android.util.Log
 import android.view.View
-import android.widget.Chronometer
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.phone.StatusBarWindowController
 import com.android.systemui.statusbar.policy.CallbackController
 import com.android.systemui.util.time.SystemClock
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.util.Optional
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -52,13 +59,18 @@
     private val activityStarter: ActivityStarter,
     @Main private val mainExecutor: Executor,
     private val iActivityManager: IActivityManager,
-    private val logger: OngoingCallLogger
-) : CallbackController<OngoingCallListener> {
+    private val logger: OngoingCallLogger,
+    private val dumpManager: DumpManager,
+    private val statusBarWindowController: Optional<StatusBarWindowController>,
+    private val swipeStatusBarAwayGestureHandler: Optional<SwipeStatusBarAwayGestureHandler>,
+    private val statusBarStateController: StatusBarStateController,
+) : CallbackController<OngoingCallListener>, Dumpable {
 
+    private var isFullscreen: Boolean = false
     /** Non-null if there's an active call notification. */
     private var callNotificationInfo: CallNotificationInfo? = null
     /** True if the application managing the call is visible to the user. */
-    private var isCallAppVisible: Boolean = true
+    private var isCallAppVisible: Boolean = false
     private var chipView: View? = null
     private var uidObserver: IUidObserver.Stub? = null
 
@@ -89,7 +101,8 @@
                         entry.sbn.notification.contentIntent?.intent,
                         entry.sbn.uid,
                         entry.sbn.notification.extras.getInt(
-                                Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING
+                                Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING,
+                        statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
                 )
                 if (newOngoingCallInfo == callNotificationInfo) {
                     return
@@ -104,16 +117,7 @@
             }
         }
 
-        // Fix for b/199600334
-        override fun onEntryCleanUp(entry: NotificationEntry) {
-            removeChipIfNeeded(entry)
-        }
-
         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-            removeChipIfNeeded(entry)
-        }
-
-        private fun removeChipIfNeeded(entry: NotificationEntry) {
             if (entry.sbn.key == callNotificationInfo?.key) {
                 removeChip()
             }
@@ -121,8 +125,10 @@
     }
 
     fun init() {
+        dumpManager.registerDumpable(this)
         if (featureFlags.isOngoingCallStatusBarChipEnabled) {
             notifCollection.addCollectionListener(notifListener)
+            statusBarStateController.addCallback(statusBarStateListener)
         }
     }
 
@@ -139,7 +145,6 @@
         }
     }
 
-
     /**
      * Called when the chip's visibility may have changed.
      *
@@ -177,10 +182,8 @@
 
         val currentChipView = chipView
         val timeView = currentChipView?.getTimeView()
-        val backgroundView =
-            currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
 
-        if (currentChipView != null && timeView != null && backgroundView != null) {
+        if (currentChipView != null && timeView != null) {
             if (currentCallNotificationInfo.hasValidStartTime()) {
                 timeView.setShouldHideText(false)
                 timeView.base = currentCallNotificationInfo.callStartTime -
@@ -191,22 +194,15 @@
                 timeView.setShouldHideText(true)
                 timeView.stop()
             }
-
-            currentCallNotificationInfo.intent?.let { intent ->
-                currentChipView.setOnClickListener {
-                    logger.logChipClicked()
-                    activityStarter.postStartActivityDismissingKeyguard(
-                            intent,
-                            0,
-                            ActivityLaunchAnimator.Controller.fromView(
-                                    backgroundView,
-                                    InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
-                    )
-                }
-            }
+            updateChipClickListener()
 
             setUpUidObserver(currentCallNotificationInfo)
-
+            if (!currentCallNotificationInfo.statusBarSwipedAway) {
+                statusBarWindowController.ifPresent {
+                    it.setOngoingProcessRequiresStatusBarVisible(true)
+                }
+            }
+            updateGestureListening()
             mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
         } else {
             // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
@@ -220,6 +216,30 @@
         }
     }
 
+    private fun updateChipClickListener() {
+        if (callNotificationInfo == null) { return }
+        if (isFullscreen && !featureFlags.isOngoingCallInImmersiveChipTapEnabled) {
+            chipView?.setOnClickListener(null)
+        } else {
+            val currentChipView = chipView
+            val backgroundView =
+                currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
+            val intent = callNotificationInfo?.intent
+            if (currentChipView != null && backgroundView != null && intent != null) {
+                currentChipView.setOnClickListener {
+                    logger.logChipClicked()
+                    activityStarter.postStartActivityDismissingKeyguard(
+                        intent,
+                        0,
+                        ActivityLaunchAnimator.Controller.fromView(
+                            backgroundView,
+                            InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
+                    )
+                }
+            }
+        }
+    }
+
     /**
      * Sets up an [IUidObserver] to monitor the status of the application managing the ongoing call.
      */
@@ -233,7 +253,11 @@
 
         uidObserver = object : IUidObserver.Stub() {
             override fun onUidStateChanged(
-                    uid: Int, procState: Int, procStateSeq: Long, capability: Int) {
+                uid: Int,
+                procState: Int,
+                procStateSeq: Long,
+                capability: Int
+            ) {
                 if (uid == currentCallNotificationInfo.uid) {
                     val oldIsCallAppVisible = isCallAppVisible
                     isCallAppVisible = isProcessVisibleToUser(procState)
@@ -266,9 +290,23 @@
         return procState <= ActivityManager.PROCESS_STATE_TOP
     }
 
+    private fun updateGestureListening() {
+        if (callNotificationInfo == null
+            || callNotificationInfo?.statusBarSwipedAway == true
+            || !isFullscreen) {
+            swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
+        } else {
+            swipeStatusBarAwayGestureHandler.ifPresent {
+                it.addOnGestureDetectedCallback(TAG, this::onSwipeAwayGestureDetected)
+            }
+        }
+    }
+
     private fun removeChip() {
         callNotificationInfo = null
         tearDownChipView()
+        statusBarWindowController.ifPresent { it.setOngoingProcessRequiresStatusBarVisible(false) }
+        swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
         mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
         if (uidObserver != null) {
             iActivityManager.unregisterUidObserver(uidObserver)
@@ -283,13 +321,42 @@
         return this.findViewById(R.id.ongoing_call_chip_time)
     }
 
+   /**
+    * If there's an active ongoing call, then we will force the status bar to always show, even if
+    * the user is in immersive mode. However, we also want to give users the ability to swipe away
+    * the status bar if they need to access the area under the status bar.
+    *
+    * This method updates the status bar window appropriately when the swipe away gesture is
+    * detected.
+    */
+   private fun onSwipeAwayGestureDetected() {
+       if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") }
+       callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true)
+       statusBarWindowController.ifPresent {
+           it.setOngoingProcessRequiresStatusBarVisible(false)
+       }
+       swipeStatusBarAwayGestureHandler.ifPresent {
+           it.removeOnGestureDetectedCallback(TAG)
+       }
+   }
+
+    private val statusBarStateListener = object : StatusBarStateController.StateListener {
+        override fun onFullscreenStateChanged(isFullscreen: Boolean) {
+            this@OngoingCallController.isFullscreen = isFullscreen
+            updateChipClickListener()
+            updateGestureListening()
+        }
+    }
+
     private data class CallNotificationInfo(
         val key: String,
         val callStartTime: Long,
         val intent: Intent?,
         val uid: Int,
         /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
-        val isOngoing: Boolean
+        val isOngoing: Boolean,
+        /** True if the user has swiped away the status bar while in this phone call. */
+        val statusBarSwipedAway: Boolean
     ) {
         /**
          * Returns true if the notification information has a valid call start time.
@@ -297,6 +364,11 @@
          */
         fun hasValidStartTime(): Boolean = callStartTime > 0
     }
+
+    override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+        pw.println("Active call notification: $callNotificationInfo")
+        pw.println("Call app visible: $isCallAppVisible")
+    }
 }
 
 private fun isCallNotification(entry: NotificationEntry): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
index d38284a..479ca8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
@@ -14,7 +14,6 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.content.Context;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
@@ -35,8 +34,8 @@
     private final AccessibilityManager mAccessibilityManager;
 
     @Inject
-    public AccessibilityManagerWrapper(Context context) {
-        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+    public AccessibilityManagerWrapper(AccessibilityManager accessibilityManager) {
+        mAccessibilityManager = accessibilityManager;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
index 3a05ec7..6b80a9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
@@ -37,7 +37,7 @@
         default void onConfigChanged(Configuration newConfig) {}
         default void onDensityOrFontScaleChanged() {}
         default void onSmallestScreenWidthChanged() {}
-        default void onOverlayChanged() {}
+        default void onMaxBoundsChanged() {}
         default void onUiModeChanged() {}
         default void onThemeChanged() {}
         default void onLocaleListChanged() {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureController.java
new file mode 100644
index 0000000..bbba19d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureController.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import static com.android.systemui.statusbar.policy.DevicePostureController.Callback;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Listener for device posture changes. This can be used to query the current posture, or register
+ * for events when it changes.
+ */
+public interface DevicePostureController extends CallbackController<Callback> {
+    @IntDef(prefix = {"DEVICE_POSTURE_"}, value = {
+            DEVICE_POSTURE_UNKNOWN,
+            DEVICE_POSTURE_CLOSED,
+            DEVICE_POSTURE_HALF_OPENED,
+            DEVICE_POSTURE_OPENED,
+            DEVICE_POSTURE_FLIPPED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface DevicePostureInt {}
+
+    // NOTE: These constants **must** match those defined for Jetpack Sidecar. This is because we
+    // use the Device State -> Jetpack Posture map in DevicePostureControllerImpl to translate
+    // between the two.
+    int DEVICE_POSTURE_UNKNOWN = 0;
+    int DEVICE_POSTURE_CLOSED = 1;
+    int DEVICE_POSTURE_HALF_OPENED = 2;
+    int DEVICE_POSTURE_OPENED = 3;
+    int DEVICE_POSTURE_FLIPPED = 4;
+    int SUPPORTED_POSTURES_SIZE = DEVICE_POSTURE_FLIPPED + 1;
+
+    /** Return the current device posture. */
+    @DevicePostureInt int getDevicePosture();
+
+    /**
+     * String representation of DevicePostureInt.
+     */
+    static String devicePostureToString(@DevicePostureInt int posture) {
+        switch (posture) {
+            case DEVICE_POSTURE_CLOSED:
+                return "DEVICE_POSTURE_CLOSED";
+            case DEVICE_POSTURE_HALF_OPENED:
+                return "DEVICE_POSTURE_HALF_OPENED";
+            case DEVICE_POSTURE_OPENED:
+                return "DEVICE_POSTURE_OPENED";
+            case DEVICE_POSTURE_FLIPPED:
+                return "DEVICE_POSTURE_FLIPPED";
+            case DEVICE_POSTURE_UNKNOWN:
+                return "DEVICE_POSTURE_UNKNOWN";
+            default:
+                return "UNSUPPORTED POSTURE posture=" + posture;
+        }
+    }
+
+    /** Callback to be notified about device posture changes. */
+    interface Callback {
+        /** Called when the posture changes. */
+        void onPostureChanged(@DevicePostureInt int posture);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerImpl.java
new file mode 100644
index 0000000..8471e0a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerImpl.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.util.SparseIntArray;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/** Implementation of {@link DevicePostureController} using the DeviceStateManager. */
+@SysUISingleton
+public class DevicePostureControllerImpl implements DevicePostureController {
+    private final List<Callback> mListeners = new ArrayList<>();
+    private int mCurrentDevicePosture = DEVICE_POSTURE_UNKNOWN;
+
+    private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+
+    @Inject
+    public DevicePostureControllerImpl(
+            Context context, DeviceStateManager deviceStateManager, @Main Executor executor) {
+        // Most of this is borrowed from WindowManager/Jetpack/DeviceStateManagerPostureProducer.
+        // Using the sidecar/extension libraries directly brings in a new dependency that it'd be
+        // good to avoid (along with the fact that sidecar is deprecated, and extensions isn't fully
+        // ready yet), and we'd have to make our own layer over the sidecar library anyway to easily
+        // allow the implementation to change, so it was easier to just interface with
+        // DeviceStateManager directly.
+        String[] deviceStatePosturePairs = context.getResources()
+                .getStringArray(R.array.config_device_state_postures);
+        for (String deviceStatePosturePair : deviceStatePosturePairs) {
+            String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
+            if (deviceStatePostureMapping.length != 2) {
+                continue;
+            }
+
+            int deviceState;
+            int posture;
+            try {
+                deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
+                posture = Integer.parseInt(deviceStatePostureMapping[1]);
+            } catch (NumberFormatException e) {
+                continue;
+            }
+
+            mDeviceStateToPostureMap.put(deviceState, posture);
+        }
+
+        deviceStateManager.registerCallback(executor, state -> {
+            mCurrentDevicePosture =
+                    mDeviceStateToPostureMap.get(state, DEVICE_POSTURE_UNKNOWN);
+
+            mListeners.forEach(l -> l.onPostureChanged(mCurrentDevicePosture));
+        });
+    }
+
+    @Override
+    public void addCallback(@NonNull Callback listener) {
+        mListeners.add(listener);
+    }
+
+    @Override
+    public void removeCallback(@NonNull Callback listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    public int getDevicePosture() {
+        return mCurrentDevicePosture;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
new file mode 100644
index 0000000..41cacf5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+
+import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED;
+import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED;
+import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED;
+
+import static com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule.DEVICE_STATE_ROTATION_LOCK_DEFAULTS;
+
+import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.wrapper.RotationPolicyWrapper;
+
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * Handles reading and writing of rotation lock settings per device state, as well as setting
+ * the rotation lock when device state changes.
+ **/
+@SysUISingleton
+public final class DeviceStateRotationLockSettingController implements Listenable,
+        RotationLockController.RotationLockControllerCallback {
+
+    private static final String TAG = "DSRotateLockSettingCon";
+
+    private static final String SEPARATOR_REGEX = ":";
+
+    private final SecureSettings mSecureSettings;
+    private final RotationPolicyWrapper mRotationPolicyWrapper;
+    private final DeviceStateManager mDeviceStateManager;
+    private final Executor mMainExecutor;
+    private final String[] mDeviceStateRotationLockDefaults;
+
+    private SparseIntArray mDeviceStateRotationLockSettings;
+    // TODO(b/183001527): Add API to query current device state and initialize this.
+    private int mDeviceState = -1;
+    @Nullable
+    private DeviceStateManager.DeviceStateCallback mDeviceStateCallback;
+
+
+    @Inject
+    public DeviceStateRotationLockSettingController(
+            SecureSettings secureSettings,
+            RotationPolicyWrapper rotationPolicyWrapper,
+            DeviceStateManager deviceStateManager,
+            @Main Executor executor,
+            @Named(DEVICE_STATE_ROTATION_LOCK_DEFAULTS) String[] deviceStateRotationLockDefaults
+    ) {
+        mSecureSettings = secureSettings;
+        mRotationPolicyWrapper = rotationPolicyWrapper;
+        mDeviceStateManager = deviceStateManager;
+        mMainExecutor = executor;
+        mDeviceStateRotationLockDefaults = deviceStateRotationLockDefaults;
+    }
+
+    /**
+     * Loads the settings from storage.
+     */
+    public void initialize() {
+        String serializedSetting =
+                mSecureSettings.getStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                        UserHandle.USER_CURRENT);
+        if (TextUtils.isEmpty(serializedSetting)) {
+            // No settings saved, we should load the defaults and persist them.
+            fallbackOnDefaults();
+            return;
+        }
+        String[] values = serializedSetting.split(SEPARATOR_REGEX);
+        if (values.length % 2 != 0) {
+            // Each entry should be a key/value pair, so this is corrupt.
+            Log.wtf(TAG, "Can't deserialize saved settings, falling back on defaults");
+            fallbackOnDefaults();
+            return;
+        }
+        mDeviceStateRotationLockSettings = new SparseIntArray(values.length / 2);
+        int key;
+        int value;
+
+        for (int i = 0; i < values.length - 1; ) {
+            try {
+                key = Integer.parseInt(values[i++]);
+                value = Integer.parseInt(values[i++]);
+                mDeviceStateRotationLockSettings.put(key, value);
+            } catch (NumberFormatException e) {
+                Log.wtf(TAG, "Error deserializing one of the saved settings", e);
+                fallbackOnDefaults();
+                return;
+            }
+        }
+    }
+
+    private void fallbackOnDefaults() {
+        loadDefaults();
+        persistSettings();
+    }
+
+    @Override
+    public void setListening(boolean listening) {
+        if (listening) {
+            // Note that this is called once with the initial state of the device, even if there
+            // is no user action.
+            mDeviceStateCallback = this::updateDeviceState;
+            mDeviceStateManager.registerCallback(mMainExecutor, mDeviceStateCallback);
+        } else {
+            if (mDeviceStateCallback != null) {
+                mDeviceStateManager.unregisterCallback(mDeviceStateCallback);
+            }
+        }
+    }
+
+    @Override
+    public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) {
+        if (mDeviceState == -1) {
+            Log.wtf(TAG, "Device state was not initialized.");
+            return;
+        }
+
+        if (rotationLocked == isRotationLockedForCurrentState()) {
+            Log.v(TAG, "Rotation lock same as the current setting, no need to update.");
+            return;
+        }
+
+        saveNewRotationLockSetting(rotationLocked);
+    }
+
+    private void saveNewRotationLockSetting(boolean isRotationLocked) {
+        Log.v(TAG, "saveNewRotationLockSetting [state=" + mDeviceState + "] [isRotationLocked="
+                + isRotationLocked + "]");
+
+        mDeviceStateRotationLockSettings.put(mDeviceState,
+                isRotationLocked
+                        ? DEVICE_STATE_ROTATION_LOCK_LOCKED
+                        : DEVICE_STATE_ROTATION_LOCK_UNLOCKED);
+        persistSettings();
+    }
+
+    private boolean isRotationLockedForCurrentState() {
+        return mDeviceStateRotationLockSettings.get(mDeviceState,
+                DEVICE_STATE_ROTATION_LOCK_IGNORED) == DEVICE_STATE_ROTATION_LOCK_LOCKED;
+    }
+
+    private void updateDeviceState(int state) {
+        Log.v(TAG, "updateDeviceState [state=" + state + "]");
+        if (mDeviceState == state) {
+            return;
+        }
+
+        int rotationLockSetting =
+                mDeviceStateRotationLockSettings.get(state, DEVICE_STATE_ROTATION_LOCK_IGNORED);
+        if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) {
+            // We won't handle this device state. The same rotation lock setting as before should
+            // apply and any changes to the rotation lock setting will be written for the previous
+            // valid device state.
+            Log.v(TAG, "Ignoring new device state: " + state);
+            return;
+        }
+
+        // Accept the new state
+        mDeviceState = state;
+
+        // Update the rotation lock setting if needed for this new device state
+        boolean newRotationLockSetting = rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_LOCKED;
+        if (newRotationLockSetting != mRotationPolicyWrapper.isRotationLocked()) {
+            mRotationPolicyWrapper.setRotationLock(newRotationLockSetting);
+        }
+    }
+
+    private void persistSettings() {
+        if (mDeviceStateRotationLockSettings.size() == 0) {
+            mSecureSettings.putStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                    /* value= */"", UserHandle.USER_CURRENT);
+            return;
+        }
+
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append(mDeviceStateRotationLockSettings.keyAt(0))
+                .append(SEPARATOR_REGEX)
+                .append(mDeviceStateRotationLockSettings.valueAt(0));
+
+        for (int i = 1; i < mDeviceStateRotationLockSettings.size(); i++) {
+            stringBuilder
+                    .append(SEPARATOR_REGEX)
+                    .append(mDeviceStateRotationLockSettings.keyAt(i))
+                    .append(SEPARATOR_REGEX)
+                    .append(mDeviceStateRotationLockSettings.valueAt(i));
+        }
+        mSecureSettings.putStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                stringBuilder.toString(), UserHandle.USER_CURRENT);
+    }
+
+    private void loadDefaults() {
+        if (mDeviceStateRotationLockDefaults.length == 0) {
+            Log.w(TAG, "Empty default settings");
+            mDeviceStateRotationLockSettings = new SparseIntArray(/* initialCapacity= */0);
+            return;
+        }
+        mDeviceStateRotationLockSettings =
+                new SparseIntArray(mDeviceStateRotationLockDefaults.length);
+        for (String serializedDefault : mDeviceStateRotationLockDefaults) {
+            String[] entry = serializedDefault.split(SEPARATOR_REGEX);
+            try {
+                int key = Integer.parseInt(entry[0]);
+                int value = Integer.parseInt(entry[1]);
+                mDeviceStateRotationLockSettings.put(key, value);
+            } catch (NumberFormatException e) {
+                Log.wtf(TAG, "Error deserializing default settings", e);
+            }
+        }
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java
index 5011d96..4c6c7e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java
@@ -56,7 +56,8 @@
     /**
      */
     @Inject
-    public ExtensionControllerImpl(Context context,
+    public ExtensionControllerImpl(
+            Context context,
             LeakDetector leakDetector,
             PluginManager pluginManager,
             TunerService tunerService,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
index d7c2b96..ad47e2b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -33,6 +33,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -72,10 +73,11 @@
     private boolean mTorchAvailable;
 
     @Inject
-    public FlashlightControllerImpl(Context context) {
+    public FlashlightControllerImpl(Context context, DumpManager dumpManager) {
         mContext = context;
         mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
 
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
         tryInitCamera();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
index 987812b..f364e49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
@@ -36,6 +36,7 @@
 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 java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -91,14 +92,18 @@
      * Controller used to retrieve information related to a hotspot.
      */
     @Inject
-    public HotspotControllerImpl(Context context, @Main Handler mainHandler,
-            @Background Handler backgroundHandler) {
+    public HotspotControllerImpl(
+            Context context,
+            @Main Handler mainHandler,
+            @Background Handler backgroundHandler,
+            DumpManager dumpManager) {
         mContext = context;
         mTetheringManager = context.getSystemService(TetheringManager.class);
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mMainHandler = mainHandler;
         mTetheringManager.registerTetheringEventCallback(
                 new HandlerExecutor(backgroundHandler), mTetheringCallback);
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index d838a05..b4aba38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -35,10 +35,12 @@
 import com.android.settingslib.drawable.CircleFramedDrawable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.tiles.UserDetailView;
+import com.android.systemui.qs.user.UserSwitchDialogController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -76,6 +78,8 @@
     private final ConfigurationController mConfigurationController;
     private final KeyguardVisibilityHelper mKeyguardVisibilityHelper;
     private final KeyguardUserDetailAdapter mUserDetailAdapter;
+    private final FeatureFlags mFeatureFlags;
+    private final UserSwitchDialogController mUserSwitchDialogController;
     private NotificationPanelViewController mNotificationPanelViewController;
     private UserAvatarView mUserAvatarView;
     UserSwitcherController.UserRecord mCurrentUser;
@@ -124,7 +128,9 @@
             SysuiStatusBarStateController statusBarStateController,
             DozeParameters dozeParameters,
             Provider<UserDetailView.Adapter> userDetailViewAdapterProvider,
-            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
+            FeatureFlags featureFlags,
+            UserSwitchDialogController userSwitchDialogController) {
         super(view);
         if (DEBUG) Log.d(TAG, "New KeyguardQsUserSwitchController");
         mContext = context;
@@ -139,6 +145,8 @@
                 keyguardStateController, dozeParameters,
                 unlockedScreenOffAnimationController,  /* animateYPos= */ false);
         mUserDetailAdapter = new KeyguardUserDetailAdapter(context, userDetailViewAdapterProvider);
+        mFeatureFlags = featureFlags;
+        mUserSwitchDialogController = userSwitchDialogController;
     }
 
     @Override
@@ -162,7 +170,11 @@
             }
 
             // Tapping anywhere in the view will open QS user panel
-            openQsUserPanel();
+            if (mFeatureFlags.useNewUserSwitcher()) {
+                mUserSwitchDialogController.showDialog(mView);
+            } else {
+                openQsUserPanel();
+            }
         });
 
         mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
index 7057618..7bf1601 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
@@ -247,6 +247,12 @@
         default void onKeyguardDismissAmountChanged() {}
 
         /**
+         * Triggered when face auth becomes available or unavailable. Value should be queried with
+         * {@link KeyguardStateController#isFaceAuthEnabled()}.
+         */
+        default void onFaceAuthEnabledChanged() {}
+
+        /**
          * Triggered when the notification panel is starting or has finished
          * fading away on transition to an app.
          */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index f787ecf..2dfcc98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -34,6 +34,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController;
 
 import java.io.FileDescriptor;
@@ -100,15 +101,20 @@
     /**
      */
     @Inject
-    public KeyguardStateControllerImpl(Context context,
-            KeyguardUpdateMonitor keyguardUpdateMonitor, LockPatternUtils lockPatternUtils,
-            SmartspaceTransitionController smartspaceTransitionController) {
+    public KeyguardStateControllerImpl(
+            Context context,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            LockPatternUtils lockPatternUtils,
+            SmartspaceTransitionController smartspaceTransitionController,
+            DumpManager dumpManager) {
         mContext = context;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mLockPatternUtils = lockPatternUtils;
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
         mSmartspaceTransitionController = smartspaceTransitionController;
 
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
         update(true /* updateAlways */);
         if (Build.IS_DEBUGGABLE && DEBUG_AUTH_WITH_ADB) {
             // Watch for interesting updates
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
index 1158324..f258fb1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
@@ -23,7 +23,6 @@
     int getRotationLockOrientation();
     boolean isRotationLockAffordanceVisible();
     boolean isRotationLocked();
-    boolean isCameraRotationEnabled();
     void setRotationLocked(boolean locked);
     void setRotationLockedAtAngle(boolean locked, int rotation);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
index c185928..67f5364 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
@@ -16,40 +16,54 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.content.Context;
+import static com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule.DEVICE_STATE_ROTATION_LOCK_DEFAULTS;
+
 import android.os.UserHandle;
-import android.provider.Settings.Secure;
 
 import androidx.annotation.NonNull;
 
-import com.android.internal.view.RotationPolicy;
+import com.android.internal.view.RotationPolicy.RotationPolicyListener;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.wrapper.RotationPolicyWrapper;
 
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /** Platform implementation of the rotation lock controller. **/
 @SysUISingleton
 public final class RotationLockControllerImpl implements RotationLockController {
-    private final Context mContext;
-    private final SecureSettings mSecureSettings;
     private final CopyOnWriteArrayList<RotationLockControllerCallback> mCallbacks =
-            new CopyOnWriteArrayList<RotationLockControllerCallback>();
+            new CopyOnWriteArrayList<>();
 
-    private final RotationPolicy.RotationPolicyListener mRotationPolicyListener =
-            new RotationPolicy.RotationPolicyListener() {
-                @Override
-                public void onChange() {
-                    notifyChanged();
-                }
-            };
+    private final RotationPolicyListener mRotationPolicyListener =
+            new RotationPolicyListener() {
+        @Override
+        public void onChange() {
+            notifyChanged();
+        }
+    };
+
+    private final RotationPolicyWrapper mRotationPolicy;
+    private final DeviceStateRotationLockSettingController
+            mDeviceStateRotationLockSettingController;
+    private final boolean mIsPerDeviceStateRotationLockEnabled;
 
     @Inject
-    public RotationLockControllerImpl(Context context, SecureSettings secureSettings) {
-        mContext = context;
-        mSecureSettings = secureSettings;
+    public RotationLockControllerImpl(
+            RotationPolicyWrapper rotationPolicyWrapper,
+            DeviceStateRotationLockSettingController deviceStateRotationLockSettingController,
+            @Named(DEVICE_STATE_ROTATION_LOCK_DEFAULTS) String[] deviceStateRotationLockDefaults
+    ) {
+        mRotationPolicy = rotationPolicyWrapper;
+        mDeviceStateRotationLockSettingController = deviceStateRotationLockSettingController;
+        mIsPerDeviceStateRotationLockEnabled = deviceStateRotationLockDefaults.length > 0;
+        if (mIsPerDeviceStateRotationLockEnabled) {
+            deviceStateRotationLockSettingController.initialize();
+            mCallbacks.add(mDeviceStateRotationLockSettingController);
+        }
+
         setListening(true);
     }
 
@@ -65,37 +79,35 @@
     }
 
     public int getRotationLockOrientation() {
-        return RotationPolicy.getRotationLockOrientation(mContext);
+        return mRotationPolicy.getRotationLockOrientation();
     }
 
     public boolean isRotationLocked() {
-        return RotationPolicy.isRotationLocked(mContext);
-    }
-
-    public boolean isCameraRotationEnabled() {
-        return mSecureSettings.getIntForUser(Secure.CAMERA_AUTOROTATE, 0, UserHandle.USER_CURRENT)
-                == 1;
+        return mRotationPolicy.isRotationLocked();
     }
 
     public void setRotationLocked(boolean locked) {
-        RotationPolicy.setRotationLock(mContext, locked);
+        mRotationPolicy.setRotationLock(locked);
     }
 
-    public void setRotationLockedAtAngle(boolean locked, int rotation) {
-        RotationPolicy.setRotationLockAtAngle(mContext, locked, rotation);
+    public void setRotationLockedAtAngle(boolean locked, int rotation){
+        mRotationPolicy.setRotationLockAtAngle(locked, rotation);
     }
 
     public boolean isRotationLockAffordanceVisible() {
-        return RotationPolicy.isRotationLockToggleVisible(mContext);
+        return mRotationPolicy.isRotationLockToggleVisible();
     }
 
     @Override
     public void setListening(boolean listening) {
         if (listening) {
-            RotationPolicy.registerRotationPolicyListener(mContext, mRotationPolicyListener,
+            mRotationPolicy.registerRotationPolicyListener(mRotationPolicyListener,
                     UserHandle.USER_ALL);
         } else {
-            RotationPolicy.unregisterRotationPolicyListener(mContext, mRotationPolicyListener);
+            mRotationPolicy.unregisterRotationPolicyListener(mRotationPolicyListener);
+        }
+        if (mIsPerDeviceStateRotationLockEnabled) {
+            mDeviceStateRotationLockSettingController.setListening(listening);
         }
     }
 
@@ -106,7 +118,7 @@
     }
 
     private void notifyChanged(RotationLockControllerCallback callback) {
-        callback.onRotationLockStateChanged(RotationPolicy.isRotationLocked(mContext),
-                RotationPolicy.isRotationLockToggleVisible(mContext));
+        callback.onRotationLockStateChanged(mRotationPolicy.isRotationLocked(),
+                mRotationPolicy.isRotationLockToggleVisible());
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 3e661df..7c13173 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -55,6 +55,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.settings.CurrentUserTracker;
 
 import org.xmlpull.v1.XmlPullParserException;
@@ -109,7 +110,8 @@
             Context context,
             @Background Handler bgHandler,
             BroadcastDispatcher broadcastDispatcher,
-            @Background Executor bgExecutor
+            @Background Executor bgExecutor,
+            DumpManager dumpManager
     ) {
         super(broadcastDispatcher);
         mContext = context;
@@ -122,6 +124,8 @@
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
         mBgExecutor = bgExecutor;
 
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
         IntentFilter filter = new IntentFilter();
         filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED);
         filter.addAction(Intent.ACTION_USER_UNLOCKED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensorPrivacyControllerImpl.java
index 6f659c1..2b8d3a2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensorPrivacyControllerImpl.java
@@ -29,7 +29,8 @@
  * Controls sensor privacy state and notification.
  */
 @SysUISingleton
-public class SensorPrivacyControllerImpl implements SensorPrivacyController,
+public class SensorPrivacyControllerImpl implements
+        SensorPrivacyController,
         SensorPrivacyManager.OnAllSensorPrivacyChangedListener {
     private SensorPrivacyManager mSensorPrivacyManager;
     private final List<OnSensorPrivacyChangedListener> mListeners = new ArrayList<>(1);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 251ecc6..b630689 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -25,6 +25,7 @@
 import android.app.ActivityManager;
 import android.app.AlertDialog;
 import android.app.Dialog;
+import android.app.IActivityManager;
 import android.app.IActivityTaskManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
@@ -55,8 +56,10 @@
 import android.widget.BaseAdapter;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.LatencyTracker;
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.systemui.Dumpable;
 import com.android.systemui.GuestResumeSessionReceiver;
@@ -68,12 +71,14 @@
 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.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.qs.QSUserSwitcherEvent;
 import com.android.systemui.qs.tiles.UserDetailView;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.user.CreateUserActivity;
@@ -106,6 +111,7 @@
     private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000;
 
     private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+    private static final long MULTI_USER_JOURNEY_TIMEOUT = 20000l;
 
     protected final Context mContext;
     protected final UserTracker mUserTracker;
@@ -122,6 +128,8 @@
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final TelephonyListenerManager mTelephonyListenerManager;
     private final IActivityTaskManager mActivityTaskManager;
+    private final InteractionJankMonitor mInteractionJankMonitor;
+    private final LatencyTracker mLatencyTracker;
 
     private ArrayList<UserRecord> mUsers = new ArrayList<>();
     @VisibleForTesting
@@ -140,15 +148,18 @@
     private Intent mSecondaryUserServiceIntent;
     private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2);
     private final UiEventLogger mUiEventLogger;
+    private final IActivityManager mActivityManager;
     public final DetailAdapter mUserDetailAdapter;
     private final Executor mBgExecutor;
     private final boolean mGuestUserAutoCreated;
     private final AtomicBoolean mGuestIsResetting;
     private final AtomicBoolean mGuestCreationScheduled;
     private FalsingManager mFalsingManager;
+    private NotificationShadeWindowView mRootView;
 
     @Inject
     public UserSwitcherController(Context context,
+            IActivityManager activityManager,
             UserManager userManager,
             UserTracker userTracker,
             KeyguardStateController keyguardStateController,
@@ -163,14 +174,20 @@
             IActivityTaskManager activityTaskManager,
             UserDetailAdapter userDetailAdapter,
             SecureSettings secureSettings,
-            @Background Executor bgExecutor) {
+            @Background Executor bgExecutor,
+            InteractionJankMonitor interactionJankMonitor,
+            LatencyTracker latencyTracker,
+            DumpManager dumpManager) {
         mContext = context;
+        mActivityManager = activityManager;
         mUserTracker = userTracker;
         mBroadcastDispatcher = broadcastDispatcher;
         mTelephonyListenerManager = telephonyListenerManager;
         mActivityTaskManager = activityTaskManager;
         mUiEventLogger = uiEventLogger;
         mFalsingManager = falsingManager;
+        mInteractionJankMonitor = interactionJankMonitor;
+        mLatencyTracker = latencyTracker;
         mGuestResumeSessionReceiver = new GuestResumeSessionReceiver(
                 this, mUserTracker, mUiEventLogger, secureSettings);
         mUserDetailAdapter = userDetailAdapter;
@@ -231,6 +248,8 @@
         keyguardStateController.addCallback(mCallback);
         listenForCallState();
 
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
         refreshUsers(UserHandle.USER_NULL);
     }
 
@@ -481,8 +500,12 @@
 
     protected void switchToUserId(int id) {
         try {
+            mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
+                    .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mRootView)
+                    .setTimeout(MULTI_USER_JOURNEY_TIMEOUT));
+            mLatencyTracker.onActionStart(LatencyTracker.ACTION_USER_SWITCH);
             pauseRefreshUsers();
-            ActivityManager.getService().switchUser(id);
+            mActivityManager.switchUser(id);
         } catch (RemoteException e) {
             Log.e(TAG, "Couldn't switch user.", e);
         }
@@ -789,6 +812,10 @@
         return guest.id;
     }
 
+    public void init(NotificationShadeWindowView notificationShadeWindowView) {
+        mRootView = notificationShadeWindowView;
+    }
+
     public static abstract class BaseUserAdapter extends BaseAdapter {
 
         final UserSwitcherController mController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index 897a3b8..5acce7f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -44,6 +44,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.qs.GlobalSetting;
 import com.android.systemui.settings.CurrentUserTracker;
 import com.android.systemui.util.Utils;
@@ -80,8 +81,11 @@
     private NotificationManager.Policy mConsolidatedNotificationPolicy;
 
     @Inject
-    public ZenModeControllerImpl(Context context, @Main Handler handler,
-            BroadcastDispatcher broadcastDispatcher) {
+    public ZenModeControllerImpl(
+            Context context,
+            @Main Handler handler,
+            BroadcastDispatcher broadcastDispatcher,
+            DumpManager dumpManager) {
         super(broadcastDispatcher);
         mContext = context;
         mModeSetting = new GlobalSetting(mContext, handler, Global.ZEN_MODE) {
@@ -108,6 +112,8 @@
         mSetupObserver.register();
         mUserManager = context.getSystemService(UserManager.class);
         startTracking();
+
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index 9fb0453..9290879 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -16,18 +16,24 @@
 
 package com.android.systemui.statusbar.policy.dagger;
 
+import android.content.res.Resources;
 import android.os.UserManager;
 
+import com.android.internal.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.policy.AccessPointControllerImpl;
+import com.android.systemui.statusbar.connectivity.AccessPointControllerImpl;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkControllerImpl;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.CastControllerImpl;
 import com.android.systemui.statusbar.policy.DeviceControlsController;
 import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.statusbar.policy.DevicePostureControllerImpl;
 import com.android.systemui.statusbar.policy.ExtensionController;
 import com.android.systemui.statusbar.policy.ExtensionControllerImpl;
 import com.android.systemui.statusbar.policy.FlashlightController;
@@ -38,8 +44,6 @@
 import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl;
 import com.android.systemui.statusbar.policy.LocationController;
 import com.android.systemui.statusbar.policy.LocationControllerImpl;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkControllerImpl;
 import com.android.systemui.statusbar.policy.NextAlarmController;
 import com.android.systemui.statusbar.policy.NextAlarmControllerImpl;
 import com.android.systemui.statusbar.policy.RotationLockController;
@@ -55,6 +59,8 @@
 
 import java.util.concurrent.Executor;
 
+import javax.inject.Named;
+
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
@@ -63,6 +69,9 @@
 /** Dagger Module for code in the statusbar.policy package. */
 @Module
 public interface StatusBarPolicyModule {
+
+    String DEVICE_STATE_ROTATION_LOCK_DEFAULTS = "DEVICE_STATE_ROTATION_LOCK_DEFAULTS";
+
     /** */
     @Binds
     BluetoothController provideBluetoothController(BluetoothControllerImpl controllerImpl);
@@ -130,6 +139,11 @@
             AccessPointControllerImpl accessPointControllerImpl);
 
     /** */
+    @Binds
+    DevicePostureController provideDevicePostureController(
+            DevicePostureControllerImpl devicePostureControllerImpl);
+
+    /** */
     @SysUISingleton
     @Provides
     static AccessPointControllerImpl  provideAccessPointControllerImpl(
@@ -147,4 +161,14 @@
         controller.init();
         return controller;
     }
+
+    /**
+     * Default values for per-device state rotation lock settings.
+     */
+    @Provides
+    @Named(DEVICE_STATE_ROTATION_LOCK_DEFAULTS)
+    static String[] providesDeviceStateRotationLockDefaults(@Main Resources resources) {
+        return resources.getStringArray(
+                R.array.config_perDeviceStateRotationLockDefaults);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 81999b5..cd5865f 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -37,6 +37,7 @@
 import android.content.om.FabricatedOverlay;
 import android.content.om.OverlayIdentifier;
 import android.content.pm.UserInfo;
+import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.graphics.Color;
 import android.net.Uri;
@@ -47,9 +48,11 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.TypedValue;
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.graphics.ColorUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.SystemUI;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -57,9 +60,10 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.util.settings.SecureSettings;
@@ -71,6 +75,7 @@
 import java.io.PrintWriter;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executor;
@@ -107,6 +112,7 @@
     private DeviceProvisionedController mDeviceProvisionedController;
     private WallpaperColors mCurrentColors;
     private WallpaperManager mWallpaperManager;
+    private ColorScheme mColorScheme;
     // If fabricated overlays were already created for the current theme.
     private boolean mNeedsOverlayCreation;
     // Dominant color extracted from wallpaper, NOT the color used on the overlay
@@ -403,25 +409,47 @@
      * Return the main theme color from a given {@link WallpaperColors} instance.
      */
     protected int getNeutralColor(@NonNull WallpaperColors wallpaperColors) {
-        return wallpaperColors.getPrimaryColor().toArgb();
+        return ColorScheme.getSeedColor(wallpaperColors);
     }
 
     protected int getAccentColor(@NonNull WallpaperColors wallpaperColors) {
-        Color accentCandidate = wallpaperColors.getSecondaryColor();
-        if (accentCandidate == null) {
-            accentCandidate = wallpaperColors.getTertiaryColor();
-        }
-        if (accentCandidate == null) {
-            accentCandidate = wallpaperColors.getPrimaryColor();
-        }
-        return accentCandidate.toArgb();
+        return ColorScheme.getSeedColor(wallpaperColors);
     }
 
     /**
      * Given a color candidate, return an overlay definition.
      */
     protected @Nullable FabricatedOverlay getOverlay(int color, int type) {
-        return null;
+        boolean nightMode = (mContext.getResources().getConfiguration().uiMode
+                & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
+
+        mColorScheme = new ColorScheme(color, nightMode);
+        List<Integer> colorShades = type == ACCENT
+                ? mColorScheme.getAllAccentColors() : mColorScheme.getAllNeutralColors();
+        String name = type == ACCENT ? "accent" : "neutral";
+        int paletteSize = mColorScheme.getAccent1().size();
+        FabricatedOverlay.Builder overlay =
+                new FabricatedOverlay.Builder("com.android.systemui", name, "android");
+        for (int i = 0; i < colorShades.size(); i++) {
+            int luminosity = i % paletteSize;
+            int paletteIndex = i / paletteSize + 1;
+            String resourceName;
+            switch (luminosity) {
+                case 0:
+                    resourceName = "android:color/system_" + name + paletteIndex + "_10";
+                    break;
+                case 1:
+                    resourceName = "android:color/system_" + name + paletteIndex + "_50";
+                    break;
+                default:
+                    int l = luminosity - 1;
+                    resourceName = "android:color/system_" + name + paletteIndex + "_" + l + "00";
+            }
+            overlay.setResourceValue(resourceName, TypedValue.TYPE_INT_COLOR_ARGB8,
+                    ColorUtils.setAlphaComponent(colorShades.get(i), 0xFF));
+        }
+
+        return overlay.build();
     }
 
     private void updateThemeOverlays() {
@@ -540,6 +568,7 @@
         pw.println("mSecondaryOverlay=" + mSecondaryOverlay);
         pw.println("mNeutralOverlay=" + mNeutralOverlay);
         pw.println("mIsMonetEnabled=" + mIsMonetEnabled);
+        pw.println("mColorScheme=" + mColorScheme);
         pw.println("mNeedsOverlayCreation=" + mNeedsOverlayCreation);
         pw.println("mAcceptColorEvents=" + mAcceptColorEvents);
         pw.println("mDeferredThemeEvaluation=" + mDeferredThemeEvaluation);
diff --git a/packages/SystemUI/src/com/android/systemui/tracing/ProtoTracer.java b/packages/SystemUI/src/com/android/systemui/tracing/ProtoTracer.java
index 8a8f92b..98b2cca 100644
--- a/packages/SystemUI/src/com/android/systemui/tracing/ProtoTracer.java
+++ b/packages/SystemUI/src/com/android/systemui/tracing/ProtoTracer.java
@@ -48,8 +48,13 @@
  * Controller for coordinating winscope proto tracing.
  */
 @SysUISingleton
-public class ProtoTracer implements Dumpable, ProtoTraceParams<MessageNano, SystemUiTraceFileProto,
-        SystemUiTraceEntryProto, SystemUiTraceProto> {
+public class ProtoTracer implements
+        Dumpable,
+        ProtoTraceParams<
+                MessageNano,
+                SystemUiTraceFileProto,
+                SystemUiTraceEntryProto,
+                SystemUiTraceProto> {
 
     private static final String TAG = "ProtoTracer";
     private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
@@ -62,7 +67,7 @@
     public ProtoTracer(Context context, DumpManager dumpManager) {
         mContext = context;
         mProtoTracer = new FrameProtoTracer<>(this);
-        dumpManager.registerDumpable(getClass().getName(), this);
+        dumpManager.registerDumpable(this);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java
index 0a29e04..fe183fc 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java
@@ -38,8 +38,8 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.PluginEnablerImpl;
+import com.android.systemui.shared.plugins.PluginActionManager;
 import com.android.systemui.shared.plugins.PluginEnabler;
-import com.android.systemui.shared.plugins.PluginInstanceManager;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.plugins.PluginPrefs;
 
@@ -102,12 +102,12 @@
         }
 
         List<PackageInfo> apps = pm.getPackagesHoldingPermissions(new String[]{
-                PluginInstanceManager.PLUGIN_PERMISSION},
+                PluginActionManager.PLUGIN_PERMISSION},
                 PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES);
         apps.forEach(app -> {
             if (!plugins.containsKey(app.packageName)) return;
-            if (ArrayUtils.contains(manager.getWhitelistedPlugins(), app.packageName)) {
-                // Don't manage whitelisted plugins, they are part of the OS.
+            if (ArrayUtils.contains(manager.getPrivilegedPlugins(), app.packageName)) {
+                // Don't manage privileged plugins, they are part of the OS.
                 return;
             }
             SwitchPreference pref = new PluginPreference(prefContext, app, mPluginEnabler);
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 3525100..8912448 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -70,6 +70,7 @@
 import com.android.systemui.statusbar.policy.SensorPrivacyController;
 import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
 import com.android.systemui.statusbar.tv.notifications.TvNotificationHandler;
+import com.android.systemui.volume.dagger.VolumeModule;
 
 import javax.inject.Named;
 
@@ -83,7 +84,8 @@
  */
 @Module(includes = {
             PowerModule.class,
-            QSModule.class
+            QSModule.class,
+            VolumeModule.class,
         },
         subcomponents = {
         })
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/ShellUnfoldProgressProvider.kt b/packages/SystemUI/src/com/android/systemui/unfold/ShellUnfoldProgressProvider.kt
new file mode 100644
index 0000000..4a88435
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/ShellUnfoldProgressProvider.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold
+
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener
+import java.util.concurrent.Executor
+
+class ShellUnfoldProgressProvider(
+    private val unfoldProgressProvider: UnfoldTransitionProgressProvider
+) : ShellUnfoldProgressProvider {
+
+    override fun addListener(executor: Executor, listener: UnfoldListener) {
+        unfoldProgressProvider.addCallback(object : TransitionProgressListener {
+            override fun onTransitionStarted() {
+                executor.execute {
+                    listener.onStateChangeStarted()
+                }
+            }
+
+            override fun onTransitionProgress(progress: Float) {
+                executor.execute {
+                    listener.onStateChangeProgress(progress)
+                }
+            }
+
+            override fun onTransitionFinished() {
+                executor.execute {
+                    listener.onStateChangeFinished()
+                }
+            }
+        })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
new file mode 100644
index 0000000..f0760d4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.hardware.devicestate.DeviceStateManager
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener
+import android.hardware.display.DisplayManager
+import android.os.Handler
+import android.os.Trace
+import android.view.Choreographer
+import android.view.Display
+import android.view.DisplayInfo
+import android.view.Surface
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.SurfaceSession
+import android.view.WindowManager
+import android.view.WindowlessWindowManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.statusbar.LightRevealEffect
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.LinearLightRevealEffect
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.wm.shell.displayareahelper.DisplayAreaHelper
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import javax.inject.Inject
+
+@SysUISingleton
+class UnfoldLightRevealOverlayAnimation @Inject constructor(
+    private val context: Context,
+    private val deviceStateManager: DeviceStateManager,
+    private val displayManager: DisplayManager,
+    private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
+    private val displayAreaHelper: Optional<DisplayAreaHelper>,
+    @Main private val executor: Executor,
+    @Main private val handler: Handler,
+    @UiBackground private val backgroundExecutor: Executor
+) {
+
+    private val transitionListener = TransitionListener()
+    private val displayListener = DisplayChangeListener()
+
+    private lateinit var wwm: WindowlessWindowManager
+    private lateinit var unfoldedDisplayInfo: DisplayInfo
+    private lateinit var overlayContainer: SurfaceControl
+
+    private var root: SurfaceControlViewHost? = null
+    private var scrimView: LightRevealScrim? = null
+    private var isFolded: Boolean = false
+    private var isUnfoldHandled: Boolean = true
+
+    private var currentRotation: Int = context.display!!.rotation
+
+    fun init() {
+        deviceStateManager.registerCallback(executor, FoldListener())
+        unfoldTransitionProgressProvider.addCallback(transitionListener)
+
+        val containerBuilder = SurfaceControl.Builder(SurfaceSession())
+            .setContainerLayer()
+            .setName("unfold-overlay-container")
+
+        displayAreaHelper.get().attachToRootDisplayArea(Display.DEFAULT_DISPLAY,
+            containerBuilder) { builder ->
+            executor.execute {
+                overlayContainer = builder.build()
+
+                SurfaceControl.Transaction()
+                    .setLayer(overlayContainer, Integer.MAX_VALUE)
+                    .show(overlayContainer)
+                    .apply()
+
+                wwm = WindowlessWindowManager(context.resources.configuration,
+                    overlayContainer, null)
+            }
+        }
+
+        displayManager.registerDisplayListener(displayListener, handler,
+            DisplayManager.EVENT_FLAG_DISPLAY_CHANGED)
+
+        // Get unfolded display size immediately as 'current display info' might be
+        // not up-to-date during unfolding
+        unfoldedDisplayInfo = getUnfoldedDisplayInfo()
+    }
+
+    /**
+     * Called when screen starts turning on, the contents of the screen might not be visible yet.
+     * This method reports back that the overlay is ready in [onOverlayReady] callback.
+     *
+     * @param onOverlayReady callback when the overlay is drawn and visible on the screen
+     * @see [com.android.systemui.keyguard.KeyguardViewMediator]
+     */
+    fun onScreenTurningOn(onOverlayReady: Runnable) {
+        Trace.beginSection("UnfoldLightRevealOverlayAnimation#onScreenTurningOn")
+        try {
+            // Add the view only if we are unfolding and this is the first screen on
+            if (!isFolded && !isUnfoldHandled) {
+                addView(onOverlayReady)
+                isUnfoldHandled = true
+            } else {
+                // No unfold transition, immediately report that overlay is ready
+                ensureOverlayRemoved()
+                onOverlayReady.run()
+            }
+        } finally {
+            Trace.endSection()
+        }
+    }
+
+    private fun addView(onOverlayReady: Runnable? = null) {
+        if (!::wwm.isInitialized) {
+            // Surface overlay is not created yet on the first SysUI launch
+            onOverlayReady?.run()
+            return
+        }
+
+        ensureOverlayRemoved()
+
+        val newRoot = SurfaceControlViewHost(context, context.display!!, wwm, false)
+        val newView = LightRevealScrim(context, null)
+            .apply {
+                revealEffect = createLightRevealEffect()
+                isScrimOpaqueChangedListener = Consumer {}
+                revealAmount = 0f
+            }
+
+        val params = getLayoutParams()
+        newRoot.setView(newView, params)
+
+        onOverlayReady?.let { callback ->
+            Trace.beginAsyncSection(
+                "UnfoldLightRevealOverlayAnimation#relayout", 0)
+
+            newRoot.relayout(params) { transaction ->
+                val vsyncId = Choreographer.getSfInstance().vsyncId
+
+                backgroundExecutor.execute {
+                    // Apply the transaction that contains the first frame of the overlay
+                    // synchronously and apply another empty transaction with
+                    // 'vsyncId + 1' to make sure that it is actually displayed on
+                    // the screen. The second transaction is necessary to remove the screen blocker
+                    // (turn on the brightness) only when the content is actually visible as it
+                    // might be presented only in the next frame.
+                    // See b/197538198
+                    transaction.setFrameTimelineVsync(vsyncId)
+                        .apply(/* sync */true)
+
+                    transaction
+                        .setFrameTimelineVsync(vsyncId + 1)
+                        .apply(/* sync */ true)
+
+                    Trace.endAsyncSection(
+                        "UnfoldLightRevealOverlayAnimation#relayout", 0)
+                    callback.run()
+                }
+            }
+        }
+
+        scrimView = newView
+        root = newRoot
+    }
+
+    private fun getLayoutParams(): WindowManager.LayoutParams {
+        val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
+
+        val rotation = context.display!!.rotation
+        val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+
+        params.height = if (isNatural)
+            unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth
+        params.width = if (isNatural)
+            unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight
+
+        params.format = PixelFormat.TRANSLUCENT
+        params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
+        params.title = "Unfold Light Reveal Animation"
+        params.layoutInDisplayCutoutMode =
+            WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+        params.fitInsetsTypes = 0
+        params.flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+            or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
+        params.setTrustedOverlay()
+
+        val packageName: String = context.opPackageName
+        params.packageName = packageName
+
+        return params
+    }
+
+    private fun createLightRevealEffect(): LightRevealEffect {
+        val isVerticalFold = currentRotation == Surface.ROTATION_0 ||
+            currentRotation == Surface.ROTATION_180
+        return LinearLightRevealEffect(isVertical = isVerticalFold)
+    }
+
+    private fun ensureOverlayRemoved() {
+        root?.release()
+        root = null
+        scrimView = null
+    }
+
+    private fun getUnfoldedDisplayInfo(): DisplayInfo =
+        displayManager.displays
+            .asSequence()
+            .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
+            .filter { it.type == Display.TYPE_INTERNAL }
+            .maxByOrNull { it.naturalWidth }!!
+
+    private inner class TransitionListener : TransitionProgressListener {
+
+        override fun onTransitionProgress(progress: Float) {
+            scrimView?.revealAmount = progress
+        }
+
+        override fun onTransitionFinished() {
+            ensureOverlayRemoved()
+        }
+
+        override fun onTransitionStarted() {
+            // Add view for folding case (when unfolding the view is added earlier)
+            if (scrimView == null) {
+                addView()
+            }
+        }
+    }
+
+    private inner class DisplayChangeListener : DisplayManager.DisplayListener {
+
+        override fun onDisplayChanged(displayId: Int) {
+            val newRotation: Int = context.display!!.rotation
+            if (currentRotation != newRotation) {
+                currentRotation = newRotation
+                scrimView?.revealEffect = createLightRevealEffect()
+                root?.relayout(getLayoutParams())
+            }
+        }
+
+        override fun onDisplayAdded(displayId: Int) {
+        }
+
+        override fun onDisplayRemoved(displayId: Int) {
+        }
+    }
+
+    private inner class FoldListener : FoldStateListener(context, Consumer { isFolded ->
+        if (isFolded) {
+            ensureOverlayRemoved()
+            isUnfoldHandled = false
+        }
+        this.isFolded = isFolded
+    })
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
new file mode 100644
index 0000000..b23aa20
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold
+
+import android.content.Context
+import android.hardware.SensorManager
+import android.hardware.devicestate.DeviceStateManager
+import android.os.Handler
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.LifecycleScreenStatusProvider
+import com.android.systemui.unfold.config.UnfoldTransitionConfig
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import java.util.Optional
+import java.util.concurrent.Executor
+import javax.inject.Singleton
+
+@Module
+class UnfoldTransitionModule {
+
+    @Provides
+    @Singleton
+    fun provideUnfoldTransitionProgressProvider(
+        context: Context,
+        config: UnfoldTransitionConfig,
+        screenStatusProvider: LifecycleScreenStatusProvider,
+        deviceStateManager: DeviceStateManager,
+        sensorManager: SensorManager,
+        @Main executor: Executor,
+        @Main handler: Handler
+    ): UnfoldTransitionProgressProvider =
+        createUnfoldTransitionProgressProvider(
+            context,
+            config,
+            screenStatusProvider,
+            deviceStateManager,
+            sensorManager,
+            handler,
+            executor
+        )
+
+    @Provides
+    @Singleton
+    fun provideUnfoldTransitionConfig(context: Context): UnfoldTransitionConfig =
+        createConfig(context)
+
+    @Provides
+    @Singleton
+    fun provideShellProgressProvider(
+        config: UnfoldTransitionConfig,
+        provider: Lazy<UnfoldTransitionProgressProvider>
+    ): Optional<ShellUnfoldProgressProvider> =
+        if (config.isEnabled) {
+            Optional.ofNullable(ShellUnfoldProgressProvider(provider.get()))
+        } else {
+            Optional.empty()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionWallpaperController.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionWallpaperController.kt
new file mode 100644
index 0000000..4f45aaf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionWallpaperController.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.util.WallpaperController
+import javax.inject.Inject
+
+@SysUISingleton
+class UnfoldTransitionWallpaperController @Inject constructor(
+    private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
+    private val wallpaperController: WallpaperController
+) {
+
+    fun init() {
+        unfoldTransitionProgressProvider.addCallback(TransitionListener())
+    }
+
+    private inner class TransitionListener : TransitionProgressListener {
+        override fun onTransitionProgress(progress: Float) {
+            // Fully zoomed in when fully unfolded
+            wallpaperController.setUnfoldTransitionZoom(1 - progress)
+        }
+
+        override fun onTransitionFinished() {
+            // Resets wallpaper zoom-out to 0f when fully folded
+            // When fully unfolded it is set to 0f by onTransitionProgress
+            wallpaperController.setUnfoldTransitionZoom(0f)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/AlphaTintDrawableWrapper.java b/packages/SystemUI/src/com/android/systemui/util/AlphaTintDrawableWrapper.java
index a22793b..516efc0 100644
--- a/packages/SystemUI/src/com/android/systemui/util/AlphaTintDrawableWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/util/AlphaTintDrawableWrapper.java
@@ -109,6 +109,12 @@
         }
     }
 
+    @Override
+    public void setTintList(ColorStateList tint) {
+        super.setTintList(tint);
+        mTint = tint;
+    }
+
     private void applyTint() {
         if (getDrawable() != null && mTint != null) {
             getDrawable().mutate().setTintList(mTint);
diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
index 0ba072e..f97f4d0 100644
--- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
+++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
@@ -31,7 +31,6 @@
 
 public class NotificationChannels extends SystemUI {
     public static String ALERTS      = "ALR";
-    public static String SCREENSHOTS_LEGACY = "SCN";
     public static String SCREENSHOTS_HEADSUP = "SCN_HEADSUP";
     public static String GENERAL     = "GEN";
     public static String STORAGE     = "DSK";
@@ -84,18 +83,11 @@
                 general,
                 storage,
                 createScreenshotChannel(
-                        context.getString(R.string.notification_channel_screenshot),
-                        nm.getNotificationChannel(SCREENSHOTS_LEGACY)),
+                        context.getString(R.string.notification_channel_screenshot)),
                 batteryChannel,
                 hint
         ));
 
-        // Delete older SS channel if present.
-        // Screenshots promoted to heads-up in P, this cleans up the lower priority channel from O.
-        // This line can be deleted in Q.
-        nm.deleteNotificationChannel(SCREENSHOTS_LEGACY);
-
-
         if (isTv(context)) {
             // TV specific notification channel for TV PIP controls.
             // Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest
@@ -113,7 +105,7 @@
      * @return
      */
     @VisibleForTesting static NotificationChannel createScreenshotChannel(
-            String name, NotificationChannel legacySS) {
+            String name) {
         NotificationChannel screenshotChannel = new NotificationChannel(SCREENSHOTS_HEADSUP,
                 name, NotificationManager.IMPORTANCE_HIGH); // pop on screen
 
@@ -121,24 +113,6 @@
                 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
         screenshotChannel.setBlockable(true);
 
-        if (legacySS != null) {
-            // Respect any user modified fields from the old channel.
-            int userlock = legacySS.getUserLockedFields();
-            if ((userlock & NotificationChannel.USER_LOCKED_IMPORTANCE) != 0) {
-                screenshotChannel.setImportance(legacySS.getImportance());
-            }
-            if ((userlock & NotificationChannel.USER_LOCKED_SOUND) != 0)  {
-                screenshotChannel.setSound(legacySS.getSound(), legacySS.getAudioAttributes());
-            }
-            if ((userlock & NotificationChannel.USER_LOCKED_VIBRATION) != 0)  {
-                screenshotChannel.setVibrationPattern(legacySS.getVibrationPattern());
-            }
-            if ((userlock & NotificationChannel.USER_LOCKED_LIGHTS) != 0)  {
-                screenshotChannel.setLightColor(legacySS.getLightColor());
-            }
-            // skip show_badge, irrelevant for system channel
-        }
-
         return screenshotChannel;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt
index 3b64f9f..1059d6c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt
@@ -69,6 +69,10 @@
         return super.getChangingConfigurations() or ActivityInfo.CONFIG_DENSITY
     }
 
+    override fun canApplyTheme(): Boolean {
+        return (drawable?.canApplyTheme() ?: false) || super.canApplyTheme()
+    }
+
     private class RoundedCornerState(private val wrappedState: ConstantState) : ConstantState() {
         override fun newDrawable(): Drawable {
             return newDrawable(null, null)
@@ -82,5 +86,9 @@
         override fun getChangingConfigurations(): Int {
             return wrappedState.changingConfigurations
         }
+
+        override fun canApplyTheme(): Boolean {
+            return true
+        }
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java
index bf00667..a1cdfd8 100644
--- a/packages/SystemUI/src/com/android/systemui/util/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java
@@ -29,7 +29,6 @@
 import com.android.systemui.R;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 
 import java.util.List;
 import java.util.function.Consumer;
@@ -161,13 +160,11 @@
     }
 
     /**
-     * Returns true if the device should use the split notification shade, based on feature flags,
-     * orientation and screen width.
+     * Returns true if the device should use the split notification shade, based on orientation and
+     * screen width.
      */
-    public static boolean shouldUseSplitNotificationShade(FeatureFlags featureFlags,
-            Resources resources) {
-        return featureFlags.isTwoColumnNotificationShadeEnabled()
-                && resources.getBoolean(R.bool.config_use_split_notification_shade);
+    public static boolean shouldUseSplitNotificationShade(Resources resources) {
+        return resources.getBoolean(R.bool.config_use_split_notification_shade);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
new file mode 100644
index 0000000..db2aca8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import android.app.WallpaperInfo
+import android.app.WallpaperManager
+import android.util.Log
+import android.view.View
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlin.math.max
+
+private const val TAG = "WallpaperController"
+
+@SysUISingleton
+class WallpaperController @Inject constructor(private val wallpaperManager: WallpaperManager) {
+
+    var rootView: View? = null
+
+    private var notificationShadeZoomOut: Float = 0f
+    private var unfoldTransitionZoomOut: Float = 0f
+
+    private var wallpaperInfo: WallpaperInfo? = null
+
+    fun onWallpaperInfoUpdated(wallpaperInfo: WallpaperInfo?) {
+        this.wallpaperInfo = wallpaperInfo
+    }
+
+    private val shouldUseDefaultUnfoldTransition: Boolean
+        get() = wallpaperInfo?.shouldUseDefaultUnfoldTransition()
+            ?: true
+
+    fun setNotificationShadeZoom(zoomOut: Float) {
+        notificationShadeZoomOut = zoomOut
+        updateZoom()
+    }
+
+    fun setUnfoldTransitionZoom(zoomOut: Float) {
+        if (shouldUseDefaultUnfoldTransition) {
+            unfoldTransitionZoomOut = zoomOut
+            updateZoom()
+        }
+    }
+
+    private fun updateZoom() {
+        setWallpaperZoom(max(notificationShadeZoomOut, unfoldTransitionZoomOut))
+    }
+
+    private fun setWallpaperZoom(zoomOut: Float) {
+        try {
+            rootView?.let { root ->
+                if (root.isAttachedToWindow && root.windowToken != null) {
+                    wallpaperManager.setWallpaperZoomOut(root.windowToken, zoomOut)
+                } else {
+                    Log.i(TAG, "Won't set zoom. Window not attached $root")
+                }
+            }
+        } catch (e: IllegalArgumentException) {
+            Log.w(TAG, "Can't set zoom. Window is gone: ${rootView?.windowToken}", e)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java
index 1c50496..107fe87 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java
@@ -22,8 +22,10 @@
 
 import com.android.systemui.dagger.qualifiers.Main;
 
+import java.util.Optional;
 import java.util.concurrent.Executor;
 
+import javax.inject.Named;
 import javax.inject.Singleton;
 
 import dagger.Binds;
@@ -35,6 +37,7 @@
  */
 @Module
 public abstract class GlobalConcurrencyModule {
+    public static final String PRE_HANDLER = "pre_handler";
 
     /**
      * Binds {@link ThreadFactoryImpl} to {@link ThreadFactory}.
@@ -64,13 +67,32 @@
      * Provide a Main-Thread Executor.
      */
     @Provides
+    @Singleton
     @Main
     public static Executor provideMainExecutor(Context context) {
         return context.getMainExecutor();
     }
 
+    /**
+     * Provide a Main-Thread DelayableExecutor.
+     */
+    @Provides
+    @Singleton
+    @Main
+    public static DelayableExecutor provideMainDelayableExecutor(@Main Looper looper) {
+        return new ExecutorImpl(looper);
+    }
+
+
     /** */
     @Binds
     @Singleton
     public abstract Execution provideExecution(ExecutionImpl execution);
+
+    /** */
+    @Provides
+    @Named(PRE_HANDLER)
+    public static Optional<Thread.UncaughtExceptionHandler> providesUncaughtExceptionHandler() {
+        return Optional.ofNullable(Thread.getUncaughtExceptionPreHandler());
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouter.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouter.java
new file mode 100644
index 0000000..542cf65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouter.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.concurrency;
+
+/**
+ * Allows triggering methods based on a passed in id or message, generally on another thread.
+ *
+ * Messages sent on to this router must be processed in order. That is to say, if three
+ * messages are sent with no delay, they must be processed in the order they were sent. Moreover,
+ * if messages are sent with various delays, they must be processed in order of their delay.
+ *
+ *  Messages can be passed by either a simple integer or an instance of a class. Unique integers are
+ *  considered unique messages. Unique message classes (not instances) are considered unique
+ *  messages. You can use message classes to pass extra data for processing to subscribers.
+ *
+ *  <pre>
+ *      // Three messages with three unique integer messages.
+ *      // They can be subscribed to independently.
+ *      router.sendMessage(0);
+ *      router.sendMessage(1);
+ *      router.sendMessage(2);
+ *
+ *      // Three messages with two unique message classes.
+ *      // The first and third messages will be delivered to the same subscribers.
+ *      router.sendMessage(new Foo(0));
+ *      router.sendMessage(new Bar(1));
+ *      router.sendMessage(new Foo(2));
+ *  </pre>
+ *
+ * The number of unique ids and message types used should be relatively constrained. Construct
+ * a custom message-class and put unique, per-message data inside of it.
+ */
+public interface MessageRouter {
+    /**
+     * Alerts any listeners subscribed to the passed in id.
+     *
+     * The number of unique ids used should be relatively constrained - used to identify the type
+     * of message being sent. If unique information needs to be passed with each call, use
+     * {@link #sendMessage(Object)}.
+     *
+     * @param id An identifier for the message
+     */
+    default void sendMessage(int id) {
+        sendMessageDelayed(id, 0);
+    }
+
+    /**
+     * Alerts any listeners subscribed to the passed in message.
+     *
+     * The number of message types used should be relatively constrained. If no unique information
+     * needs to be passed in, you can simply use {@link #sendMessage(int)}} which takes an integer
+     * instead of a unique class type.
+     *
+     * The class of the passed in object will be used to router the message.
+     *
+     * @param data A message containing extra data for processing.
+     */
+    default void sendMessage(Object data) {
+        sendMessageDelayed(data, 0);
+    }
+
+    /**
+     * Alerts any listeners subscribed to the passed in id in the future.
+     *
+     * The number of unique ids used should be relatively constrained - used to identify the type
+     * of message being sent. If unique information needs to be passed with each call, use
+     * {@link #sendMessageDelayed(Object, long)}.
+     *
+     * @param id An identifier for the message
+     * @param delayMs Number of milliseconds to wait before alerting.
+     */
+    void sendMessageDelayed(int id, long delayMs);
+
+
+    /**
+     * Alerts any listeners subscribed to the passed in message in the future.
+     *
+     * The number of message types used should be relatively constrained. If no unique information
+     * needs to be passed in, you can simply use {@link #sendMessageDelayed(int, long)} which takes
+     * an integer instead of a unique class type.
+     *
+     * @param data A message containing extra data for processing.
+     * @param delayMs Number of milliseconds to wait before alerting.
+     */
+    void sendMessageDelayed(Object data, long delayMs);
+
+    /**
+     * Cancel all unprocessed messages for a given id.
+     *
+     * If a message has multiple listeners and one of those listeners has been alerted, the other
+     * listeners that follow it may also be alerted. This is only guaranteed to cancel messages
+     * that are still queued.
+     *
+     * @param id The message id to cancel.
+     */
+    void cancelMessages(int id);
+
+    /**
+     * Cancel all unprocessed messages for a given message type.
+     *
+     * If a message has multiple listeners and one of those listeners has been alerted, the other
+     * listeners that follow it may also be alerted. This is only guaranteed to cancel messages
+     * that are still queued.
+     *
+     * @param messageType The class of the message to cancel
+     */
+    <T> void cancelMessages(Class<T> messageType);
+
+    /**
+     * Add a listener for a message that does not handle any extra data.
+     *
+     * See also {@link #subscribeTo(Class, DataMessageListener)}.
+     *
+     * @param id The message id to listener for.
+     * @param listener
+     */
+    void subscribeTo(int id, SimpleMessageListener listener);
+
+    /**
+     * Add a listener for a message of a specific type.
+     *
+     * See also {@link #subscribeTo(Class, DataMessageListener)}.
+     *
+     * @param messageType The class of message to listen for.
+     * @param listener
+     */
+    <T> void subscribeTo(Class<T> messageType, DataMessageListener<T> listener);
+
+    /**
+     * Remove a listener for a specific message.
+     *
+     * See also {@link #unsubscribeFrom(Class, DataMessageListener)}
+     *
+     * @param id The message id to stop listening for.
+     * @param listener The listener to remove.
+     */
+    void unsubscribeFrom(int id, SimpleMessageListener listener);
+
+    /**
+     * Remove a listener for a specific message.
+     *
+     * See also {@link #unsubscribeFrom(int, SimpleMessageListener)}.
+     *
+     * @param messageType The class of message to stop listening for.
+     * @param listener The listener to remove.
+     */
+    <T> void unsubscribeFrom(Class<T> messageType, DataMessageListener<T> listener);
+
+    /**
+     * Remove a listener for all messages that it is subscribed to.
+     *
+     * See also {@link #unsubscribeFrom(DataMessageListener)}.
+     *
+     * @param listener The listener to remove.
+     */
+    void unsubscribeFrom(SimpleMessageListener listener);
+
+    /**
+     * Remove a listener for all messages that it is subscribed to.
+     *
+     * See also {@link #unsubscribeFrom(SimpleMessageListener)}.
+     *
+     * @param listener The listener to remove.
+     */
+    <T> void unsubscribeFrom(DataMessageListener<T> listener);
+
+    /**
+     * A Listener interface for when no extra data is expected or desired.
+     */
+    interface SimpleMessageListener {
+        /** */
+        void onMessage(int id);
+    }
+
+    /**
+     * A Listener interface for when extra data is expected or desired.
+     *
+     * @param <T>
+     */
+    interface DataMessageListener<T> {
+        /** */
+        void onMessage(T data);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouterImpl.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouterImpl.java
new file mode 100644
index 0000000..7145c77
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/MessageRouterImpl.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.concurrency;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of {@link MessageRouter}.
+ */
+public class MessageRouterImpl implements MessageRouter {
+
+    private final DelayableExecutor mDelayableExecutor;
+
+    private final Map<Integer, List<Runnable>> mIdMessageCancelers = new HashMap<>();
+    private final Map<Class<Object>, List<Runnable>> mDataMessageCancelers = new HashMap<>();
+    private final Map<Integer, List<SimpleMessageListener>> mSimpleMessageListenerMap =
+            new HashMap<>();
+    private final Map<Class<?>, List<DataMessageListener<Object>>> mDataMessageListenerMap =
+            new HashMap<>();
+
+    public MessageRouterImpl(DelayableExecutor delayableExecutor) {
+        mDelayableExecutor = delayableExecutor;
+    }
+
+    @Override
+    public void sendMessageDelayed(int id, long delayMs) {
+        addCanceler(id, mDelayableExecutor.executeDelayed(() -> onMessage(id), delayMs));
+    }
+
+    @Override
+    public void sendMessageDelayed(Object data, long delayMs) {
+        addCanceler((Class<Object>) data.getClass(), mDelayableExecutor.executeDelayed(
+                () -> onMessage(data), delayMs));
+    }
+
+    @Override
+    public void cancelMessages(int id) {
+        synchronized (mIdMessageCancelers) {
+            if (mIdMessageCancelers.containsKey(id)) {
+                for (Runnable canceler : mIdMessageCancelers.get(id)) {
+                    canceler.run();
+                }
+                // Remove, don't clear, otherwise this could look like a memory leak as
+                // more and more unique message ids are passed in.
+                mIdMessageCancelers.remove(id);
+            }
+        }
+    }
+
+    @Override
+    public <T> void cancelMessages(Class<T> messageType) {
+        synchronized (mDataMessageCancelers) {
+            if (mDataMessageCancelers.containsKey(messageType)) {
+                for (Runnable canceler : mDataMessageCancelers.get(messageType)) {
+                    canceler.run();
+                }
+                // Remove, don't clear, otherwise this could look like a memory leak as
+                // more and more unique message types are passed in.
+                mDataMessageCancelers.remove(messageType);
+            }
+        }
+    }
+
+    @Override
+    public void subscribeTo(int id, SimpleMessageListener listener) {
+        synchronized (mSimpleMessageListenerMap) {
+            mSimpleMessageListenerMap.putIfAbsent(id, new ArrayList<>());
+            mSimpleMessageListenerMap.get(id).add(listener);
+        }
+    }
+
+    @Override
+    public <T> void subscribeTo(Class<T> messageType, DataMessageListener<T> listener) {
+        synchronized (mDataMessageListenerMap) {
+            mDataMessageListenerMap.putIfAbsent(messageType, new ArrayList<>());
+            mDataMessageListenerMap.get(messageType).add((DataMessageListener<Object>) listener);
+        }
+    }
+
+    @Override
+    public void unsubscribeFrom(int id, SimpleMessageListener listener) {
+        synchronized (mSimpleMessageListenerMap) {
+            if (mSimpleMessageListenerMap.containsKey(id)) {
+                mSimpleMessageListenerMap.get(id).remove(listener);
+            }
+        }
+    }
+
+    @Override
+    public <T> void unsubscribeFrom(Class<T> messageType, DataMessageListener<T> listener) {
+        synchronized (mDataMessageListenerMap) {
+            if (mDataMessageListenerMap.containsKey(messageType)) {
+                mDataMessageListenerMap.get(messageType).remove(listener);
+            }
+        }
+    }
+
+    @Override
+    public void unsubscribeFrom(SimpleMessageListener listener) {
+        synchronized (mSimpleMessageListenerMap) {
+            for (Integer id : mSimpleMessageListenerMap.keySet()) {
+                mSimpleMessageListenerMap.get(id).remove(listener);
+            }
+        }
+    }
+
+    @Override
+    public <T> void unsubscribeFrom(DataMessageListener<T> listener) {
+        synchronized (mDataMessageListenerMap) {
+            for (Class<?> messageType : mDataMessageListenerMap.keySet()) {
+                mDataMessageListenerMap.get(messageType).remove(listener);
+            }
+        }
+    }
+
+    private void addCanceler(int id, Runnable canceler) {
+        synchronized (mIdMessageCancelers) {
+            mIdMessageCancelers.putIfAbsent(id, new ArrayList<>());
+            mIdMessageCancelers.get(id).add(canceler);
+        }
+    }
+
+    private void addCanceler(Class<Object> data, Runnable canceler) {
+        synchronized (mDataMessageCancelers) {
+            mDataMessageCancelers.putIfAbsent(data, new ArrayList<>());
+            mDataMessageCancelers.get(data).add(canceler);
+        }
+    }
+
+    private void onMessage(int id) {
+        synchronized (mSimpleMessageListenerMap) {
+            if (mSimpleMessageListenerMap.containsKey(id)) {
+                for (SimpleMessageListener listener : mSimpleMessageListenerMap.get(id)) {
+                    listener.onMessage(id);
+                }
+            }
+        }
+
+        synchronized (mIdMessageCancelers) {
+            if (mIdMessageCancelers.containsKey(id) && !mIdMessageCancelers.get(id).isEmpty()) {
+                mIdMessageCancelers.get(id).remove(0);
+                if (mIdMessageCancelers.get(id).isEmpty()) {
+                    mIdMessageCancelers.remove(id);
+                }
+            }
+        }
+    }
+
+    private void onMessage(Object data) {
+        synchronized (mDataMessageListenerMap) {
+            if (mDataMessageListenerMap.containsKey(data.getClass())) {
+                for (DataMessageListener<Object> listener : mDataMessageListenerMap.get(
+                        data.getClass())) {
+                    listener.onMessage(data);
+                }
+            }
+        }
+
+        synchronized (mDataMessageCancelers) {
+            if (mDataMessageCancelers.containsKey(data.getClass())
+                    && !mDataMessageCancelers.get(data.getClass()).isEmpty()) {
+                mDataMessageCancelers.get(data.getClass()).remove(0);
+                if (mDataMessageCancelers.get(data.getClass()).isEmpty()) {
+                    mDataMessageCancelers.remove(data.getClass());
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
index b9b20c7..e8a9bc7 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
@@ -120,16 +120,6 @@
     }
 
     /**
-     * Provide a Main-Thread Executor.
-     */
-    @Provides
-    @SysUISingleton
-    @Main
-    public static DelayableExecutor provideMainDelayableExecutor(@Main Looper looper) {
-        return new ExecutorImpl(looper);
-    }
-
-    /**
      * Provide a Background-Thread Executor by default.
      */
     @Provides
@@ -170,4 +160,20 @@
     public static Executor provideUiBackgroundExecutor() {
         return Executors.newSingleThreadExecutor();
     }
+
+    /** */
+    @Provides
+    @Main
+    public static MessageRouter providesMainMessageRouter(
+            @Main DelayableExecutor executor) {
+        return new MessageRouterImpl(executor);
+    }
+
+    /** */
+    @Provides
+    @Background
+    public static MessageRouter providesBackgroundMessageRouter(
+            @Background DelayableExecutor executor) {
+        return new MessageRouterImpl(executor);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
index cdfa145..981bf01 100644
--- a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
@@ -18,12 +18,15 @@
 
 import com.android.systemui.util.RingerModeTracker;
 import com.android.systemui.util.RingerModeTrackerImpl;
+import com.android.systemui.util.wrapper.UtilWrapperModule;
 
 import dagger.Binds;
 import dagger.Module;
 
 /** Dagger Module for code in the util package. */
-@Module
+@Module(includes = {
+                UtilWrapperModule.class
+        })
 public interface UtilModule {
     /** */
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
index edea305..4318994 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
@@ -36,7 +36,6 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.Message;
 import android.os.Process;
 import android.os.SystemProperties;
 import android.provider.Settings;
@@ -52,6 +51,7 @@
 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.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QSTile;
@@ -60,6 +60,8 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSIconViewImpl;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.concurrency.MessageRouter;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -110,18 +112,18 @@
     private static final int DO_GARBAGE_INSPECTION = 1000;
     private static final int DO_HEAP_TRACK = 3000;
 
-    private static final int GARBAGE_ALLOWANCE = 5;
+    static final int GARBAGE_ALLOWANCE = 5;
 
     private static final String TAG = "GarbageMonitor";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    private final Handler mHandler;
+    private final MessageRouter mMessageRouter;
     private final TrackedGarbage mTrackedGarbage;
     private final LeakReporter mLeakReporter;
     private final Context mContext;
-    private final ActivityManager mAm;
+    private final DelayableExecutor mDelayableExecutor;
     private MemoryTile mQSTile;
-    private DumpTruck mDumpTruck;
+    private final DumpTruck mDumpTruck;
 
     private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
     private final ArrayList<Long> mPids = new ArrayList<>();
@@ -133,19 +135,25 @@
     @Inject
     public GarbageMonitor(
             Context context,
-            @Background Looper bgLooper,
+            @Background DelayableExecutor delayableExecutor,
+            @Background MessageRouter messageRouter,
             LeakDetector leakDetector,
-            LeakReporter leakReporter) {
+            LeakReporter leakReporter,
+            DumpManager dumpManager) {
         mContext = context.getApplicationContext();
-        mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
 
-        mHandler = new BackgroundHeapCheckHandler(bgLooper);
+        mDelayableExecutor = delayableExecutor;
+        mMessageRouter = messageRouter;
+        mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection);
+        mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack);
 
         mTrackedGarbage = leakDetector.getTrackedGarbage();
         mLeakReporter = leakReporter;
 
         mDumpTruck = new DumpTruck(mContext);
 
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
         if (ENABLE_AM_HEAP_LIMIT) {
             mHeapLimit = Settings.Global.getInt(context.getContentResolver(),
                     SETTINGS_KEY_AM_HEAP_LIMIT,
@@ -158,13 +166,13 @@
             return;
         }
 
-        mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION);
+        mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION);
     }
 
     public void startHeapTracking() {
         startTrackingProcess(
                 android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
-        mHandler.sendEmptyMessage(DO_HEAP_TRACK);
+        mMessageRouter.sendMessage(DO_HEAP_TRACK);
     }
 
     private boolean gcAndCheckGarbage() {
@@ -586,33 +594,18 @@
         }
     }
 
-    private class BackgroundHeapCheckHandler extends Handler {
-        BackgroundHeapCheckHandler(Looper onLooper) {
-            super(onLooper);
-            if (Looper.getMainLooper().equals(onLooper)) {
-                throw new RuntimeException(
-                        "BackgroundHeapCheckHandler may not run on the ui thread");
-            }
+    private void doGarbageInspection(int id) {
+        if (gcAndCheckGarbage()) {
+            mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100);
         }
 
-        @Override
-        public void handleMessage(Message m) {
-            switch (m.what) {
-                case DO_GARBAGE_INSPECTION:
-                    if (gcAndCheckGarbage()) {
-                        postDelayed(GarbageMonitor.this::reinspectGarbageAfterGc, 100);
-                    }
+        mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION);
+        mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
+    }
 
-                    removeMessages(DO_GARBAGE_INSPECTION);
-                    sendEmptyMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
-                    break;
-
-                case DO_HEAP_TRACK:
-                    update();
-                    removeMessages(DO_HEAP_TRACK);
-                    sendEmptyMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
-                    break;
-            }
-        }
+    private void doHeapTrack(int id) {
+        update();
+        mMessageRouter.cancelMessages(DO_HEAP_TRACK);
+        mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
index c50e8f8..f215082 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
@@ -21,6 +21,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.systemui.Dumpable;
+import com.android.systemui.dump.DumpManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -38,12 +39,16 @@
     private final TrackedObjects mTrackedObjects;
 
     @VisibleForTesting
-    public LeakDetector(TrackedCollections trackedCollections,
+    public LeakDetector(
+            TrackedCollections trackedCollections,
             TrackedGarbage trackedGarbage,
-            TrackedObjects trackedObjects) {
+            TrackedObjects trackedObjects,
+            DumpManager dumpManager) {
         mTrackedCollections = trackedCollections;
         mTrackedGarbage = trackedGarbage;
         mTrackedObjects = trackedObjects;
+
+        dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     /**
@@ -130,13 +135,16 @@
         pw.println();
     }
 
-    public static LeakDetector create() {
+    public static LeakDetector create(DumpManager dumpManager) {
         if (ENABLED) {
             TrackedCollections collections = new TrackedCollections();
-            return new LeakDetector(collections, new TrackedGarbage(collections),
-                    new TrackedObjects(collections));
+            return new LeakDetector(
+                    collections,
+                    new TrackedGarbage(collections),
+                    new TrackedObjects(collections),
+                    dumpManager);
         } else {
-            return new LeakDetector(null, null, null);
+            return new LeakDetector(null, null, null, dumpManager);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/PostureDependentProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/PostureDependentProximitySensor.java
new file mode 100644
index 0000000..40982bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/PostureDependentProximitySensor.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import android.util.Log;
+
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.concurrency.Execution;
+
+import javax.inject.Inject;
+
+/**
+ * Proximity sensor that changes proximity sensor usage based on the current posture.
+ * Posture -> prox sensor mapping can be found in SystemUI config overlays at:
+ *   - proximity_sensor_posture_mapping
+ *   - proximity_sensor_secondary_posture_mapping.
+ * where the array indices correspond to the following postures:
+ *     [UNKNOWN, CLOSED, HALF_OPENED, OPENED]
+ */
+class PostureDependentProximitySensor extends ProximitySensorImpl {
+    private final ThresholdSensor[] mPostureToPrimaryProxSensorMap;
+    private final ThresholdSensor[] mPostureToSecondaryProxSensorMap;
+
+    @Inject
+    PostureDependentProximitySensor(
+            @PrimaryProxSensor ThresholdSensor[] postureToPrimaryProxSensorMap,
+            @SecondaryProxSensor ThresholdSensor[] postureToSecondaryProxSensorMap,
+            @Main DelayableExecutor delayableExecutor,
+            Execution execution,
+            DevicePostureController devicePostureController
+    ) {
+        super(
+                postureToPrimaryProxSensorMap[0],
+                postureToSecondaryProxSensorMap[0],
+                delayableExecutor,
+                execution
+        );
+        mPostureToPrimaryProxSensorMap = postureToPrimaryProxSensorMap;
+        mPostureToSecondaryProxSensorMap = postureToSecondaryProxSensorMap;
+        mDevicePosture = devicePostureController.getDevicePosture();
+        devicePostureController.addCallback(mDevicePostureCallback);
+
+        chooseSensors();
+    }
+    private void chooseSensors() {
+        if (mDevicePosture >= mPostureToPrimaryProxSensorMap.length
+                || mDevicePosture >= mPostureToSecondaryProxSensorMap.length) {
+            Log.e("PostureDependentProxSensor",
+                    "unsupported devicePosture=" + mDevicePosture);
+            return;
+        }
+
+        ThresholdSensor newPrimaryProx = mPostureToPrimaryProxSensorMap[mDevicePosture];
+        ThresholdSensor newSecondaryProx = mPostureToSecondaryProxSensorMap[mDevicePosture];
+
+        if (newPrimaryProx != mPrimaryThresholdSensor
+                || newSecondaryProx != mSecondaryThresholdSensor) {
+            logDebug("Register new proximity sensors newPosture="
+                    + DevicePostureController.devicePostureToString(mDevicePosture));
+            unregisterInternal();
+
+            if (mPrimaryThresholdSensor != null) {
+                mPrimaryThresholdSensor.unregister(mPrimaryEventListener);
+            }
+            if (mSecondaryThresholdSensor != null) {
+                mSecondaryThresholdSensor.unregister(mSecondaryEventListener);
+            }
+
+            mPrimaryThresholdSensor = newPrimaryProx;
+            mSecondaryThresholdSensor = newSecondaryProx;
+
+            mInitializedListeners = false;
+            registerInternal();
+        }
+    }
+
+    private final DevicePostureController.Callback mDevicePostureCallback =
+            posture -> {
+                if (mDevicePosture == posture) {
+                    return;
+                }
+
+                mDevicePosture = posture;
+                chooseSensors();
+            };
+
+    @Override
+    public String toString() {
+        return String.format("{posture=%s, proximitySensor=%s}",
+                DevicePostureController.devicePostureToString(mDevicePosture), super.toString());
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java
new file mode 100644
index 0000000..a8a6341
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * Convenience class allowing for briefly checking the proximity sensor.
+ */
+public class ProximityCheck implements Runnable {
+
+    private final ProximitySensor mSensor;
+    private final DelayableExecutor mDelayableExecutor;
+    private List<Consumer<Boolean>> mCallbacks = new ArrayList<>();
+    private final ThresholdSensor.Listener mListener;
+    private final AtomicBoolean mRegistered = new AtomicBoolean();
+
+    @Inject
+    public ProximityCheck(
+            ProximitySensor sensor,
+            @Main DelayableExecutor delayableExecutor) {
+        mSensor = sensor;
+        mSensor.setTag("prox_check");
+        mDelayableExecutor = delayableExecutor;
+        mListener = this::onProximityEvent;
+    }
+
+    /** Set a descriptive tag for the sensors registration. */
+    public void setTag(String tag) {
+        mSensor.setTag(tag);
+    }
+
+    @Override
+    public void run() {
+        unregister();
+        onProximityEvent(null);
+    }
+
+    /**
+     * Query the proximity sensor, timing out if no result.
+     */
+    public void check(long timeoutMs, Consumer<Boolean> callback) {
+        if (!mSensor.isLoaded()) {
+            callback.accept(null);
+            return;
+        }
+        mCallbacks.add(callback);
+        if (!mRegistered.getAndSet(true)) {
+            mSensor.register(mListener);
+            mDelayableExecutor.executeDelayed(this, timeoutMs);
+        }
+    }
+
+    private void unregister() {
+        mSensor.unregister(mListener);
+        mRegistered.set(false);
+    }
+
+    private void onProximityEvent(ThresholdSensorEvent proximityEvent) {
+        mCallbacks.forEach(
+                booleanConsumer ->
+                        booleanConsumer.accept(
+                                proximityEvent == null ? null : proximityEvent.getBelow()));
+        mCallbacks.clear();
+        unregister();
+        mRegistered.set(false);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
index bd11039..d3f1c93 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,149 +16,24 @@
 
 package com.android.systemui.util.sensors;
 
-import android.hardware.SensorManager;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.concurrency.Execution;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-
-import javax.inject.Inject;
-
 /**
- * Wrapper around SensorManager customized for the Proximity sensor.
- *
- * The ProximitySensor supports the concept of a primary and a
- * secondary hardware sensor. The primary sensor is used for a first
- * pass check if the phone covered. When triggered, it then checks
- * the secondary sensor for confirmation (if there is one). It does
- * not send a proximity event until the secondary sensor confirms (or
- * rejects) the reading. The secondary sensor is, in fact, the source
- * of truth.
- *
- * This is necessary as sometimes keeping the secondary sensor on for
- * extends periods is undesirable. It may, however, result in increased
- * latency for proximity readings.
- *
- * Phones should configure this via a config.xml overlay. If no
- * proximity sensor is set (primary or secondary) we fall back to the
- * default Sensor.TYPE_PROXIMITY. If proximity_sensor_type is set in
- * config.xml, that will be used as the primary sensor. If
- * proximity_sensor_secondary_type is set, that will function as the
- * secondary sensor. If no secondary is set, only the primary will be
- * used.
+ * Wrapper class for a dual ProximitySensor which supports a secondary sensor gated upon the
+ * primary).
  */
-public class ProximitySensor implements ThresholdSensor {
-    private static final String TAG = "ProxSensor";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    private static final long SECONDARY_PING_INTERVAL_MS = 5000;
-
-    private final ThresholdSensor mPrimaryThresholdSensor;
-    private final ThresholdSensor mSecondaryThresholdSensor;
-    private final DelayableExecutor mDelayableExecutor;
-    private final Execution mExecution;
-    private final List<ThresholdSensor.Listener> mListeners = new ArrayList<>();
-    private String mTag = null;
-    @VisibleForTesting protected boolean mPaused;
-    private ThresholdSensorEvent mLastPrimaryEvent;
-    @VisibleForTesting
-    ThresholdSensorEvent mLastEvent;
-    private boolean mRegistered;
-    private final AtomicBoolean mAlerting = new AtomicBoolean();
-    private Runnable mCancelSecondaryRunnable;
-    private boolean mInitializedListeners = false;
-    private boolean mSecondarySafe = false;
-
-    private final ThresholdSensor.Listener mPrimaryEventListener = this::onPrimarySensorEvent;
-
-    private final ThresholdSensor.Listener mSecondaryEventListener =
-            new ThresholdSensor.Listener() {
-        @Override
-        public void onThresholdCrossed(ThresholdSensorEvent event) {
-            // If we no longer have a "below" signal and the secondary sensor is not
-            // considered "safe", then we need to turn it off.
-            if (!mSecondarySafe
-                    && (mLastPrimaryEvent == null
-                    || !mLastPrimaryEvent.getBelow()
-                    || !event.getBelow())) {
-                chooseSensor();
-                if (mLastPrimaryEvent == null || !mLastPrimaryEvent.getBelow()) {
-                    // Only check the secondary as long as the primary thinks we're near.
-                    if (mCancelSecondaryRunnable != null) {
-                        mCancelSecondaryRunnable.run();
-                        mCancelSecondaryRunnable = null;
-                    }
-                    return;
-                } else {
-                    // Check this sensor again in a moment.
-                    mCancelSecondaryRunnable = mDelayableExecutor.executeDelayed(() -> {
-                        // This is safe because we know that mSecondaryThresholdSensor
-                        // is loaded, otherwise we wouldn't be here.
-                        mPrimaryThresholdSensor.pause();
-                        mSecondaryThresholdSensor.resume();
-                    },
-                        SECONDARY_PING_INTERVAL_MS);
-                }
-            }
-            logDebug("Secondary sensor event: " + event.getBelow() + ".");
-
-            if (!mPaused) {
-                onSensorEvent(event);
-            }
-        }
-    };
-
-    @Inject
-    public ProximitySensor(
-            @PrimaryProxSensor ThresholdSensor primary,
-            @SecondaryProxSensor ThresholdSensor  secondary,
-            @Main DelayableExecutor delayableExecutor,
-            Execution execution) {
-        mPrimaryThresholdSensor = primary;
-        mSecondaryThresholdSensor = secondary;
-        mDelayableExecutor = delayableExecutor;
-        mExecution = execution;
-    }
-
-    @Override
-    public void setTag(String tag) {
-        mTag = tag;
-        mPrimaryThresholdSensor.setTag(tag + ":primary");
-        mSecondaryThresholdSensor.setTag(tag + ":secondary");
-    }
-
-    @Override
-    public void setDelay(int delay) {
-        mExecution.assertIsMainThread();
-        mPrimaryThresholdSensor.setDelay(delay);
-        mSecondaryThresholdSensor.setDelay(delay);
-    }
+public interface ProximitySensor extends ThresholdSensor {
+    /**
+     * Returns true if we are registered with the SensorManager.
+     */
+    boolean isRegistered();
 
     /**
-     * Unregister with the {@link SensorManager} without unsetting listeners on this object.
+     * Whether the proximity sensor reports near. Can return null if no information has been
+     * received yet.
      */
-    @Override
-    public void pause() {
-        mExecution.assertIsMainThread();
-        mPaused = true;
-        unregisterInternal();
-    }
+    Boolean isNear();
 
-    /**
-     * Register with the {@link SensorManager}. No-op if no listeners are registered on this object.
-     */
-    @Override
-    public void resume() {
-        mExecution.assertIsMainThread();
-        mPaused = false;
-        registerInternal();
-    }
+    /** Update all listeners with the last value this class received from the sensor. */
+    void alertListeners();
 
     /**
      * Sets that it is safe to leave the secondary sensor on indefinitely.
@@ -166,249 +41,5 @@
      * The secondary sensor will be turned on if there are any registered listeners, regardless
      * of what is reported by the primary sensor.
      */
-    public void setSecondarySafe(boolean safe) {
-        mSecondarySafe = mSecondaryThresholdSensor.isLoaded() && safe;
-        chooseSensor();
-    }
-
-    /**
-     * Returns true if we are registered with the SensorManager.
-     */
-    public boolean isRegistered() {
-        return mRegistered;
-    }
-
-    /**
-     * Returns {@code false} if a Proximity sensor is not available.
-     */
-    @Override
-    public boolean isLoaded() {
-        return mPrimaryThresholdSensor.isLoaded();
-    }
-
-    /**
-     * Add a listener.
-     *
-     * Registers itself with the {@link SensorManager} if this is the first listener
-     * added. If the ProximitySensor is paused, it will be registered when resumed.
-     */
-    @Override
-    public void register(ThresholdSensor.Listener listener) {
-        mExecution.assertIsMainThread();
-        if (!isLoaded()) {
-            return;
-        }
-
-        if (mListeners.contains(listener)) {
-            logDebug("ProxListener registered multiple times: " + listener);
-        } else {
-            mListeners.add(listener);
-        }
-        registerInternal();
-    }
-
-    protected void registerInternal() {
-        mExecution.assertIsMainThread();
-        if (mRegistered || mPaused || mListeners.isEmpty()) {
-            return;
-        }
-        if (!mInitializedListeners) {
-            mPrimaryThresholdSensor.pause();
-            mSecondaryThresholdSensor.pause();
-            mPrimaryThresholdSensor.register(mPrimaryEventListener);
-            mSecondaryThresholdSensor.register(mSecondaryEventListener);
-            mInitializedListeners = true;
-        }
-        logDebug("Registering sensor listener");
-
-        mRegistered = true;
-        chooseSensor();
-    }
-
-    private void chooseSensor() {
-        mExecution.assertIsMainThread();
-        if (!mRegistered || mPaused || mListeners.isEmpty()) {
-            return;
-        }
-        if (mSecondarySafe) {
-            mSecondaryThresholdSensor.resume();
-            mPrimaryThresholdSensor.pause();
-        } else {
-            mPrimaryThresholdSensor.resume();
-            mSecondaryThresholdSensor.pause();
-        }
-    }
-
-    /**
-     * Remove a listener.
-     *
-     * If all listeners are removed from an instance of this class,
-     * it will unregister itself with the SensorManager.
-     */
-    @Override
-    public void unregister(ThresholdSensor.Listener listener) {
-        mExecution.assertIsMainThread();
-        mListeners.remove(listener);
-        if (mListeners.size() == 0) {
-            unregisterInternal();
-        }
-    }
-
-    protected void unregisterInternal() {
-        mExecution.assertIsMainThread();
-        if (!mRegistered) {
-            return;
-        }
-        logDebug("unregistering sensor listener");
-        mPrimaryThresholdSensor.pause();
-        mSecondaryThresholdSensor.pause();
-        if (mCancelSecondaryRunnable != null) {
-            mCancelSecondaryRunnable.run();
-            mCancelSecondaryRunnable = null;
-        }
-        mLastPrimaryEvent = null;  // Forget what we know.
-        mLastEvent = null;
-        mRegistered = false;
-    }
-
-    public Boolean isNear() {
-        return isLoaded() && mLastEvent != null ? mLastEvent.getBelow() : null;
-    }
-
-    /** Update all listeners with the last value this class received from the sensor. */
-    public void alertListeners() {
-        mExecution.assertIsMainThread();
-        if (mAlerting.getAndSet(true)) {
-            return;
-        }
-        if (mLastEvent != null) {
-            ThresholdSensorEvent lastEvent = mLastEvent;  // Listeners can null out mLastEvent.
-            List<ThresholdSensor.Listener> listeners = new ArrayList<>(mListeners);
-            listeners.forEach(proximitySensorListener ->
-                    proximitySensorListener.onThresholdCrossed(lastEvent));
-        }
-
-        mAlerting.set(false);
-    }
-
-    private void onPrimarySensorEvent(ThresholdSensorEvent event) {
-        mExecution.assertIsMainThread();
-        if (mLastPrimaryEvent != null && event.getBelow() == mLastPrimaryEvent.getBelow()) {
-            return;
-        }
-
-        mLastPrimaryEvent = event;
-
-        if (mSecondarySafe && mSecondaryThresholdSensor.isLoaded()) {
-            logDebug("Primary sensor reported " + (event.getBelow() ? "near" : "far")
-                    + ". Checking secondary.");
-            if (mCancelSecondaryRunnable == null) {
-                mSecondaryThresholdSensor.resume();
-            }
-            return;
-        }
-
-
-        if (!mSecondaryThresholdSensor.isLoaded()) {  // No secondary
-            logDebug("Primary sensor event: " + event.getBelow() + ". No secondary.");
-            onSensorEvent(event);
-        } else if (event.getBelow()) {  // Covered? Check secondary.
-            logDebug("Primary sensor event: " + event.getBelow() + ". Checking secondary.");
-            if (mCancelSecondaryRunnable != null) {
-                mCancelSecondaryRunnable.run();
-            }
-            mSecondaryThresholdSensor.resume();
-        } else {  // Uncovered. Report immediately.
-            onSensorEvent(event);
-        }
-    }
-
-    private void onSensorEvent(ThresholdSensorEvent event) {
-        mExecution.assertIsMainThread();
-        if (mLastEvent != null && event.getBelow() == mLastEvent.getBelow()) {
-            return;
-        }
-
-        if (!mSecondarySafe && !event.getBelow()) {
-            chooseSensor();
-        }
-
-        mLastEvent = event;
-        alertListeners();
-    }
-
-    @Override
-    public String toString() {
-        return String.format("{registered=%s, paused=%s, near=%s, primarySensor=%s, "
-                + "secondarySensor=%s secondarySafe=%s}",
-                isRegistered(), mPaused, isNear(), mPrimaryThresholdSensor,
-                mSecondaryThresholdSensor, mSecondarySafe);
-    }
-
-    /**
-     * Convenience class allowing for briefly checking the proximity sensor.
-     */
-    public static class ProximityCheck implements Runnable {
-
-        private final ProximitySensor mSensor;
-        private final DelayableExecutor mDelayableExecutor;
-        private List<Consumer<Boolean>> mCallbacks = new ArrayList<>();
-        private final ThresholdSensor.Listener mListener;
-        private final AtomicBoolean mRegistered = new AtomicBoolean();
-
-        @Inject
-        public ProximityCheck(ProximitySensor sensor, @Main DelayableExecutor delayableExecutor) {
-            mSensor = sensor;
-            mSensor.setTag("prox_check");
-            mDelayableExecutor = delayableExecutor;
-            mListener = this::onProximityEvent;
-        }
-
-        /** Set a descriptive tag for the sensors registration. */
-        public void setTag(String tag) {
-            mSensor.setTag(tag);
-        }
-
-        @Override
-        public void run() {
-            unregister();
-            onProximityEvent(null);
-        }
-
-        /**
-         * Query the proximity sensor, timing out if no result.
-         */
-        public void check(long timeoutMs, Consumer<Boolean> callback) {
-            if (!mSensor.isLoaded()) {
-                callback.accept(null);
-                return;
-            }
-            mCallbacks.add(callback);
-            if (!mRegistered.getAndSet(true)) {
-                mSensor.register(mListener);
-                mDelayableExecutor.executeDelayed(this, timeoutMs);
-            }
-        }
-
-        private void unregister() {
-            mSensor.unregister(mListener);
-            mRegistered.set(false);
-        }
-
-        private void onProximityEvent(ThresholdSensorEvent proximityEvent) {
-            mCallbacks.forEach(
-                    booleanConsumer ->
-                            booleanConsumer.accept(
-                                    proximityEvent == null ? null : proximityEvent.getBelow()));
-            mCallbacks.clear();
-            unregister();
-            mRegistered.set(false);
-        }
-    }
-
-    private void logDebug(String msg) {
-        if (DEBUG) {
-            Log.d(TAG, (mTag != null ? "[" + mTag + "] " : "") + msg);
-        }
-    }
+    void setSecondarySafe(boolean safe);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensorImpl.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensorImpl.java
new file mode 100644
index 0000000..e639313a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensorImpl.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import android.hardware.SensorManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.concurrency.Execution;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.inject.Inject;
+
+/**
+ * Wrapper around SensorManager customized for the Proximity sensor.
+ *
+ * The ProximitySensor supports the concept of a primary and a
+ * secondary hardware sensor. The primary sensor is used for a first
+ * pass check if the phone covered. When triggered, it then checks
+ * the secondary sensor for confirmation (if there is one). It does
+ * not send a proximity event until the secondary sensor confirms (or
+ * rejects) the reading. The secondary sensor is, in fact, the source
+ * of truth.
+ *
+ * This is necessary as sometimes keeping the secondary sensor on for
+ * extends periods is undesirable. It may, however, result in increased
+ * latency for proximity readings.
+ *
+ * Phones should configure this via a config.xml overlay. If no
+ * proximity sensor is set (primary or secondary) we fall back to the
+ * default Sensor.TYPE_PROXIMITY. If proximity_sensor_type is set in
+ * config.xml, that will be used as the primary sensor. If
+ * proximity_sensor_secondary_type is set, that will function as the
+ * secondary sensor. If no secondary is set, only the primary will be
+ * used.
+ */
+class ProximitySensorImpl implements ProximitySensor {
+    private static final String TAG = "ProxSensor";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final long SECONDARY_PING_INTERVAL_MS = 5000;
+
+    ThresholdSensor mPrimaryThresholdSensor;
+    ThresholdSensor mSecondaryThresholdSensor;
+    private final DelayableExecutor mDelayableExecutor;
+    private final Execution mExecution;
+    private final List<ThresholdSensor.Listener> mListeners = new ArrayList<>();
+    private String mTag = null;
+    @VisibleForTesting protected boolean mPaused;
+    private ThresholdSensorEvent mLastPrimaryEvent;
+    @VisibleForTesting
+    ThresholdSensorEvent mLastEvent;
+    private boolean mRegistered;
+    private final AtomicBoolean mAlerting = new AtomicBoolean();
+    private Runnable mCancelSecondaryRunnable;
+    boolean mInitializedListeners = false;
+    private boolean mSecondarySafe = false; // safe to skip primary sensor check and use secondary
+    protected @DevicePostureController.DevicePostureInt int mDevicePosture;
+
+    final ThresholdSensor.Listener mPrimaryEventListener = this::onPrimarySensorEvent;
+
+    final ThresholdSensor.Listener mSecondaryEventListener =
+            new ThresholdSensor.Listener() {
+        @Override
+        public void onThresholdCrossed(ThresholdSensorEvent event) {
+            // If we no longer have a "below" signal and the secondary sensor is not
+            // considered "safe", then we need to turn it off.
+            if (!mSecondarySafe
+                    && (mLastPrimaryEvent == null
+                    || !mLastPrimaryEvent.getBelow()
+                    || !event.getBelow())) {
+                chooseSensor();
+                if (mLastPrimaryEvent == null || !mLastPrimaryEvent.getBelow()) {
+                    // Only check the secondary as long as the primary thinks we're near.
+                    if (mCancelSecondaryRunnable != null) {
+                        mCancelSecondaryRunnable.run();
+                        mCancelSecondaryRunnable = null;
+                    }
+                    return;
+                } else {
+                    // Check this sensor again in a moment.
+                    mCancelSecondaryRunnable = mDelayableExecutor.executeDelayed(() -> {
+                        // This is safe because we know that mSecondaryThresholdSensor
+                        // is loaded, otherwise we wouldn't be here.
+                        mPrimaryThresholdSensor.pause();
+                        mSecondaryThresholdSensor.resume();
+                    },
+                        SECONDARY_PING_INTERVAL_MS);
+                }
+            }
+            logDebug("Secondary sensor event: " + event.getBelow() + ".");
+
+            if (!mPaused) {
+                onSensorEvent(event);
+            }
+        }
+    };
+
+    @Inject
+    ProximitySensorImpl(
+            @PrimaryProxSensor ThresholdSensor primary,
+            @SecondaryProxSensor ThresholdSensor  secondary,
+            @Main DelayableExecutor delayableExecutor,
+            Execution execution) {
+        mPrimaryThresholdSensor = primary;
+        mSecondaryThresholdSensor = secondary;
+        mDelayableExecutor = delayableExecutor;
+        mExecution = execution;
+    }
+
+    @Override
+    public void setTag(String tag) {
+        mTag = tag;
+        mPrimaryThresholdSensor.setTag(tag + ":primary");
+        mSecondaryThresholdSensor.setTag(tag + ":secondary");
+    }
+
+    @Override
+    public void setDelay(int delay) {
+        mExecution.assertIsMainThread();
+        mPrimaryThresholdSensor.setDelay(delay);
+        mSecondaryThresholdSensor.setDelay(delay);
+    }
+
+    /**
+     * Unregister with the {@link SensorManager} without unsetting listeners on this object.
+     */
+    @Override
+    public void pause() {
+        mExecution.assertIsMainThread();
+        mPaused = true;
+        unregisterInternal();
+    }
+
+    /**
+     * Register with the {@link SensorManager}. No-op if no listeners are registered on this object.
+     */
+    @Override
+    public void resume() {
+        mExecution.assertIsMainThread();
+        mPaused = false;
+        registerInternal();
+    }
+
+    @Override
+    public void setSecondarySafe(boolean safe) {
+        mSecondarySafe = mSecondaryThresholdSensor.isLoaded() && safe;
+        chooseSensor();
+    }
+
+    /**
+     * Returns true if we are registered with the SensorManager.
+     */
+    @Override
+    public boolean isRegistered() {
+        return mRegistered;
+    }
+
+    /**
+     * Returns {@code false} if a Proximity sensor is not available.
+     */
+    @Override
+    public boolean isLoaded() {
+        return mPrimaryThresholdSensor.isLoaded();
+    }
+
+    /**
+     * Add a listener.
+     *
+     * Registers itself with the {@link SensorManager} if this is the first listener
+     * added. If the ProximitySensor is paused, it will be registered when resumed.
+     */
+    @Override
+    public void register(ThresholdSensor.Listener listener) {
+        mExecution.assertIsMainThread();
+        if (!isLoaded()) {
+            return;
+        }
+
+        if (mListeners.contains(listener)) {
+            logDebug("ProxListener registered multiple times: " + listener);
+        } else {
+            mListeners.add(listener);
+        }
+        registerInternal();
+    }
+
+    protected void registerInternal() {
+        mExecution.assertIsMainThread();
+        if (mRegistered || mPaused || mListeners.isEmpty()) {
+            return;
+        }
+        if (!mInitializedListeners) {
+            mPrimaryThresholdSensor.pause();
+            mSecondaryThresholdSensor.pause();
+            mPrimaryThresholdSensor.register(mPrimaryEventListener);
+            mSecondaryThresholdSensor.register(mSecondaryEventListener);
+            mInitializedListeners = true;
+        }
+
+        mRegistered = true;
+        chooseSensor();
+    }
+
+    private void chooseSensor() {
+        mExecution.assertIsMainThread();
+        if (!mRegistered || mPaused || mListeners.isEmpty()) {
+            return;
+        }
+        if (mSecondarySafe) {
+            mSecondaryThresholdSensor.resume();
+            mPrimaryThresholdSensor.pause();
+        } else {
+            mPrimaryThresholdSensor.resume();
+            mSecondaryThresholdSensor.pause();
+        }
+    }
+
+    /**
+     * Remove a listener.
+     *
+     * If all listeners are removed from an instance of this class,
+     * it will unregister itself with the SensorManager.
+     */
+    @Override
+    public void unregister(ThresholdSensor.Listener listener) {
+        mExecution.assertIsMainThread();
+        mListeners.remove(listener);
+        if (mListeners.size() == 0) {
+            unregisterInternal();
+        }
+    }
+
+    @Override
+    public String getName() {
+        return mPrimaryThresholdSensor.getName();
+    }
+
+    @Override
+    public String getType() {
+        return mPrimaryThresholdSensor.getType();
+    }
+
+    protected void unregisterInternal() {
+        mExecution.assertIsMainThread();
+        if (!mRegistered) {
+            return;
+        }
+        logDebug("unregistering sensor listener");
+        mPrimaryThresholdSensor.pause();
+        mSecondaryThresholdSensor.pause();
+        if (mCancelSecondaryRunnable != null) {
+            mCancelSecondaryRunnable.run();
+            mCancelSecondaryRunnable = null;
+        }
+        mLastPrimaryEvent = null;  // Forget what we know.
+        mLastEvent = null;
+        mRegistered = false;
+    }
+
+    @Override
+    public Boolean isNear() {
+        return isLoaded() && mLastEvent != null ? mLastEvent.getBelow() : null;
+    }
+
+    @Override
+    public void alertListeners() {
+        mExecution.assertIsMainThread();
+        if (mAlerting.getAndSet(true)) {
+            return;
+        }
+        if (mLastEvent != null) {
+            ThresholdSensorEvent lastEvent = mLastEvent;  // Listeners can null out mLastEvent.
+            List<ThresholdSensor.Listener> listeners = new ArrayList<>(mListeners);
+            listeners.forEach(proximitySensorListener ->
+                    proximitySensorListener.onThresholdCrossed(lastEvent));
+        }
+
+        mAlerting.set(false);
+    }
+
+    private void onPrimarySensorEvent(ThresholdSensorEvent event) {
+        mExecution.assertIsMainThread();
+        if (mLastPrimaryEvent != null && event.getBelow() == mLastPrimaryEvent.getBelow()) {
+            return;
+        }
+
+        mLastPrimaryEvent = event;
+
+        if (mSecondarySafe && mSecondaryThresholdSensor.isLoaded()) {
+            logDebug("Primary sensor reported " + (event.getBelow() ? "near" : "far")
+                    + ". Checking secondary.");
+            if (mCancelSecondaryRunnable == null) {
+                mSecondaryThresholdSensor.resume();
+            }
+            return;
+        }
+
+
+        if (!mSecondaryThresholdSensor.isLoaded()) {  // No secondary
+            logDebug("Primary sensor event: " + event.getBelow() + ". No secondary.");
+            onSensorEvent(event);
+        } else if (event.getBelow()) {  // Covered? Check secondary.
+            logDebug("Primary sensor event: " + event.getBelow() + ". Checking secondary.");
+            if (mCancelSecondaryRunnable != null) {
+                mCancelSecondaryRunnable.run();
+            }
+            mSecondaryThresholdSensor.resume();
+        } else {  // Uncovered. Report immediately.
+            onSensorEvent(event);
+        }
+    }
+
+    private void onSensorEvent(ThresholdSensorEvent event) {
+        mExecution.assertIsMainThread();
+        if (mLastEvent != null && event.getBelow() == mLastEvent.getBelow()) {
+            return;
+        }
+
+        if (!mSecondarySafe && !event.getBelow()) {
+            chooseSensor();
+        }
+
+        mLastEvent = event;
+        alertListeners();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{registered=%s, paused=%s, near=%s, posture=%s, primarySensor=%s, "
+                + "secondarySensor=%s secondarySafe=%s}",
+                isRegistered(), mPaused, isNear(), mDevicePosture, mPrimaryThresholdSensor,
+                mSecondaryThresholdSensor, mSecondarySafe);
+    }
+
+    void logDebug(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, (mTag != null ? "[" + mTag + "] " : "") + msg);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java
index 11e7df8..96980b8 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java
@@ -16,11 +16,24 @@
 
 package com.android.systemui.util.sensors;
 
+import android.content.res.Resources;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
 
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
 
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
@@ -31,8 +44,10 @@
 public class SensorModule {
     @Provides
     @PrimaryProxSensor
-    static ThresholdSensor providePrimaryProxSensor(SensorManager sensorManager,
-            ThresholdSensorImpl.Builder thresholdSensorBuilder) {
+    static ThresholdSensor providePrimaryProximitySensor(
+            SensorManager sensorManager,
+            ThresholdSensorImpl.Builder thresholdSensorBuilder
+    ) {
         try {
             return thresholdSensorBuilder
                     .setSensorDelay(SensorManager.SENSOR_DELAY_NORMAL)
@@ -52,8 +67,9 @@
 
     @Provides
     @SecondaryProxSensor
-    static ThresholdSensor provideSecondaryProxSensor(
-            ThresholdSensorImpl.Builder thresholdSensorBuilder) {
+    static ThresholdSensor provideSecondaryProximitySensor(
+            ThresholdSensorImpl.Builder thresholdSensorBuilder
+    ) {
         try {
             return thresholdSensorBuilder
                     .setSensorResourceId(R.string.proximity_sensor_secondary_type, true)
@@ -64,4 +80,151 @@
             return thresholdSensorBuilder.setSensor(null).setThresholdValue(0).build();
         }
     }
+
+    /**
+     * If postures are supported on the device, returns a posture dependent proximity sensor
+     * which switches proximity sensors based on the current posture.
+     *
+     * If postures are not supported the regular {@link ProximitySensorImpl} will be returned.
+     */
+    @Provides
+    static ProximitySensor provideProximitySensor(
+            @Main Resources resources,
+            Lazy<PostureDependentProximitySensor> postureDependentProximitySensorProvider,
+            Lazy<ProximitySensorImpl> proximitySensorProvider
+    ) {
+        if (hasPostureSupport(
+                resources.getStringArray(R.array.proximity_sensor_posture_mapping))) {
+            return postureDependentProximitySensorProvider.get();
+        } else  {
+            return proximitySensorProvider.get();
+        }
+    }
+
+    @Provides
+    static ProximityCheck provideProximityCheck(
+            ProximitySensor proximitySensor,
+            @Main DelayableExecutor delayableExecutor
+    ) {
+        return new ProximityCheck(
+                proximitySensor,
+                delayableExecutor
+        );
+    }
+
+    @Provides
+    @PrimaryProxSensor
+    @NonNull
+    static ThresholdSensor[] providePostureToProximitySensorMapping(
+            ThresholdSensorImpl.BuilderFactory thresholdSensorImplBuilderFactory,
+            @Main Resources resources
+    ) {
+        return createPostureToSensorMapping(
+                thresholdSensorImplBuilderFactory,
+                resources.getStringArray(R.array.proximity_sensor_posture_mapping),
+                R.dimen.proximity_sensor_threshold,
+                R.dimen.proximity_sensor_threshold_latch
+        );
+    }
+
+    @Provides
+    @SecondaryProxSensor
+    @NonNull
+    static ThresholdSensor[] providePostureToSecondaryProximitySensorMapping(
+            ThresholdSensorImpl.BuilderFactory thresholdSensorImplBuilderFactory,
+            @Main Resources resources
+    ) {
+        return createPostureToSensorMapping(
+                thresholdSensorImplBuilderFactory,
+                resources.getStringArray(R.array.proximity_sensor_secondary_posture_mapping),
+                R.dimen.proximity_sensor_secondary_threshold,
+                R.dimen.proximity_sensor_secondary_threshold_latch
+        );
+    }
+
+    /**
+     * Builds sensors to use per posture.
+     *
+     * @param sensorTypes an array where the index represents
+     *                    {@link DevicePostureController.DevicePostureInt} and the value
+     *                    at the given index is the sensorType. Empty values represent
+     *                    no sensor desired.
+     * @param proximitySensorThresholdResourceId resource id for the threshold for all sensor
+     *                                           postures. This currently only supports one value.
+     *                                           This needs to be updated in the future if postures
+     *                                           use different sensors with differing thresholds.
+     * @param proximitySensorThresholdLatchResourceId resource id for the latch for all sensor
+     *                                                postures. This currently only supports one
+     *                                                value. This needs to be updated in the future
+     *                                                if postures use different sensors with
+     *                                                differing latches.
+     * @return an array where the index represents the device posture
+     * {@link DevicePostureController.DevicePostureInt} and the value at the index is the sensor to
+     * use when the device is in that posture.
+     */
+    @NonNull
+    private static ThresholdSensor[] createPostureToSensorMapping(
+            ThresholdSensorImpl.BuilderFactory thresholdSensorImplBuilderFactory,
+            String[] sensorTypes,
+            int proximitySensorThresholdResourceId,
+            int proximitySensorThresholdLatchResourceId
+
+    ) {
+        ThresholdSensor noProxSensor = thresholdSensorImplBuilderFactory
+                .createBuilder()
+                .setSensor(null).setThresholdValue(0).build();
+
+
+        // length and index of sensorMap correspond to DevicePostureController.DevicePostureInt:
+        final ThresholdSensor[] sensorMap =
+                new ThresholdSensor[DevicePostureController.SUPPORTED_POSTURES_SIZE];
+        Arrays.fill(sensorMap, noProxSensor);
+
+        if (!hasPostureSupport(sensorTypes)) {
+            Log.e("SensorModule", "config doesn't support postures,"
+                    + " but attempting to retrieve proxSensorMapping");
+            return sensorMap;
+        }
+
+        // Map of sensorType => Sensor, so we reuse the same sensor if it's the same between
+        // postures
+        Map<String, ThresholdSensor> typeToSensorMap = new HashMap<>();
+        for (int i = 0; i < sensorTypes.length; i++) {
+            try {
+                final String sensorType = sensorTypes[i];
+                if (typeToSensorMap.containsKey(sensorType)) {
+                    sensorMap[i] = typeToSensorMap.get(sensorType);
+                } else {
+                    sensorMap[i] = thresholdSensorImplBuilderFactory
+                            .createBuilder()
+                            .setSensorType(sensorTypes[i], true)
+                            .setThresholdResourceId(proximitySensorThresholdResourceId)
+                            .setThresholdLatchResourceId(proximitySensorThresholdLatchResourceId)
+                            .build();
+                    typeToSensorMap.put(sensorType, sensorMap[i]);
+                }
+            } catch (IllegalStateException e) {
+                // do nothing, sensor at this posture is already set to noProxSensor
+            }
+        }
+
+        return sensorMap;
+    }
+
+    /**
+     * Returns true if there's at least one non-empty sensor type in the given array.
+     */
+    private static boolean hasPostureSupport(String[] postureToSensorTypeMapping) {
+        if (postureToSensorTypeMapping == null || postureToSensorTypeMapping.length == 0) {
+            return false;
+        }
+
+        for (String sensorType : postureToSensorTypeMapping) {
+            if (!TextUtils.isEmpty(sensorType)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java
index 363a734..d81a8d5 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.util.sensors;
 
-import java.util.Locale;
-
 /**
  * A wrapper class for sensors that have a boolean state - above/below.
  */
@@ -77,6 +75,16 @@
     void unregister(Listener listener);
 
     /**
+     * Name of the sensor.
+     */
+    String getName();
+
+    /**
+     * Type of the sensor.
+     */
+    String getType();
+
+    /**
      * Interface for listening to events on {@link ThresholdSensor}
      */
     interface Listener {
@@ -85,34 +93,4 @@
          */
         void onThresholdCrossed(ThresholdSensorEvent event);
     }
-
-    /**
-     * Returned when the below/above state of a {@link ThresholdSensor} changes.
-     */
-    class ThresholdSensorEvent {
-        private final boolean mBelow;
-        private final long mTimestampNs;
-
-        public ThresholdSensorEvent(boolean below, long timestampNs) {
-            mBelow = below;
-            mTimestampNs = timestampNs;
-        }
-
-        public boolean getBelow() {
-            return mBelow;
-        }
-
-        public long getTimestampNs() {
-            return mTimestampNs;
-        }
-
-        public long getTimestampMs() {
-            return mTimestampNs / 1000000;
-        }
-
-        @Override
-        public String toString() {
-            return String.format((Locale) null, "{near=%s, timestamp_ns=%d}", mBelow, mTimestampNs);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorEvent.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorEvent.java
new file mode 100644
index 0000000..afce09f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorEvent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import java.util.Locale;
+
+/**
+ * Returned when the below/above state of a {@link ThresholdSensor} changes.
+ */
+public class ThresholdSensorEvent {
+    private final boolean mBelow;
+    private final long mTimestampNs;
+
+    public ThresholdSensorEvent(boolean below, long timestampNs) {
+        mBelow = below;
+        mTimestampNs = timestampNs;
+    }
+
+    public boolean getBelow() {
+        return mBelow;
+    }
+
+    public long getTimestampNs() {
+        return mTimestampNs;
+    }
+
+    public long getTimestampMs() {
+        return mTimestampNs / 1000000;
+    }
+
+    @Override
+    public String toString() {
+        return String.format((Locale) null, "{near=%s, timestamp_ns=%d}", mBelow, mTimestampNs);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java
index d10cf9b..a9086b1 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java
@@ -21,6 +21,7 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -32,7 +33,10 @@
 
 import javax.inject.Inject;
 
-class ThresholdSensorImpl implements ThresholdSensor {
+/**
+ * Sensor that will only trigger beyond some lower and upper threshold.
+ */
+public class ThresholdSensorImpl implements ThresholdSensor {
     private static final String TAG = "ThresholdSensor";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -198,6 +202,15 @@
         alertListenersInternal(belowThreshold, timestampNs);
     }
 
+    @Override
+    public String getName() {
+        return mSensor != null ? mSensor.getName() : null;
+    }
+
+    @Override
+    public String getType() {
+        return mSensor != null ? mSensor.getStringType() : null;
+    }
 
     @Override
     public String toString() {
@@ -211,7 +224,12 @@
         }
     }
 
-    static class Builder {
+    /**
+     * Use to build a ThresholdSensor. Should only be used once per sensor built, since
+     * parameters are not reset after calls to build(). For ease of retrievingnew Builders, use
+     * {@link BuilderFactory}.
+     */
+    public static class Builder {
         private final Resources mResources;
         private final AsyncSensorManager mSensorManager;
         private final Execution mExecution;
@@ -318,7 +336,7 @@
 
         @VisibleForTesting
         Sensor findSensorByType(String sensorType, boolean requireWakeUp) {
-            if (sensorType.isEmpty()) {
+            if (TextUtils.isEmpty(sensorType)) {
                 return null;
             }
 
@@ -336,4 +354,29 @@
             return sensor;
         }
     }
+
+    /**
+     * Factory that creates a new ThresholdSensorImpl.Builder. In general, Builders should not be
+     * reused after creating a ThresholdSensor or else there may be default threshold and sensor
+     * values set from the previous built sensor.
+     */
+    public static class BuilderFactory {
+        private final Resources mResources;
+        private final AsyncSensorManager mSensorManager;
+        private final Execution mExecution;
+
+        @Inject
+        BuilderFactory(
+                @Main Resources resources,
+                AsyncSensorManager sensorManager,
+                Execution execution) {
+            mResources = resources;
+            mSensorManager = sensorManager;
+            mExecution = execution;
+        }
+
+        ThresholdSensorImpl.Builder createBuilder() {
+            return new Builder(mResources, mSensorManager, mExecution);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
new file mode 100644
index 0000000..2a0cc7d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.wrapper
+
+import android.content.Context
+import com.android.internal.view.RotationPolicy
+import com.android.internal.view.RotationPolicy.RotationPolicyListener
+import javax.inject.Inject
+
+/**
+ * Testable wrapper interface around RotationPolicy {link com.android.internal.view.RotationPolicy}
+ */
+interface RotationPolicyWrapper {
+    fun setRotationLock(enabled: Boolean)
+    fun setRotationLockAtAngle(enabled: Boolean, rotation: Int)
+    fun getRotationLockOrientation(): Int
+    fun isRotationLockToggleVisible(): Boolean
+    fun isRotationLocked(): Boolean
+    fun registerRotationPolicyListener(listener: RotationPolicyListener, userHandle: Int)
+    fun unregisterRotationPolicyListener(listener: RotationPolicyListener)
+}
+
+class RotationPolicyWrapperImpl @Inject constructor(private val context: Context) :
+    RotationPolicyWrapper {
+
+    override fun setRotationLock(enabled: Boolean) {
+        RotationPolicy.setRotationLock(context, enabled)
+    }
+
+    override fun setRotationLockAtAngle(enabled: Boolean, rotation: Int) {
+        RotationPolicy.setRotationLockAtAngle(context, enabled, rotation)
+    }
+
+    override fun getRotationLockOrientation(): Int =
+        RotationPolicy.getRotationLockOrientation(context)
+
+    override fun isRotationLockToggleVisible(): Boolean =
+        RotationPolicy.isRotationLockToggleVisible(context)
+
+    override fun isRotationLocked(): Boolean =
+        RotationPolicy.isRotationLocked(context)
+
+    override fun registerRotationPolicyListener(
+        listener: RotationPolicyListener,
+        userHandle: Int
+    ) {
+        RotationPolicy.registerRotationPolicyListener(context, listener, userHandle)
+    }
+
+    override fun unregisterRotationPolicyListener(listener: RotationPolicyListener) {
+        RotationPolicy.unregisterRotationPolicyListener(context, listener)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java b/packages/SystemUI/src/com/android/systemui/util/wrapper/UtilWrapperModule.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
copy to packages/SystemUI/src/com/android/systemui/util/wrapper/UtilWrapperModule.kt
index b391bd9..7e3aa27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EthernetIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/UtilWrapperModule.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.util.wrapper
 
-import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
 
-class EthernetIcons {
-    static final int[][] ETHERNET_ICONS = {
-            { R.drawable.stat_sys_ethernet },
-            { R.drawable.stat_sys_ethernet_fully },
-    };
+@Module
+abstract class UtilWrapperModule {
+
+    @Binds
+    @SysUISingleton
+    abstract fun bindRotationPolicyWrapper(impl: RotationPolicyWrapperImpl): RotationPolicyWrapper
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
index 56f1c09..c083c14 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
@@ -20,13 +20,11 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
-import android.media.AudioManager;
 import android.media.VolumePolicy;
 import android.os.Bundle;
 import android.view.WindowManager.LayoutParams;
 
 import com.android.settingslib.applications.InterestingConfigChanges;
-import com.android.systemui.Dependency;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
@@ -67,6 +65,7 @@
             ActivityInfo.CONFIG_FONT_SCALE | ActivityInfo.CONFIG_LOCALE
             | ActivityInfo.CONFIG_ASSETS_PATHS | ActivityInfo.CONFIG_UI_MODE);
     private final KeyguardViewMediator mKeyguardViewMediator;
+    private final ActivityStarter mActivityStarter;
     private VolumeDialog mDialog;
     private VolumePolicy mVolumePolicy = new VolumePolicy(
             DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT,  // volumeDownToEnterSilent
@@ -79,18 +78,23 @@
     public VolumeDialogComponent(
             Context context,
             KeyguardViewMediator keyguardViewMediator,
+            ActivityStarter activityStarter,
             VolumeDialogControllerImpl volumeDialogController,
-            DemoModeController demoModeController) {
+            DemoModeController demoModeController,
+            PluginDependencyProvider pluginDependencyProvider,
+            ExtensionController extensionController,
+            TunerService tunerService,
+            VolumeDialog volumeDialog) {
         mContext = context;
         mKeyguardViewMediator = keyguardViewMediator;
+        mActivityStarter = activityStarter;
         mController = volumeDialogController;
         mController.setUserActivityListener(this);
         // Allow plugins to reference the VolumeDialogController.
-        Dependency.get(PluginDependencyProvider.class)
-                .allowPluginDependency(VolumeDialogController.class);
-        Dependency.get(ExtensionController.class).newExtension(VolumeDialog.class)
+        pluginDependencyProvider.allowPluginDependency(VolumeDialogController.class);
+        extensionController.newExtension(VolumeDialog.class)
                 .withPlugin(VolumeDialog.class)
-                .withDefault(this::createDefault)
+                .withDefault(() -> volumeDialog)
                 .withCallback(dialog -> {
                     if (mDialog != null) {
                         mDialog.destroy();
@@ -99,19 +103,11 @@
                     mDialog.init(LayoutParams.TYPE_VOLUME_OVERLAY, mVolumeDialogCallback);
                 }).build();
         applyConfiguration();
-        Dependency.get(TunerService.class).addTunable(this, VOLUME_DOWN_SILENT, VOLUME_UP_SILENT,
+        tunerService.addTunable(this, VOLUME_DOWN_SILENT, VOLUME_UP_SILENT,
                 VOLUME_SILENT_DO_NOT_DISTURB);
         demoModeController.addCallback(this);
     }
 
-    protected VolumeDialog createDefault() {
-        VolumeDialogImpl impl = new VolumeDialogImpl(mContext);
-        impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
-        impl.setAutomute(true);
-        impl.setSilentMode(false);
-        return impl;
-    }
-
     @Override
     public void onTuningChanged(String key, String newValue) {
         boolean volumeDownToEnterSilent = mVolumePolicy.volumeDownToEnterSilent;
@@ -189,8 +185,7 @@
     }
 
     private void startSettings(Intent intent) {
-        Dependency.get(ActivityStarter.class).startActivity(intent,
-                true /* onlyProvisioned */, true /* dismissShade */);
+        mActivityStarter.startActivity(intent, true /* onlyProvisioned */, true /* dismissShade */);
     }
 
     private final VolumeDialogImpl.Callback mVolumeDialogCallback = new VolumeDialogImpl.Callback() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 60b92ef..58f74a0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -104,8 +104,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
+import com.android.internal.view.RotationPolicy;
 import com.android.settingslib.Utils;
-import com.android.systemui.Dependency;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -234,6 +234,10 @@
     private final Object mSafetyWarningLock = new Object();
     private final Accessibility mAccessibility = new Accessibility();
 
+    private final ConfigurationController mConfigurationController;
+    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final ActivityStarter mActivityStarter;
+
     private boolean mShowing;
     private boolean mShowA11yStream;
 
@@ -255,14 +259,24 @@
     private Consumer<Boolean> mCrossWindowBlurEnabledListener;
     private BackgroundBlurDrawable mDialogRowsViewBackground;
 
-    public VolumeDialogImpl(Context context) {
+    public VolumeDialogImpl(
+            Context context,
+            VolumeDialogController volumeDialogController,
+            AccessibilityManagerWrapper accessibilityManagerWrapper,
+            DeviceProvisionedController deviceProvisionedController,
+            ConfigurationController configurationController,
+            MediaOutputDialogFactory mediaOutputDialogFactory,
+            ActivityStarter activityStarter) {
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
-        mController = Dependency.get(VolumeDialogController.class);
+        mController = volumeDialogController;
         mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
-        mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class);
-        mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
+        mAccessibilityMgr = accessibilityManagerWrapper;
+        mDeviceProvisionedController = deviceProvisionedController;
+        mConfigurationController = configurationController;
+        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mActivityStarter = activityStarter;
         mShowActiveStreamOnly = showActiveStreamOnly();
         mHasSeenODICaptionsTooltip =
                 Prefs.getBoolean(context, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
@@ -305,14 +319,14 @@
         mController.addCallback(mControllerCallbackH, mHandler);
         mController.getState();
 
-        Dependency.get(ConfigurationController.class).addCallback(this);
+        mConfigurationController.addCallback(this);
     }
 
     @Override
     public void destroy() {
         mController.removeCallback(mControllerCallbackH);
         mHandler.removeCallbacksAndMessages(null);
-        Dependency.get(ConfigurationController.class).removeCallback(this);
+        mConfigurationController.removeCallback(this);
     }
 
     @Override
@@ -400,7 +414,9 @@
         mDialog.setCanceledOnTouchOutside(true);
         mDialog.setOnShowListener(dialog -> {
             mDialogView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
-            if (!isLandscape()) mDialogView.setTranslationX(mDialogView.getWidth() / 2.0f);
+            if (!shouldSlideInVolumeTray()) {
+                mDialogView.setTranslationX(mDialogView.getWidth() / 2.0f);
+            }
             mDialogView.setAlpha(0);
             mDialogView.animate()
                     .alpha(1)
@@ -587,6 +603,10 @@
         return (int) (alpha * 255);
     }
 
+    private boolean shouldSlideInVolumeTray() {
+        return mContext.getDisplay().getRotation() != RotationPolicy.NATURAL_ROTATION;
+    }
+
     private boolean isLandscape() {
         return mContext.getResources().getConfiguration().orientation ==
                 Configuration.ORIENTATION_LANDSCAPE;
@@ -1010,9 +1030,8 @@
                 Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
                 Intent intent = new Intent(Settings.Panel.ACTION_VOLUME);
                 dismissH(DISMISS_REASON_SETTINGS_CLICKED);
-                Dependency.get(MediaOutputDialogFactory.class).dismiss();
-                Dependency.get(ActivityStarter.class).startActivity(intent,
-                        true /* dismissShade */);
+                mMediaOutputDialogFactory.dismiss();
+                mActivityStarter.startActivity(intent, true /* dismissShade */);
             });
         }
     }
@@ -1320,7 +1339,7 @@
 
                     hideRingerDrawer();
                 }, 50));
-        if (!isLandscape()) animator.translationX(mDialogView.getWidth() / 2.0f);
+        if (!shouldSlideInVolumeTray()) animator.translationX(mDialogView.getWidth() / 2.0f);
         animator.start();
         checkODICaptionsTooltip(true);
         mController.notifyVisible(false);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 1ef4c16..79aa643 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -16,11 +16,23 @@
 
 package com.android.systemui.volume.dagger;
 
+import android.content.Context;
+import android.media.AudioManager;
+
+import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.VolumeDialog;
+import com.android.systemui.plugins.VolumeDialogController;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.volume.VolumeDialogComponent;
+import com.android.systemui.volume.VolumeDialogImpl;
 
 import dagger.Binds;
 import dagger.Module;
+import dagger.Provides;
 
 
 /** Dagger Module for code in the volume package. */
@@ -29,4 +41,28 @@
     /** */
     @Binds
     VolumeComponent provideVolumeComponent(VolumeDialogComponent volumeDialogComponent);
+
+    /** */
+    @Provides
+    static VolumeDialog provideVolumeDialog(
+            Context context,
+            VolumeDialogController volumeDialogController,
+            AccessibilityManagerWrapper accessibilityManagerWrapper,
+            DeviceProvisionedController deviceProvisionedController,
+            ConfigurationController configurationController,
+            MediaOutputDialogFactory mediaOutputDialogFactory,
+            ActivityStarter activityStarter) {
+        VolumeDialogImpl impl = new VolumeDialogImpl(
+                context,
+                volumeDialogController,
+                accessibilityManagerWrapper,
+                deviceProvisionedController,
+                configurationController,
+                mediaOutputDialogFactory,
+                activityStarter);
+        impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
+        impl.setAutomute(true);
+        impl.setSilentMode(false);
+        return impl;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardCarousel.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardCarousel.java
index 1e1b459..77fd2e8 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardCarousel.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardCarousel.java
@@ -190,6 +190,15 @@
     }
 
     /**
+     * Sets the adapter again in the RecyclerView, updating the ViewHolders children's layout.
+     * This is needed when changing the state of the device (eg fold/unfold) so the ViewHolders are
+     * recreated.
+     */
+    void resetAdapter() {
+        setAdapter(mWalletCardCarouselAdapter);
+    }
+
+    /**
      * Returns true if the data set is changed.
      */
     boolean setData(List<WalletCardViewInfo> data, int selectedIndex, boolean hasLockStateChanged) {
@@ -376,8 +385,8 @@
             CardView cardView = viewHolder.mCardView;
             cardView.setRadius(mCornerRadiusPx);
             ViewGroup.LayoutParams layoutParams = cardView.getLayoutParams();
-            layoutParams.width = mCardWidthPx;
-            layoutParams.height = mCardHeightPx;
+            layoutParams.width = getCardWidthPx();
+            layoutParams.height = getCardHeightPx();
             view.setTag(viewHolder);
             return viewHolder;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
index 420f84a..9b2702f 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
@@ -99,17 +99,13 @@
         mCardCarousel.setExpectedViewWidth(getWidth());
     }
 
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        updateViewForOrientation(newConfig.orientation);
-    }
-
     private void updateViewForOrientation(@Configuration.Orientation int orientation) {
         if (orientation == Configuration.ORIENTATION_PORTRAIT) {
             renderViewPortrait();
         } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
             renderViewLandscape();
         }
+        mCardCarousel.resetAdapter(); // necessary to update cards width
         ViewGroup.LayoutParams params = mCardCarouselContainer.getLayoutParams();
         if (params instanceof MarginLayoutParams) {
             ((MarginLayoutParams) params).topMargin =
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index a29a638..291c64d 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -50,7 +50,6 @@
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
-import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -61,11 +60,10 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationChannelHelper;
@@ -80,7 +78,6 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -118,7 +115,6 @@
     private final NotifPipeline mNotifPipeline;
     private final Executor mSysuiMainExecutor;
 
-    private ScrimView mBubbleScrim;
     private final Bubbles.SysuiProxy mSysuiProxy;
     // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline
     private final List<NotifCallback> mCallbacks = new ArrayList<>();
@@ -193,12 +189,6 @@
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE))
                 : statusBarService;
 
-        mBubbleScrim = new ScrimView(mContext);
-        mBubbleScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-        mBubbles.setBubbleScrim(mBubbleScrim, (executor, looper) -> {
-            mBubbleScrim.setExecutor(executor, looper);
-        });
-
         if (featureFlags.isNewNotifPipelineRenderingEnabled()) {
             setupNotifPipeline();
         } else {
@@ -387,10 +377,24 @@
                 sysuiMainExecutor.execute(() -> {
                     sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
                             .commitUpdate(mContext.getDisplayId());
+                    if (!shouldExpand) {
+                        sysUiState.setFlag(
+                                QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
+                                false).commitUpdate(mContext.getDisplayId());
+                    }
                 });
             }
 
             @Override
+            public void onManageMenuExpandChanged(boolean menuExpanded) {
+                sysuiMainExecutor.execute(() -> {
+                    sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
+                            menuExpanded).commitUpdate(mContext.getDisplayId());
+                });
+            }
+
+
+            @Override
             public void onUnbubbleConversation(String key) {
                 sysuiMainExecutor.execute(() -> {
                     final NotificationEntry entry =
@@ -603,15 +607,6 @@
     }
 
     /**
-     * Returns the scrim drawn behind the bubble stack. This is managed by {@link ScrimController}
-     * since we want the scrim's appearance and behavior to be identical to that of the notification
-     * shade scrim.
-     */
-    public ScrimView getScrimForBubble() {
-        return mBubbleScrim;
-    }
-
-    /**
      * We intercept notification entries (including group summaries) dismissed by the user when
      * there is an active bubble associated with it. We do this so that developers can still
      * cancel it (and hence the bubbles associated with it).
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java
index 2216a91..3be1d3c 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java
@@ -38,6 +38,7 @@
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.pip.tv.TvPipController;
 import com.android.wm.shell.pip.tv.TvPipMenuController;
@@ -144,10 +145,17 @@
 
     @WMSingleton
     @Provides
+    static PipTransitionState providePipTransitionState() {
+        return new PipTransitionState();
+    }
+
+    @WMSingleton
+    @Provides
     static PipTaskOrganizer providePipTaskOrganizer(Context context,
             TvPipMenuController tvPipMenuController,
             SyncTransactionQueue syncTransactionQueue,
             PipBoundsState pipBoundsState,
+            PipTransitionState pipTransitionState,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             PipAnimationController pipAnimationController,
             PipTransitionController pipTransitionController,
@@ -157,7 +165,7 @@
             PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new PipTaskOrganizer(context,
-                syncTransactionQueue, pipBoundsState, pipBoundsAlgorithm,
+                syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm,
                 tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper,
                 pipTransitionController, splitScreenOptional, displayController, pipUiEventLogger,
                 shellTaskOrganizer, mainExecutor);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java
index 39a8bd9..dbdc460 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java
@@ -25,6 +25,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
@@ -58,10 +59,10 @@
     @WMSingleton
     @Provides
     static DisplayImeController provideDisplayImeController(IWindowManager wmService,
-            DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor,
-            TransactionPool transactionPool) {
-        return new DisplayImeController(wmService, displayController, mainExecutor,
-                transactionPool);
+            DisplayController displayController, DisplayInsetsController displayInsetsController,
+            @ShellMainThread ShellExecutor mainExecutor, TransactionPool transactionPool) {
+        return new DisplayImeController(wmService, displayController, displayInsetsController,
+                mainExecutor, transactionPool);
     }
 
     //
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index bc956dc..74611cc 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -20,6 +20,7 @@
 
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
@@ -64,6 +65,7 @@
 import com.android.wm.shell.onehanded.OneHandedUiEventLogger;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.protolog.ShellProtoLogImpl;
+import com.android.wm.shell.splitscreen.SplitScreen;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -100,11 +102,13 @@
                     | SYSUI_STATE_BOUNCER_SHOWING
                     | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
                     | SYSUI_STATE_BUBBLES_EXPANDED
+                    | SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED
                     | SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 
     // Shell interfaces
     private final Optional<Pip> mPipOptional;
-    private final Optional<LegacySplitScreen> mSplitScreenOptional;
+    private final Optional<LegacySplitScreen> mLegacySplitScreenOptional;
+    private final Optional<SplitScreen> mSplitScreenOptional;
     private final Optional<OneHanded> mOneHandedOptional;
     private final Optional<HideDisplayCutout> mHideDisplayCutoutOptional;
     private final Optional<ShellCommandHandler> mShellCommandHandler;
@@ -120,6 +124,7 @@
     private final Executor mSysUiMainExecutor;
 
     private boolean mIsSysUiStateValid;
+    private KeyguardUpdateMonitorCallback mLegacySplitScreenKeyguardCallback;
     private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback;
     private KeyguardUpdateMonitorCallback mPipKeyguardCallback;
     private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback;
@@ -128,7 +133,8 @@
     @Inject
     public WMShell(Context context,
             Optional<Pip> pipOptional,
-            Optional<LegacySplitScreen> splitScreenOptional,
+            Optional<LegacySplitScreen> legacySplitScreenOptional,
+            Optional<SplitScreen> splitScreenOptional,
             Optional<OneHanded> oneHandedOptional,
             Optional<HideDisplayCutout> hideDisplayCutoutOptional,
             Optional<ShellCommandHandler> shellCommandHandler,
@@ -149,6 +155,7 @@
         mScreenLifecycle = screenLifecycle;
         mSysUiState = sysUiState;
         mPipOptional = pipOptional;
+        mLegacySplitScreenOptional = legacySplitScreenOptional;
         mSplitScreenOptional = splitScreenOptional;
         mOneHandedOptional = oneHandedOptional;
         mHideDisplayCutoutOptional = hideDisplayCutoutOptional;
@@ -165,6 +172,7 @@
         mProtoTracer.add(this);
         mCommandQueue.addCallback(this);
         mPipOptional.ifPresent(this::initPip);
+        mLegacySplitScreenOptional.ifPresent(this::initLegacySplitScreen);
         mSplitScreenOptional.ifPresent(this::initSplitScreen);
         mOneHandedOptional.ifPresent(this::initOneHanded);
         mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout);
@@ -206,7 +214,7 @@
             }
 
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 pip.onOverlayChanged();
             }
         });
@@ -218,8 +226,8 @@
     }
 
     @VisibleForTesting
-    void initSplitScreen(LegacySplitScreen legacySplitScreen) {
-        mSplitScreenKeyguardCallback = new KeyguardUpdateMonitorCallback() {
+    void initLegacySplitScreen(LegacySplitScreen legacySplitScreen) {
+        mLegacySplitScreenKeyguardCallback = new KeyguardUpdateMonitorCallback() {
             @Override
             public void onKeyguardVisibilityChanged(boolean showing) {
                 // Hide the divider when keyguard is showing. Even though keyguard/statusbar is
@@ -229,6 +237,22 @@
                 legacySplitScreen.onKeyguardVisibilityChanged(showing);
             }
         };
+        mKeyguardUpdateMonitor.registerCallback(mLegacySplitScreenKeyguardCallback);
+    }
+
+    @VisibleForTesting
+    void initSplitScreen(SplitScreen splitScreen) {
+        mSplitScreenKeyguardCallback = new KeyguardUpdateMonitorCallback() {
+            @Override
+            public void onKeyguardVisibilityChanged(boolean showing) {
+                splitScreen.onKeyguardVisibilityChanged(showing);
+            }
+
+            @Override
+            public void onKeyguardOccludedChanged(boolean occluded) {
+                splitScreen.onKeyguardOccludedChanged(occluded);
+            }
+        };
         mKeyguardUpdateMonitor.registerCallback(mSplitScreenKeyguardCallback);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
index 6ef7450..5af2cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
@@ -28,7 +28,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.dagger.WMComponent;
 import com.android.systemui.dagger.WMSingleton;
-import com.android.wm.shell.FullscreenTaskListener;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellCommandHandler;
 import com.android.wm.shell.ShellCommandHandlerImpl;
@@ -44,6 +44,7 @@
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
@@ -54,7 +55,12 @@
 import com.android.wm.shell.common.annotations.ShellAnimationThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
+import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
 import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.freeform.FreeformTaskListener;
+import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
@@ -70,6 +76,7 @@
 import com.android.wm.shell.sizecompatui.SizeCompatUIController;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.splitscreen.StageTaskUnfoldController;
 import com.android.wm.shell.startingsurface.StartingSurface;
 import com.android.wm.shell.startingsurface.StartingWindowController;
 import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
@@ -77,10 +84,15 @@
 import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelperController;
 import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
 
 import java.util.Optional;
 
+import javax.inject.Provider;
+
 import dagger.BindsOptionalOf;
+import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
@@ -109,6 +121,14 @@
 
     @WMSingleton
     @Provides
+    static DisplayInsetsController provideDisplayInsetsController( IWindowManager wmService,
+            DisplayController displayController,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return new DisplayInsetsController(wmService, displayController, mainExecutor);
+    }
+
+    @WMSingleton
+    @Provides
     static DisplayLayout provideDisplayLayout() {
         return new DisplayLayout();
     }
@@ -116,8 +136,8 @@
     @WMSingleton
     @Provides
     static DragAndDropController provideDragAndDropController(Context context,
-            DisplayController displayController) {
-        return new DragAndDropController(context, displayController);
+            DisplayController displayController, UiEventLogger uiEventLogger) {
+        return new DragAndDropController(context, displayController, uiEventLogger);
     }
 
     @WMSingleton
@@ -194,11 +214,12 @@
             ShellTaskOrganizer organizer,
             DisplayController displayController,
             @ShellMainThread ShellExecutor mainExecutor,
-            @ShellMainThread Handler mainHandler) {
+            @ShellMainThread Handler mainHandler,
+            SyncTransactionQueue syncQueue) {
         return Optional.of(BubbleController.create(context, null /* synchronizer */,
                 floatingContentCoordinator, statusBarService, windowManager,
                 windowManagerShellWrapper, launcherApps, taskStackListener,
-                uiEventLogger, organizer, displayController, mainExecutor, mainHandler));
+                uiEventLogger, organizer, displayController, mainExecutor, mainHandler, syncQueue));
     }
 
     //
@@ -207,11 +228,70 @@
 
     @WMSingleton
     @Provides
-    static FullscreenTaskListener provideFullscreenTaskListener(SyncTransactionQueue syncQueue) {
-        return new FullscreenTaskListener(syncQueue);
+    static FullscreenTaskListener provideFullscreenTaskListener(
+            SyncTransactionQueue syncQueue, Optional<FullscreenUnfoldController> controller) {
+        return new FullscreenTaskListener(syncQueue, controller);
     }
 
     //
+    // Unfold transition
+    //
+
+    @WMSingleton
+    @Provides
+    static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController(
+            Context context,
+            Optional<ShellUnfoldProgressProvider> progressProvider,
+            Lazy<UnfoldBackgroundController> unfoldBackgroundController,
+            DisplayInsetsController displayInsetsController,
+            @ShellMainThread ShellExecutor mainExecutor
+    ) {
+        return progressProvider.map(shellUnfoldTransitionProgressProvider ->
+                new FullscreenUnfoldController(context, mainExecutor,
+                        unfoldBackgroundController.get(), shellUnfoldTransitionProgressProvider,
+                        displayInsetsController));
+    }
+
+    @Provides
+    static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController(
+            Optional<ShellUnfoldProgressProvider> progressProvider,
+            Context context,
+            TransactionPool transactionPool,
+            Lazy<UnfoldBackgroundController> unfoldBackgroundController,
+            DisplayInsetsController displayInsetsController,
+            @ShellMainThread ShellExecutor mainExecutor
+    ) {
+        return progressProvider.map(shellUnfoldTransitionProgressProvider ->
+                new StageTaskUnfoldController(
+                        context,
+                        transactionPool,
+                        shellUnfoldTransitionProgressProvider,
+                        displayInsetsController,
+                        unfoldBackgroundController.get(),
+                        mainExecutor
+                ));
+    }
+
+    @WMSingleton
+    @Provides
+    static UnfoldBackgroundController provideUnfoldBackgroundController(
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            Context context
+    ) {
+        return new UnfoldBackgroundController(
+                context,
+                rootTaskDisplayAreaOrganizer
+        );
+    }
+
+    //
+    // Freeform (optional feature)
+    //
+
+    @BindsOptionalOf
+    abstract Optional<FreeformTaskListener> optionalFreeformTaskListener();
+
+    //
     // Hide display cutout
     //
 
@@ -265,13 +345,21 @@
         return taskSurfaceController.map((controller) -> controller.asTaskSurfaceHelper());
     }
 
-    @WMSingleton
     @Provides
     static Optional<TaskSurfaceHelperController> provideTaskSurfaceHelperController(
             ShellTaskOrganizer taskOrganizer, @ShellMainThread ShellExecutor mainExecutor) {
         return Optional.ofNullable(new TaskSurfaceHelperController(taskOrganizer, mainExecutor));
     }
 
+    @WMSingleton
+    @Provides
+    static Optional<DisplayAreaHelper> provideDisplayAreaHelper(
+            @ShellMainThread ShellExecutor mainExecutor,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer) {
+        return Optional.ofNullable(new DisplayAreaHelperController(mainExecutor,
+                rootDisplayAreaOrganizer));
+    }
+
     //
     // Pip (optional feature)
     //
@@ -327,9 +415,11 @@
     @WMSingleton
     @Provides
     static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool,
-            Context context, @ShellMainThread ShellExecutor mainExecutor,
+            DisplayController displayController, Context context,
+            @ShellMainThread ShellExecutor mainExecutor,
             @ShellAnimationThread ShellExecutor animExecutor) {
-        return new Transitions(organizer, pool, context, mainExecutor, animExecutor);
+        return new Transitions(organizer, pool, displayController, context, mainExecutor,
+                animExecutor);
     }
 
     //
@@ -345,6 +435,13 @@
 
     @WMSingleton
     @Provides
+    static RootDisplayAreaOrganizer provideRootDisplayAreaOrganizer(
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return new RootDisplayAreaOrganizer(mainExecutor);
+    }
+
+    @WMSingleton
+    @Provides
     static Optional<SplitScreen> provideSplitScreen(
             Optional<SplitScreenController> splitScreenController) {
         return splitScreenController.map((controller) -> controller.asSplitScreen());
@@ -357,12 +454,15 @@
             SyncTransactionQueue syncQueue, Context context,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             @ShellMainThread ShellExecutor mainExecutor,
-            DisplayImeController displayImeController, Transitions transitions,
-            TransactionPool transactionPool) {
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController, Transitions transitions,
+            TransactionPool transactionPool,
+            Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) {
         if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
             return Optional.of(new SplitScreenController(shellTaskOrganizer, syncQueue, context,
-                    rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController, transitions,
-                    transactionPool));
+                    rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController,
+                    displayInsetsController, transitions,
+                    transactionPool, stageTaskUnfoldControllerProvider));
         } else {
             return Optional.empty();
         }
@@ -424,8 +524,9 @@
     @Provides
     static TaskViewFactoryController provideTaskViewFactoryController(
             ShellTaskOrganizer shellTaskOrganizer,
-            @ShellMainThread ShellExecutor mainExecutor) {
-        return new TaskViewFactoryController(shellTaskOrganizer, mainExecutor);
+            @ShellMainThread ShellExecutor mainExecutor,
+            SyncTransactionQueue syncQueue) {
+        return new TaskViewFactoryController(shellTaskOrganizer, mainExecutor, syncQueue);
     }
 
     //
@@ -440,7 +541,9 @@
 
     @WMSingleton
     @Provides
-    static ShellInitImpl provideShellInitImpl(DisplayImeController displayImeController,
+    static ShellInitImpl provideShellInitImpl(DisplayController displayController,
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
             DragAndDropController dragAndDropController,
             ShellTaskOrganizer shellTaskOrganizer,
             Optional<BubbleController> bubblesOptional,
@@ -449,10 +552,14 @@
             Optional<AppPairsController> appPairsOptional,
             Optional<PipTouchHandler> pipTouchHandlerOptional,
             FullscreenTaskListener fullscreenTaskListener,
+            Optional<FullscreenUnfoldController> appUnfoldTransitionController,
+            Optional<Optional<FreeformTaskListener>> freeformTaskListener,
             Transitions transitions,
             StartingWindowController startingWindow,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new ShellInitImpl(displayImeController,
+        return new ShellInitImpl(displayController,
+                displayImeController,
+                displayInsetsController,
                 dragAndDropController,
                 shellTaskOrganizer,
                 bubblesOptional,
@@ -461,6 +568,8 @@
                 appPairsOptional,
                 pipTouchHandlerOptional,
                 fullscreenTaskListener,
+                appUnfoldTransitionController,
+                freeformTaskListener,
                 transitions,
                 startingWindow,
                 mainExecutor);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java
index 36fd9be..a7c5ad2 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java
@@ -28,6 +28,7 @@
 import com.android.wm.shell.apppairs.AppPairsController;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -36,6 +37,7 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ChoreographerSfVsync;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.freeform.FreeformTaskListener;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.Pip;
@@ -48,6 +50,7 @@
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransition;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.pip.phone.PhonePipMenuController;
 import com.android.wm.shell.pip.phone.PipAppOpsListener;
@@ -81,10 +84,23 @@
     @WMSingleton
     @Provides
     static DisplayImeController provideDisplayImeController(IWindowManager wmService,
-            DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor,
+            DisplayController displayController, DisplayInsetsController displayInsetsController,
+            @ShellMainThread ShellExecutor mainExecutor,
             TransactionPool transactionPool) {
-        return new DisplayImeController(wmService, displayController, mainExecutor,
-                transactionPool);
+        return new DisplayImeController(wmService, displayController, displayInsetsController,
+                mainExecutor, transactionPool);
+    }
+
+    //
+    // Freeform
+    //
+
+    @WMSingleton
+    @Provides
+    static Optional<FreeformTaskListener> provideFreeformTaskListener(
+            Context context,
+            SyncTransactionQueue syncQueue) {
+        return Optional.ofNullable(FreeformTaskListener.create(context, syncQueue));
     }
 
     //
@@ -110,9 +126,10 @@
     static AppPairsController provideAppPairs(ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue, DisplayController displayController,
             @ShellMainThread ShellExecutor mainExecutor,
-            DisplayImeController displayImeController) {
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController) {
         return new AppPairsController(shellTaskOrganizer, syncQueue, displayController,
-                mainExecutor, displayImeController);
+                mainExecutor, displayImeController, displayInsetsController);
     }
 
     //
@@ -184,8 +201,15 @@
 
     @WMSingleton
     @Provides
+    static PipTransitionState providePipTransitionState() {
+        return new PipTransitionState();
+    }
+
+    @WMSingleton
+    @Provides
     static PipTaskOrganizer providePipTaskOrganizer(Context context,
             SyncTransactionQueue syncTransactionQueue,
+            PipTransitionState pipTransitionState,
             PipBoundsState pipBoundsState,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             PhonePipMenuController menuPhoneController,
@@ -197,7 +221,7 @@
             PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new PipTaskOrganizer(context,
-                syncTransactionQueue, pipBoundsState, pipBoundsAlgorithm,
+                syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm,
                 menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper,
                 pipTransitionController, splitScreenOptional, displayController, pipUiEventLogger,
                 shellTaskOrganizer, mainExecutor);
@@ -215,8 +239,9 @@
     static PipTransitionController providePipTransitionController(Context context,
             Transitions transitions, ShellTaskOrganizer shellTaskOrganizer,
             PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipBoundsState pipBoundsState, PhonePipMenuController pipMenuController) {
-        return new PipTransition(context, pipBoundsState, pipMenuController,
+            PipBoundsState pipBoundsState, PipTransitionState pipTransitionState,
+            PhonePipMenuController pipMenuController) {
+        return new PipTransition(context, pipBoundsState, pipTransitionState, pipMenuController,
                 pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 8077dea..fe0a2a4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -30,6 +29,7 @@
 import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 import androidx.test.filters.SmallTest;
@@ -108,7 +108,7 @@
     private final View mFakeSmartspaceView = new View(mContext);
 
     private KeyguardClockSwitchController mController;
-    private View mStatusArea;
+    private View mSliceView;
 
     @Before
     public void setup() {
@@ -149,8 +149,10 @@
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
         when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors);
 
-        mStatusArea = new View(getContext());
-        when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea);
+        mSliceView = new View(getContext());
+        when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView);
+        when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(
+                new LinearLayout(getContext()));
     }
 
     @Test
@@ -215,7 +217,7 @@
         when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mController.init();
 
-        assertEquals(View.GONE, mStatusArea.getVisibility());
+        assertEquals(View.GONE, mSliceView.getVisibility());
     }
 
     @Test
@@ -223,22 +225,7 @@
         when(mSmartspaceController.isEnabled()).thenReturn(false);
         mController.init();
 
-        assertEquals(View.VISIBLE, mStatusArea.getVisibility());
-    }
-
-    @Test
-    public void testDetachRemovesSmartspaceView() {
-        when(mSmartspaceController.isEnabled()).thenReturn(true);
-        when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
-        mController.init();
-        verify(mView).addView(eq(mFakeSmartspaceView), anyInt(), any());
-
-        ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-        verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture());
-
-        listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView);
-        verify(mView).removeView(mFakeSmartspaceView);
+        assertEquals(View.VISIBLE, mSliceView.getVisibility());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 10ed1d7..e4336fe 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -19,6 +19,9 @@
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
+import static com.android.keyguard.KeyguardClockSwitch.LARGE;
+import static com.android.keyguard.KeyguardClockSwitch.SMALL;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
@@ -94,6 +97,7 @@
         mLargeClockFrame = mKeyguardClockSwitch.findViewById(R.id.lockscreen_clock_view_large);
         mLargeClockView = mKeyguardClockSwitch.findViewById(R.id.animatable_clock_view_large);
         mBigClock = new TextClock(getContext());
+        mKeyguardClockSwitch.mChildrenAreLaidOut = true;
         MockitoAnnotations.initMocks(this);
     }
 
@@ -247,4 +251,36 @@
 
         verify(plugin).setStyle(style);
     }
+
+    @Test
+    public void switchingToBigClock_makesSmallClockDisappear() {
+        mKeyguardClockSwitch.switchToClock(LARGE);
+
+        mKeyguardClockSwitch.mClockInAnim.end();
+        mKeyguardClockSwitch.mClockOutAnim.end();
+
+        assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
+        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
+        assertThat(mClockFrame.getAlpha()).isEqualTo(0);
+    }
+
+    @Test
+    public void switchingToSmallClock_makesBigClockDisappear() {
+        mKeyguardClockSwitch.switchToClock(SMALL);
+
+        mKeyguardClockSwitch.mClockInAnim.end();
+        mKeyguardClockSwitch.mClockOutAnim.end();
+
+        assertThat(mClockFrame.getAlpha()).isEqualTo(1);
+        assertThat(mClockFrame.getVisibility()).isEqualTo(VISIBLE);
+        // only big clock is removed at switch
+        assertThat(mLargeClockFrame.getParent()).isNull();
+        assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
+    }
+
+    @Test
+    public void switchingToBigClock_returnsTrueOnlyWhenItWasNotVisibleBefore() {
+        assertThat(mKeyguardClockSwitch.switchToClock(LARGE)).isTrue();
+        assertThat(mKeyguardClockSwitch.switchToClock(LARGE)).isFalse();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index bb71bed..8e1e42a 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
+import com.android.systemui.statusbar.policy.DevicePostureController
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -62,6 +63,8 @@
     private lateinit var mKeyguardMessageAreaController: KeyguardMessageAreaController
     @Mock
     private lateinit var mLockPatternView: LockPatternView
+    @Mock
+    private lateinit var mPostureController: DevicePostureController
 
     private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
 
@@ -78,7 +81,7 @@
         mKeyguardPatternViewController = KeyguardPatternViewController(mKeyguardPatternView,
         mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
                 mLatencyTracker, mFalsingCollector, mEmergencyButtonController,
-                mKeyguardMessageAreaControllerFactory)
+                mKeyguardMessageAreaControllerFactory, mPostureController)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index ca857c5..64bdc2e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -19,11 +19,13 @@
 import static android.view.WindowInsets.Type.ime;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -33,6 +35,7 @@
 import android.content.res.Resources;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.MotionEvent;
 import android.view.WindowInsetsController;
 
 import androidx.test.filters.SmallTest;
@@ -43,6 +46,7 @@
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -58,6 +62,7 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper()
 public class KeyguardSecurityContainerControllerTest extends SysuiTestCase {
+    private static final int VIEW_WIDTH = 1600;
 
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
@@ -100,6 +105,8 @@
     private EmergencyButtonController mEmergencyButtonController;
     @Mock
     private Resources mResources;
+    @Mock
+    private FalsingCollector mFalsingCollector;
     private Configuration mConfiguration;
 
     private KeyguardSecurityContainerController mKeyguardSecurityContainerController;
@@ -112,7 +119,9 @@
         mConfiguration.setToDefaults(); // Defaults to ORIENTATION_UNDEFINED.
 
         when(mResources.getConfiguration()).thenReturn(mConfiguration);
+        when(mView.getContext()).thenReturn(mContext);
         when(mView.getResources()).thenReturn(mResources);
+        when(mView.getWidth()).thenReturn(VIEW_WIDTH);
         when(mAdminSecondaryLockScreenControllerFactory.create(any(KeyguardSecurityCallback.class)))
                 .thenReturn(mAdminSecondaryLockScreenController);
         when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController);
@@ -131,7 +140,7 @@
                 mView, mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils,
                 mKeyguardUpdateMonitor, mKeyguardSecurityModel, mMetricsLogger, mUiEventLogger,
                 mKeyguardStateController, mKeyguardSecurityViewFlipperController,
-                mConfigurationController)
+                mConfigurationController, mFalsingCollector)
                 .create(mSecurityCallback);
     }
 
@@ -169,18 +178,156 @@
     public void onResourcesUpdate_callsThroughOnRotationChange() {
         // Rotation is the same, shouldn't cause an update
         mKeyguardSecurityContainerController.updateResources();
-        verify(mView, times(0)).updateLayoutForSecurityMode(any());
+        verify(mView, times(0)).setOneHandedMode(anyBoolean());
 
         // Update rotation. Should trigger update
         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
 
         mKeyguardSecurityContainerController.updateResources();
-        verify(mView, times(1)).updateLayoutForSecurityMode(any());
+        verify(mView, times(1)).setOneHandedMode(anyBoolean());
     }
 
     @Test
-    public void updateKeyguardPosition_callsThroughToView() {
+    public void updateKeyguardPosition_callsThroughToViewInOneHandedMode() {
+        when(mView.isOneHandedMode()).thenReturn(true);
+        mKeyguardSecurityContainerController.updateKeyguardPosition(VIEW_WIDTH / 3f);
+        verify(mView).setOneHandedModeLeftAligned(true, false);
+
+        mKeyguardSecurityContainerController.updateKeyguardPosition((VIEW_WIDTH / 3f) * 2);
+        verify(mView).setOneHandedModeLeftAligned(false, false);
+    }
+
+    @Test
+    public void updateKeyguardPosition_ignoredInTwoHandedMode() {
+        when(mView.isOneHandedMode()).thenReturn(false);
         mKeyguardSecurityContainerController.updateKeyguardPosition(1.0f);
-        verify(mView).updateKeyguardPosition(1.0f);
+        verify(mView, never()).setOneHandedModeLeftAligned(anyBoolean(), anyBoolean());
+    }
+
+    private void touchDownLeftSide() {
+        mKeyguardSecurityContainerController.mGlobalTouchListener.onTouchEvent(
+                MotionEvent.obtain(
+                        /* downTime= */0,
+                        /* eventTime= */0,
+                        MotionEvent.ACTION_DOWN,
+                        /* x= */VIEW_WIDTH / 3f,
+                        /* y= */0,
+                        /* metaState= */0));
+    }
+
+    private void touchDownRightSide() {
+        mKeyguardSecurityContainerController.mGlobalTouchListener.onTouchEvent(
+                MotionEvent.obtain(
+                        /* downTime= */0,
+                        /* eventTime= */0,
+                        MotionEvent.ACTION_DOWN,
+                        /* x= */(VIEW_WIDTH / 3f) * 2,
+                        /* y= */0,
+                        /* metaState= */0));
+    }
+
+    @Test
+    public void onInterceptTap_inhibitsFalsingInOneHandedMode() {
+        when(mView.isOneHandedMode()).thenReturn(true);
+        when(mView.isOneHandedModeLeftAligned()).thenReturn(true);
+
+        touchDownLeftSide();
+        verify(mFalsingCollector, never()).avoidGesture();
+
+        // Now on the right.
+        touchDownRightSide();
+        verify(mFalsingCollector).avoidGesture();
+
+        // Move and re-test
+        reset(mFalsingCollector);
+        when(mView.isOneHandedModeLeftAligned()).thenReturn(false);
+
+        // On the right...
+        touchDownRightSide();
+        verify(mFalsingCollector, never()).avoidGesture();
+
+        touchDownLeftSide();
+        verify(mFalsingCollector).avoidGesture();
+    }
+
+    @Test
+    public void showSecurityScreen_oneHandedMode_bothFlagsDisabled_noOneHandedMode() {
+        setUpKeyguardFlags(
+                /* deviceConfigCanUseOneHandedKeyguard= */false,
+                /* sysuiResourceCanUseOneHandedKeyguard= */false);
+
+        when(mKeyguardSecurityViewFlipperController.getSecurityView(
+                eq(SecurityMode.Pattern), any(KeyguardSecurityCallback.class)))
+                .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
+
+        mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
+        verify(mView).setOneHandedMode(false);
+    }
+
+    @Test
+    public void showSecurityScreen_oneHandedMode_deviceFlagDisabled_noOneHandedMode() {
+        setUpKeyguardFlags(
+                /* deviceConfigCanUseOneHandedKeyguard= */false,
+                /* sysuiResourceCanUseOneHandedKeyguard= */true);
+
+        when(mKeyguardSecurityViewFlipperController.getSecurityView(
+                eq(SecurityMode.Pattern), any(KeyguardSecurityCallback.class)))
+                .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
+
+        mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
+        verify(mView).setOneHandedMode(false);
+    }
+
+    @Test
+    public void showSecurityScreen_oneHandedMode_sysUiFlagDisabled_noOneHandedMode() {
+        setUpKeyguardFlags(
+                /* deviceConfigCanUseOneHandedKeyguard= */true,
+                /* sysuiResourceCanUseOneHandedKeyguard= */false);
+
+        when(mKeyguardSecurityViewFlipperController.getSecurityView(
+                eq(SecurityMode.Pattern), any(KeyguardSecurityCallback.class)))
+                .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
+
+        mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
+        verify(mView).setOneHandedMode(false);
+    }
+
+    @Test
+    public void showSecurityScreen_oneHandedMode_bothFlagsEnabled_oneHandedMode() {
+        setUpKeyguardFlags(
+                /* deviceConfigCanUseOneHandedKeyguard= */true,
+                /* sysuiResourceCanUseOneHandedKeyguard= */true);
+
+        when(mKeyguardSecurityViewFlipperController.getSecurityView(
+                eq(SecurityMode.Pattern), any(KeyguardSecurityCallback.class)))
+                .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
+
+        mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
+        verify(mView).setOneHandedMode(true);
+    }
+
+    @Test
+    public void showSecurityScreen_twoHandedMode_bothFlagsEnabled_noOneHandedMode() {
+        setUpKeyguardFlags(
+                /* deviceConfigCanUseOneHandedKeyguard= */true,
+                /* sysuiResourceCanUseOneHandedKeyguard= */true);
+
+        when(mKeyguardSecurityViewFlipperController.getSecurityView(
+                eq(SecurityMode.Password), any(KeyguardSecurityCallback.class)))
+                .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
+
+        mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
+        verify(mView).setOneHandedMode(false);
+    }
+
+    private void setUpKeyguardFlags(
+            boolean deviceConfigCanUseOneHandedKeyguard,
+            boolean sysuiResourceCanUseOneHandedKeyguard) {
+        when(mResources.getBoolean(
+                com.android.internal.R.bool.config_enableDynamicKeyguardPositioning))
+                .thenReturn(deviceConfigCanUseOneHandedKeyguard);
+        when(mResources.getBoolean(
+                R.bool.can_use_one_handed_bouncer))
+                .thenReturn(sysuiResourceCanUseOneHandedKeyguard);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index f5916e7..2efd369 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -28,7 +28,6 @@
 import android.graphics.Insets;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.testing.TestableResources;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
@@ -37,8 +36,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
-import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.Before;
@@ -57,11 +54,6 @@
     private static final int FAKE_MEASURE_SPEC =
             View.MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, View.MeasureSpec.EXACTLY);
 
-    private static final SecurityMode ONE_HANDED_SECURITY_MODE = SecurityMode.PIN;
-    private static final SecurityMode TWO_HANDED_SECURITY_MODE = SecurityMode.Password;
-
-
-
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
 
@@ -90,45 +82,8 @@
     }
 
     @Test
-    public void onMeasure_usesFullWidthWithoutOneHandedMode() {
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */false,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ false,
-                ONE_HANDED_SECURITY_MODE);
-
-        mKeyguardSecurityContainer.measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
-
-        verify(mSecurityViewFlipper).measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
-    }
-
-    @Test
-    public void onMeasure_usesFullWidthWithDeviceFlagDisabled() {
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */false,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ true,
-                ONE_HANDED_SECURITY_MODE);
-
-        mKeyguardSecurityContainer.measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
-        verify(mSecurityViewFlipper).measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
-    }
-
-    @Test
-    public void onMeasure_usesFullWidthWithSysUIFlagDisabled() {
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */true,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ false,
-                ONE_HANDED_SECURITY_MODE);
-
-        mKeyguardSecurityContainer.measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
-        verify(mSecurityViewFlipper).measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
-    }
-
-    @Test
-    public void onMeasure_usesHalfWidthWithFlagsEnabled() {
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */true,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ true,
-                ONE_HANDED_SECURITY_MODE);
+    public void onMeasure_usesHalfWidthWithOneHandedModeEnabled() {
+        mKeyguardSecurityContainer.setOneHandedMode(/* oneHandedMode= */true);
 
         int halfWidthMeasureSpec =
                 View.MeasureSpec.makeMeasureSpec(SCREEN_WIDTH / 2, View.MeasureSpec.EXACTLY);
@@ -138,11 +93,8 @@
     }
 
     @Test
-    public void onMeasure_usesFullWidthForFullScreenIme() {
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */true,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ true,
-                TWO_HANDED_SECURITY_MODE);
+    public void onMeasure_usesFullWidthWithOneHandedModeDisabled() {
+        mKeyguardSecurityContainer.setOneHandedMode(/* oneHandedMode= */false);
 
         mKeyguardSecurityContainer.measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
         verify(mSecurityViewFlipper).measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
@@ -153,10 +105,7 @@
         int imeInsetAmount = 100;
         int systemBarInsetAmount = 10;
 
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */false,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ false,
-                ONE_HANDED_SECURITY_MODE);
+        mKeyguardSecurityContainer.setOneHandedMode(/* oneHandedMode= */false);
 
         Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
         Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -180,10 +129,7 @@
         int imeInsetAmount = 0;
         int systemBarInsetAmount = 10;
 
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */false,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ false,
-                ONE_HANDED_SECURITY_MODE);
+        mKeyguardSecurityContainer.setOneHandedMode(/* oneHandedMode= */false);
 
         Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
         Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -201,56 +147,42 @@
         verify(mSecurityViewFlipper).measure(FAKE_MEASURE_SPEC, expectedHeightMeasureSpec);
     }
 
-    private void setupForUpdateKeyguardPosition(SecurityMode securityMode) {
-        setUpKeyguard(
-                /* deviceConfigCanUseOneHandedKeyguard= */true,
-                /* sysuiResourceCanUseOneHandedKeyguard= */ true,
-                securityMode);
+    private void setupForUpdateKeyguardPosition(boolean oneHandedMode) {
+        mKeyguardSecurityContainer.setOneHandedMode(oneHandedMode);
+        mKeyguardSecurityContainer.setOneHandedModeLeftAligned(true, false);
 
         mKeyguardSecurityContainer.measure(FAKE_MEASURE_SPEC, FAKE_MEASURE_SPEC);
         mKeyguardSecurityContainer.layout(0, 0, SCREEN_WIDTH, SCREEN_WIDTH);
 
-        // Start off left-aligned. This should happen anyway, but just do this to ensure
-        // definitely move to the left.
-        mKeyguardSecurityContainer.updateKeyguardPosition(0.0f);
-
         // Clear any interactions with the mock so we know the interactions definitely come from the
         // below testing.
         reset(mSecurityViewFlipper);
     }
 
     @Test
-    public void updateKeyguardPosition_movesKeyguard() {
-        setupForUpdateKeyguardPosition(ONE_HANDED_SECURITY_MODE);
+    public void setIsLeftAligned_movesKeyguard() {
+        setupForUpdateKeyguardPosition(/* oneHandedMode= */ true);
 
-        mKeyguardSecurityContainer.updateKeyguardPosition((SCREEN_WIDTH / 4f) * 3f);
-        verify(mSecurityViewFlipper).setTranslationX(SCREEN_WIDTH / 2.0f);
+        mKeyguardSecurityContainer.setOneHandedModeLeftAligned(
+                /* leftAligned= */false, /* animate= */false);
+        verify(mSecurityViewFlipper).setTranslationX(
+                mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth());
 
-        mKeyguardSecurityContainer.updateKeyguardPosition(0.0f);
+        mKeyguardSecurityContainer.setOneHandedModeLeftAligned(
+                /* leftAligned= */true, /* animate= */false);
         verify(mSecurityViewFlipper).setTranslationX(0.0f);
     }
 
     @Test
-    public void updateKeyguardPosition_doesntMoveTwoHandedKeyguard() {
-        setupForUpdateKeyguardPosition(TWO_HANDED_SECURITY_MODE);
+    public void setIsLeftAligned_doesntMoveTwoHandedKeyguard() {
+        setupForUpdateKeyguardPosition(/* oneHandedMode= */ false);
 
-        mKeyguardSecurityContainer.updateKeyguardPosition((SCREEN_WIDTH / 4f) * 3f);
+        mKeyguardSecurityContainer.setOneHandedModeLeftAligned(
+                /* leftAligned= */false, /* animate= */false);
         verify(mSecurityViewFlipper, never()).setTranslationX(anyInt());
 
-        mKeyguardSecurityContainer.updateKeyguardPosition(0.0f);
+        mKeyguardSecurityContainer.setOneHandedModeLeftAligned(
+                /* leftAligned= */true, /* animate= */false);
         verify(mSecurityViewFlipper, never()).setTranslationX(anyInt());
     }
-
-    private void setUpKeyguard(
-            boolean deviceConfigCanUseOneHandedKeyguard,
-            boolean sysuiResourceCanUseOneHandedKeyguard,
-            SecurityMode securityMode) {
-        TestableResources testableResources = mContext.getOrCreateTestableResources();
-        testableResources.addOverride(
-                com.android.internal.R.bool.config_enableDynamicKeyguardPositioning,
-                deviceConfigCanUseOneHandedKeyguard);
-        testableResources.addOverride(R.bool.can_use_one_handed_bouncer,
-                sysuiResourceCanUseOneHandedKeyguard);
-        mKeyguardSecurityContainer.updateLayoutForSecurityMode(securityMode);
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
index 1ab08c2..77302ce 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
@@ -54,7 +54,7 @@
         MockitoAnnotations.initMocks(this);
         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
         mKeyguardSliceView = (KeyguardSliceView) layoutInflater
-                .inflate(R.layout.keyguard_status_area, null);
+                .inflate(R.layout.keyguard_slice_view, null);
         mSliceUri = Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI);
         SliceProvider.setSpecs(new HashSet<>(Collections.singletonList(SliceSpecs.LIST)));
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index ec4dfba..e53b450 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -64,7 +64,6 @@
 import android.os.IRemoteCallback;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.Vibrator;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
@@ -78,7 +77,9 @@
 import androidx.lifecycle.Observer;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.util.LatencyTracker;
 import com.android.internal.widget.ILockSettings;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated;
@@ -86,8 +87,8 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -171,7 +172,9 @@
     @Mock
     private FeatureFlags mFeatureFlags;
     @Mock
-    private Vibrator mVibrator;
+    private InteractionJankMonitor mInteractionJankMonitor;
+    @Mock
+    private LatencyTracker mLatencyTracker;
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor;
     // Direct executor
@@ -239,8 +242,6 @@
 
         when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData);
 
-        when(mFeatureFlags.isKeyguardLayoutEnabled()).thenReturn(false);
-
         mMockitoSession = ExtendedMockito.mockitoSession()
                 .spyStatic(SubscriptionManager.class).startMocking();
         ExtendedMockito.doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
@@ -737,6 +738,17 @@
     }
 
     @Test
+    public void testMultiUserJankMonitor_whenUserSwitches() throws Exception {
+        final IRemoteCallback reply = new IRemoteCallback.Stub() {
+            @Override
+            public void sendResult(Bundle data) {} // do nothing
+        };
+        mKeyguardUpdateMonitor.handleUserSwitchComplete(10 /* user */);
+        verify(mInteractionJankMonitor).end(InteractionJankMonitor.CUJ_USER_SWITCH);
+        verify(mLatencyTracker).onActionEnd(LatencyTracker.ACTION_USER_SWITCH);
+    }
+
+    @Test
     public void testGetUserCanSkipBouncer_whenTrust() {
         int user = KeyguardUpdateMonitor.getCurrentUser();
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, user, 0 /* flags */);
@@ -1051,7 +1063,7 @@
                     mRingerModeTracker, mBackgroundExecutor,
                     mStatusBarStateController, mLockPatternUtils,
                     mAuthController, mTelephonyListenerManager, mFeatureFlags,
-                    mVibrator);
+                    mInteractionJankMonitor, mLatencyTracker);
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index ed5cbe2..c6df1c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -17,6 +17,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
 import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
+import static android.view.DisplayCutout.BOUNDS_POSITION_LENGTH;
 import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
 import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
@@ -27,11 +28,13 @@
 
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -43,6 +46,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.res.Configuration;
+import android.content.res.TypedArray;
 import android.graphics.Insets;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -52,6 +56,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
+import android.util.RotationUtils;
 import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.View;
@@ -60,7 +65,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.R.dimen;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
@@ -100,6 +104,8 @@
     private UserTracker mUserTracker;
     @Mock
     private PrivacyDotViewController mDotViewController;
+    @Mock
+    private TypedArray mMockTypedArray;
 
     @Before
     public void setup() {
@@ -121,6 +127,7 @@
                 .getDisplay(DEFAULT_DISPLAY);
         when(mDisplayManager.getDisplay(anyInt())).thenReturn(display);
         mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
+        when(mMockTypedArray.length()).thenReturn(0);
 
         mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
                 mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController,
@@ -146,41 +153,155 @@
         reset(mTunerService);
     }
 
+
+    private void verifyRoundedCornerViewsVisibility(
+            @DisplayCutout.BoundsPosition final int overlayId,
+            @View.Visibility final int visibility) {
+        final View overlay = mScreenDecorations.mOverlays[overlayId];
+        final View left = overlay.findViewById(R.id.left);
+        final View right = overlay.findViewById(R.id.right);
+        assertNotNull(left);
+        assertNotNull(right);
+        assertThat(left.getVisibility()).isEqualTo(visibility);
+        assertThat(right.getVisibility()).isEqualTo(visibility);
+    }
+
+    private void verifyTopDotViewsNullable(final boolean isAssertNull) {
+        if (isAssertNull) {
+            assertNull(mScreenDecorations.mTopLeftDot);
+            assertNull(mScreenDecorations.mTopRightDot);
+        } else {
+            assertNotNull(mScreenDecorations.mTopLeftDot);
+            assertNotNull(mScreenDecorations.mTopRightDot);
+        }
+    }
+
+    private void verifyBottomDotViewsNullable(final boolean isAssertNull) {
+        if (isAssertNull) {
+            assertNull(mScreenDecorations.mBottomLeftDot);
+            assertNull(mScreenDecorations.mBottomRightDot);
+        } else {
+            assertNotNull(mScreenDecorations.mBottomLeftDot);
+            assertNotNull(mScreenDecorations.mBottomRightDot);
+        }
+    }
+
+    private void verifyDotViewsNullable(final boolean isAssertNull) {
+        verifyTopDotViewsNullable(isAssertNull);
+        verifyBottomDotViewsNullable(isAssertNull);
+    }
+
+    private void verifyTopDotViewsVisibility(@View.Visibility final int visibility) {
+        verifyTopDotViewsNullable(false);
+        assertThat(mScreenDecorations.mTopLeftDot.getVisibility()).isEqualTo(visibility);
+        assertThat(mScreenDecorations.mTopRightDot.getVisibility()).isEqualTo(visibility);
+    }
+
+    private void verifyBottomDotViewsVisibility(@View.Visibility final int visibility) {
+        verifyBottomDotViewsNullable(false);
+        assertThat(mScreenDecorations.mBottomLeftDot.getVisibility()).isEqualTo(visibility);
+        assertThat(mScreenDecorations.mBottomRightDot.getVisibility()).isEqualTo(visibility);
+    }
+
+    private void verifyDotViewsVisibility(@View.Visibility final int visibility) {
+        verifyTopDotViewsVisibility(visibility);
+        verifyBottomDotViewsVisibility(visibility);
+    }
+
+    private void verifyOverlaysExistAndAdded(final boolean left, final boolean top,
+            final boolean right, final boolean bottom) {
+        if (left || top || right || bottom) {
+            assertNotNull(mScreenDecorations.mOverlays);
+        } else {
+            verify(mWindowManager, never()).addView(any(), any());
+            return;
+        }
+
+        if (left) {
+            assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
+            verify(mWindowManager, times(1))
+                    .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any());
+        } else {
+            assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
+        }
+
+        if (top) {
+            assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]);
+            verify(mWindowManager, times(1))
+                    .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
+        } else {
+            assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]);
+        }
+
+        if (right) {
+            assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+            verify(mWindowManager, times(1))
+                    .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]), any());
+        } else {
+            assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+        }
+
+        if (bottom) {
+            assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
+            verify(mWindowManager, times(1))
+                    .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any());
+        } else {
+            assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
+        }
+    }
+
     @Test
-    public void testNoRounding_NoCutout() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testNoRounding_NoCutout_NoPrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, false /* privacyDot */);
 
         // no cutout
         doReturn(null).when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
         // No views added.
-        verify(mWindowManager, never()).addView(any(), any());
+        verifyOverlaysExistAndAdded(false, false, false, false);
         // No Tuners tuned.
         verify(mTunerService, never()).addTunable(any(), any());
+        // No dot controller init
+        verify(mDotViewController, never()).initialize(any(), any(), any(), any());
     }
 
     @Test
-    public void testRounding_NoCutout() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testNoRounding_NoCutout_PrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
+
+        // no cutout
+        doReturn(null).when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+
+        // Top and bottom windows are created for privacy dot.
+        // Left and right window should be null.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Rounded corner views shall not exist
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE);
+
+        // Privacy dots shall exist but invisible
+        verifyDotViewsVisibility(View.INVISIBLE);
+
+        // One tunable.
+        verify(mTunerService, times(1)).addTunable(any(), any());
+        // Dot controller init
+        verify(mDotViewController, times(1)).initialize(
+                isA(View.class), isA(View.class), isA(View.class), isA(View.class));
+    }
+
+    @Test
+    public void testRounding_NoCutout_NoPrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, false /* privacyDot */);
 
         // no cutout
         doReturn(null).when(mScreenDecorations).getCutout();
@@ -188,34 +309,57 @@
         mScreenDecorations.start();
 
         // Top and bottom windows are created for rounded corners.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any());
-
         // Left and right window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Rounded corner views shall exist
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE);
+
+        // Privacy dots shall not exist
+        verifyDotViewsNullable(true);
 
         // One tunable.
         verify(mTunerService, times(1)).addTunable(any(), any());
+        // No dot controller init
+        verify(mDotViewController, never()).initialize(any(), any(), any(), any());
+    }
+
+    @Test
+    public void testRounding_NoCutout_PrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
+
+        // no cutout
+        doReturn(null).when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+
+        // Top and bottom windows are created for rounded corners.
+        // Left and right window should be null.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Rounded corner views shall exist
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE);
+
+        // Privacy dots shall exist but invisible
+        verifyDotViewsVisibility(View.INVISIBLE);
+
+        // One tunable.
+        verify(mTunerService, times(1)).addTunable(any(), any());
+        // Dot controller init
+        verify(mDotViewController, times(1)).initialize(
+                isA(View.class), isA(View.class), isA(View.class), isA(View.class));
     }
 
     @Test
     public void testRoundingRadius_NoCutout() {
-        final int testRadius = 1;
         final Point testRadiusPoint = new Point(1, 1);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, testRadius);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, testRadius);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, testRadius);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
-
+        setupResources(1 /* radius */, 1 /* radiusTop */, 1 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
         // no cutout
         doReturn(null).when(mScreenDecorations).getCutout();
 
@@ -230,16 +374,8 @@
     public void testRoundingTopBottomRadius_OnTopBottomOverlay() {
         final int testTopRadius = 1;
         final int testBottomRadius = 5;
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, testTopRadius);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, testTopRadius);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, testBottomRadius);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+        setupResources(testTopRadius, testTopRadius, testBottomRadius, 0 /* roundedPadding */,
+                false /* multipleRadius */, false /* fillCutout */, true /* privacyDot */);
 
         // no cutout
         doReturn(null).when(mScreenDecorations).getCutout();
@@ -267,75 +403,69 @@
     public void testRoundingTopBottomRadius_OnLeftRightOverlay() {
         final int testTopRadius = 1;
         final int testBottomRadius = 5;
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, testTopRadius);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, testTopRadius);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, testBottomRadius);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+        setupResources(testTopRadius, testTopRadius, testBottomRadius, 0 /* roundedPadding */,
+                false /* multipleRadius */, false /* fillCutout */, true /* privacyDot */);
 
         // left cutout
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                new Rect(0, 200, 1, 210),
-                ZERO_RECT,
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
+        final Point topRadius = new Point(testTopRadius, testTopRadius);
+        final Point bottomRadius = new Point(testBottomRadius, testBottomRadius);
         View leftRoundedCorner =
                 mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT].findViewById(R.id.left);
+        boolean isTop = mScreenDecorations.isTopRoundedCorner(BOUNDS_POSITION_LEFT, R.id.left);
+        verify(mScreenDecorations, atLeastOnce())
+                .setSize(leftRoundedCorner, isTop ? topRadius : bottomRadius);
+
         View rightRoundedCorner =
                 mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT].findViewById(R.id.right);
+        isTop = mScreenDecorations.isTopRoundedCorner(BOUNDS_POSITION_LEFT, R.id.right);
         verify(mScreenDecorations, atLeastOnce())
-                .setSize(leftRoundedCorner, new Point(testTopRadius, testTopRadius));
-        verify(mScreenDecorations, atLeastOnce())
-                .setSize(rightRoundedCorner, new Point(testBottomRadius, testBottomRadius));
+                .setSize(rightRoundedCorner, isTop ? topRadius : bottomRadius);
+
         leftRoundedCorner =
                 mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT].findViewById(R.id.left);
+        isTop = mScreenDecorations.isTopRoundedCorner(BOUNDS_POSITION_RIGHT, R.id.left);
+        verify(mScreenDecorations, atLeastOnce())
+                .setSize(leftRoundedCorner, isTop ? topRadius : bottomRadius);
+
         rightRoundedCorner =
                 mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT].findViewById(R.id.right);
+        isTop = mScreenDecorations.isTopRoundedCorner(BOUNDS_POSITION_RIGHT, R.id.right);
         verify(mScreenDecorations, atLeastOnce())
-                .setSize(leftRoundedCorner, new Point(testTopRadius, testTopRadius));
-        verify(mScreenDecorations, atLeastOnce())
-                .setSize(rightRoundedCorner, new Point(testBottomRadius, testBottomRadius));
+                .setSize(rightRoundedCorner, isTop ? topRadius : bottomRadius);
     }
 
     @Test
-    public void testRoundingMultipleRadius_NoCutout() {
+    public void testRoundingMultipleRadius_NoCutout_NoPrivacyDot() {
         final VectorDrawable d = (VectorDrawable) mContext.getDrawable(R.drawable.rounded);
         final Point multipleRadiusSize = new Point(d.getIntrinsicWidth(), d.getIntrinsicHeight());
-
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 9999);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 9999);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, true);
+        setupResources(9999 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                9999 /* roundedPadding */, true /* multipleRadius */,
+                false /* fillCutout */, false /* privacyDot */);
 
         // no cutout
         doReturn(null).when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
         // Top and bottom windows are created for rounded corners.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any());
-
         // Left and right window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Rounded corner views shall exist
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE);
+
+        // Privacy dots shall not exist
+        verifyDotViewsNullable(true);
 
         // One tunable.
         verify(mTunerService, times(1)).addTunable(any(), any());
+        // No dot controller init
+        verify(mDotViewController, never()).initialize(any(), any(), any(), any());
 
         // Size of corner view should exactly match max(width, height) of R.drawable.rounded
         assertThat(mScreenDecorations.mRoundedDefault).isEqualTo(multipleRadiusSize);
@@ -344,263 +474,376 @@
     }
 
     @Test
-    public void testNoRounding_CutoutShortEdge() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testRoundingMultipleRadius_NoCutout_PrivacyDot() {
+        final VectorDrawable d = (VectorDrawable) mContext.getDrawable(R.drawable.rounded);
+        final Point multipleRadiusSize = new Point(d.getIntrinsicWidth(), d.getIntrinsicHeight());
+        setupResources(9999 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                9999 /* roundedPadding */, true /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
+
+        // no cutout
+        doReturn(null).when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Top and bottom windows are created for rounded corners.
+        // Left and right window should be null.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Rounded corner views shall exist
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE);
+
+        // Privacy dots shall exist but invisible
+        verifyDotViewsVisibility(View.INVISIBLE);
+
+        // One tunable.
+        verify(mTunerService, times(1)).addTunable(any(), any());
+        // Dot controller init
+        verify(mDotViewController, times(1)).initialize(
+                isA(View.class), isA(View.class), isA(View.class), isA(View.class));
+
+        // Size of corner view should exactly match max(width, height) of R.drawable.rounded
+        assertThat(mScreenDecorations.mRoundedDefault).isEqualTo(multipleRadiusSize);
+        assertThat(mScreenDecorations.mRoundedDefaultTop).isEqualTo(multipleRadiusSize);
+        assertThat(mScreenDecorations.mRoundedDefaultBottom).isEqualTo(multipleRadiusSize);
+    }
+
+    @Test
+    public void testNoRounding_CutoutShortEdge_NoPrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, false /* privacyDot */);
 
         // top cutout
-        doReturn(new DisplayCutout(
-                        Insets.of(0, 10, 0, 0),
-                        ZERO_RECT,
-                        new Rect(9, 0, 10, 1),
-                        ZERO_RECT,
-                        ZERO_RECT,
-                        Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
         // Top window is created for top cutout.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
-        // Bottom window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
-        // Left window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
-        // Right window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+        // Bottom, left, or right window should be null.
+        verifyOverlaysExistAndAdded(false, true, false, false);
+
+        // Privacy dots shall not exist because of no privacy
+        verifyDotViewsNullable(true);
+
+        // No dot controller init
+        verify(mDotViewController, never()).initialize(any(), any(), any(), any());
     }
 
     @Test
-    public void testNoRounding_CutoutLongEdge() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testNoRounding_CutoutShortEdge_PrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, true /* privacyDot */);
+
+        // top cutout
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Top window is created for top cutout.
+        // Bottom window is created for privacy dot.
+        // Left or right window should be null.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Top rounded corner views shall exist because of cutout
+        // but be gone because of no rounded corner
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE);
+        // Bottom rounded corner views shall exist because of privacy dot
+        // but be gone because of no rounded corner
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE);
+
+        // Privacy dots shall exist but invisible
+        verifyDotViewsVisibility(View.INVISIBLE);
+
+        // Dot controller init
+        verify(mDotViewController, times(1)).initialize(
+                isA(View.class), isA(View.class), isA(View.class), isA(View.class));
+    }
+
+    @Test
+    public void testNoRounding_CutoutLongEdge_NoPrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, false /* privacyDot */);
 
         // left cutout
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                new Rect(0, 200, 1, 210),
-                ZERO_RECT,
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
         // Left window is created for left cutout.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any());
-        // Bottom window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
-        // Top window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]);
-        // Right window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+        // Bottom, top, or right window should be null.
+        verifyOverlaysExistAndAdded(true, false, false, false);
+
+        // Left rounded corner views shall exist because of cutout
+        // but be gone because of no rounded corner
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_LEFT, View.GONE);
+
+        // Top privacy dots shall not exist because of no privacy
+        verifyDotViewsNullable(true);
+
+        // No dot controller init
+        verify(mDotViewController, never()).initialize(any(), any(), any(), any());
     }
 
     @Test
-    public void testRounding_CutoutShortEdge() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
-
-        // top cutout
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                ZERO_RECT,
-                new Rect(9, 0, 10, 1),
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
-
-        mScreenDecorations.start();
-        // Top window is created for rouned corner and top cutout.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
-        // Bottom window is created for rouned corner.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any());
-        // Left window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
-        // Right window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
-    }
-
-    @Test
-    public void testRounding_CutoutLongEdge() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testNoRounding_CutoutLongEdge_PrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, true /* privacyDot */);
 
         // left cutout
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                new Rect(0, 200, 1, 210),
-                ZERO_RECT,
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
-        // Left window is created for rouned corner and left cutout.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any());
-        // Right window is created for rouned corner.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]), any());
-        // Top window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]);
-        // Bottom window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
+        // Left window is created for left cutout.
+        // Right window is created for privacy.
+        // Bottom, or top window should be null.
+        verifyOverlaysExistAndAdded(true, false, true, false);
+
+        // Privacy dots shall exist but invisible
+        verifyDotViewsVisibility(View.INVISIBLE);
+
+        // Dot controller init
+        verify(mDotViewController, times(1)).initialize(
+                isA(View.class), isA(View.class), isA(View.class), isA(View.class));
     }
 
     @Test
-    public void testRounding_CutoutShortAndLongEdge() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testRounding_CutoutShortEdge_NoPrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, false /* privacyDot */);
+
+        // top cutout
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Top window is created for rounded corner and top cutout.
+        // Bottom window is created for rounded corner.
+        // Left, or right window should be null.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Rounded corner views shall exist
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE);
+
+        // Top privacy dots shall not exist because of no privacy dot
+        verifyDotViewsNullable(true);
+
+        // No dot controller init
+        verify(mDotViewController, never()).initialize(any(), any(), any(), any());
+    }
+
+    @Test
+    public void testRounding_CutoutShortEdge_PrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, true /* privacyDot */);
+
+        // top cutout
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Top window is created for rounded corner and top cutout.
+        // Bottom window is created for rounded corner.
+        // Left, or right window should be null.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Rounded corner views shall exist
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE);
+
+        // Top privacy dots shall exist but invisible
+        verifyDotViewsVisibility(View.INVISIBLE);
+
+        // Dot controller init
+        verify(mDotViewController, times(1)).initialize(
+                isA(View.class), isA(View.class), isA(View.class), isA(View.class));
+    }
+
+    @Test
+    public void testRounding_CutoutLongEdge_NoPrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, false /* privacyDot */);
+
+        // left cutout
+        final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Left window is created for rounded corner and left cutout.
+        // Right window is created for rounded corner.
+        // Top, or bottom window should be null.
+        verifyOverlaysExistAndAdded(true, false, true, false);
+    }
+
+    @Test
+    public void testRounding_CutoutLongEdge_PrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, true /* privacyDot */);
+
+        // left cutout
+        final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Left window is created for rounded corner, left cutout, and privacy.
+        // Right window is created for rounded corner and privacy dot.
+        // Top, or bottom window should be null.
+        verifyOverlaysExistAndAdded(true, false, true, false);
+    }
+
+    @Test
+    public void testRounding_CutoutShortAndLongEdge_NoPrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, false /* privacyDot */);
 
         // top and left cutout
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                new Rect(0, 200, 1, 210),
-                new Rect(9, 0, 10, 1),
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] bounds = {new Rect(0, 50, 1, 60), new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
-        // Top window is created for rouned corner and top cutout.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
-        // Bottom window is created for rouned corner.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any());
+        // Top window is created for rounded corner and top cutout.
+        // Bottom window is created for rounded corner.
         // Left window is created for left cutout.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any());
         // Right window should be null.
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+        verifyOverlaysExistAndAdded(true, true, false, true);
     }
 
     @Test
-    public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testRounding_CutoutShortAndLongEdge_PrivacyDot() {
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                20 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, true /* privacyDot */);
+
+        // top and left cutout
+        final Rect[] bounds = {new Rect(0, 50, 1, 60), new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Top window is created for rounded corner and top cutout.
+        // Bottom window is created for rounded corner.
+        // Left window is created for left cutout.
+        // Right window should be null.
+        verifyOverlaysExistAndAdded(true, true, false, true);
+    }
+
+    @Test
+    public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout_NoPrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, false /* privacyDot */);
 
         // Set to short edge cutout(top).
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                ZERO_RECT,
-                new Rect(9, 0, 10, 1),
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
+        verifyOverlaysExistAndAdded(false, true, false, false);
 
         // Switch to long edge cutout(left).
-        // left cutout
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                new Rect(0, 200, 1, 210),
-                ZERO_RECT,
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] newBounds = {new Rect(0, 50, 1, 60), null, null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), newBounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.onConfigurationChanged(new Configuration());
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any());
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]);
+        verifyOverlaysExistAndAdded(true, false, false, false);
     }
 
     @Test
-    public void testDelayedCutout() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(dimen.rounded_corner_content_padding, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+    public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout_PrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                true /* fillCutout */, true /* privacyDot */);
+
+        // Set to short edge cutout(top).
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        verifyOverlaysExistAndAdded(false, true, false, true);
+
+        // Switch to long edge cutout(left).
+        final Rect[] newBounds = {new Rect(0, 50, 1, 60), null, null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), newBounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.onConfigurationChanged(new Configuration());
+        verifyOverlaysExistAndAdded(true, false, true, false);
+    }
+
+    @Test
+    public void testDelayedCutout_NoPrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, false /* privacyDot */);
 
         // top cutout
-        doReturn(new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                ZERO_RECT,
-                new Rect(9, 0, 10, 1),
-                ZERO_RECT,
-                ZERO_RECT,
-                Insets.NONE)).when(mScreenDecorations).getCutout();
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
 
         mScreenDecorations.start();
         assertNull(mScreenDecorations.mOverlays);
 
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, true);
+        when(mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout))
+                .thenReturn(true);
         mScreenDecorations.onConfigurationChanged(new Configuration());
 
         // Only top windows should be added.
-        verify(mWindowManager, times(1))
-                .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any());
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]);
-        assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]);
+        verifyOverlaysExistAndAdded(false, true, false, false);
+    }
+
+    @Test
+    public void testDelayedCutout_PrivacyDot() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
+
+        // top cutout
+        final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
+        doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
+                .when(mScreenDecorations).getCutout();
+
+        mScreenDecorations.start();
+        // Both top and bottom windows should be added because of privacy dot,
+        // but their visibility shall be gone because of no rounding.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE);
+
+        when(mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout))
+                .thenReturn(true);
+        mScreenDecorations.onConfigurationChanged(new Configuration());
+
+        assertNotNull(mScreenDecorations.mOverlays);
+        // Both top and bottom windows should be added because of privacy dot,
+        // but their visibility shall be gone because of no rounding.
+        verifyOverlaysExistAndAdded(false, true, false, true);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE);
+        verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE);
     }
 
     @Test
@@ -612,34 +855,24 @@
 
     @Test
     public void testUpdateRoundedCorners() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
 
         mScreenDecorations.start();
         assertEquals(mScreenDecorations.mRoundedDefault, new Point(20, 20));
 
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 5);
+        when(mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.rounded_corner_radius)).thenReturn(5);
         mScreenDecorations.onConfigurationChanged(null);
         assertEquals(mScreenDecorations.mRoundedDefault, new Point(5, 5));
     }
 
     @Test
     public void testOnlyRoundedCornerRadiusTop() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, 10);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, 0);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+        setupResources(0 /* radius */, 10 /* radiusTop */, 0 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
 
         mScreenDecorations.start();
         assertEquals(new Point(0, 0), mScreenDecorations.mRoundedDefault);
@@ -649,16 +882,9 @@
 
     @Test
     public void testOnlyRoundedCornerRadiusBottom() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, false);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_top, 0);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.dimen.rounded_corner_radius_bottom, 20);
-        mContext.getOrCreateTestableResources()
-                .addOverride(R.bool.config_roundedCornerMultipleRadius, false);
+        setupResources(0 /* radius */, 0 /* radiusTop */, 20 /* radiusBottom */,
+                0 /* roundedPadding */, false /* multipleRadius */,
+                false /* fillCutout */, true /* privacyDot */);
 
         mScreenDecorations.start();
         assertEquals(new Point(0, 0), mScreenDecorations.mRoundedDefault);
@@ -666,7 +892,6 @@
         assertEquals(new Point(20, 20), mScreenDecorations.mRoundedDefaultBottom);
     }
 
-
     @Test
     public void testBoundingRectsToRegion() throws Exception {
         Rect rect = new Rect(1, 2, 3, 4);
@@ -721,4 +946,61 @@
         verify(mTunerService, times(1)).removeTunable(any());
         assertThat(mScreenDecorations.mIsRegistered, is(false));
     }
+
+    private void setupResources(int radius, int radiusTop, int radiusBottom, int roundedPadding,
+            boolean multipleRadius, boolean fillCutout, boolean privacyDot) {
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.array.config_displayUniqueIdArray,
+                new String[]{});
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.array.config_roundedCornerRadiusArray,
+                mMockTypedArray);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.array.config_roundedCornerTopRadiusArray,
+                mMockTypedArray);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.array.config_roundedCornerBottomRadiusArray,
+                mMockTypedArray);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.array.config_roundedCornerDrawableArray,
+                mMockTypedArray);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.array.config_roundedCornerTopDrawableArray,
+                mMockTypedArray);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.array.config_roundedCornerBottomDrawableArray,
+                mMockTypedArray);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.array.config_roundedCornerMultipleRadiusArray,
+                mMockTypedArray);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.dimen.rounded_corner_radius, radius);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.dimen.rounded_corner_radius_top, radiusTop);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.dimen.rounded_corner_radius_bottom, radiusBottom);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.dimen.rounded_corner_content_padding, roundedPadding);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.bool.config_roundedCornerMultipleRadius, multipleRadius);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, fillCutout);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.bool.config_enablePrivacyDot, privacyDot);
+    }
+
+    private DisplayCutout getDisplayCutoutForRotation(Insets safeInsets, Rect[] cutoutBounds) {
+        final int rotation = mContext.getDisplay().getRotation();
+        final Insets insets = RotationUtils.rotateInsets(safeInsets, rotation);
+        final Rect[] sorted = new Rect[BOUNDS_POSITION_LENGTH];
+        for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
+            final int rotatedPos = ScreenDecorations.getBoundPositionFromRotation(i, rotation);
+            if (cutoutBounds[i] != null) {
+                RotationUtils.rotateBounds(cutoutBounds[i], new Rect(0, 0, 100, 200), rotation);
+            }
+            sorted[rotatedPos] = cutoutBounds[i];
+        }
+        return new DisplayCutout(insets, sorted[BOUNDS_POSITION_LEFT], sorted[BOUNDS_POSITION_TOP],
+                sorted[BOUNDS_POSITION_RIGHT], sorted[BOUNDS_POSITION_BOTTOM]);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java b/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java
index ee52c78..0751475 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java
@@ -56,11 +56,6 @@
         return mParent.createDependency(key);
     }
 
-    @Override
-    protected boolean autoRegisterModulesForDump() {
-        return false;
-    }
-
     public <T> boolean hasInstantiatedDependency(Class<T> key) {
         return mObjs.containsKey(key) || mInstantiatedObjects.contains(key);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java
index 5617f1b..1561b20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java
@@ -507,6 +507,30 @@
                 expectedY, mWindowManager.getLayoutParamsFromAttachedView().y);
     }
 
+    @Test
+    public void onScreenSizeChanged_buttonIsShowingOnTheRightSide_expectedPosition() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        final Rect oldDraggableBounds = new Rect(mMagnificationModeSwitch.mDraggableWindowBounds);
+        final float windowHeightFraction =
+                (float) (mWindowManager.getLayoutParamsFromAttachedView().y
+                        - oldDraggableBounds.top) / oldDraggableBounds.height();
+
+        // The window bounds and the draggable bounds are changed due to the screen size change.
+        final Rect tmpRect = new Rect(windowBounds);
+        tmpRect.scale(2);
+        final Rect newWindowBounds = new Rect(tmpRect);
+        mWindowManager.setWindowBounds(newWindowBounds);
+        mMagnificationModeSwitch.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+
+        final int expectedX = mMagnificationModeSwitch.mDraggableWindowBounds.right;
+        final int expectedY = (int) (windowHeightFraction
+                * mMagnificationModeSwitch.mDraggableWindowBounds.height())
+                + mMagnificationModeSwitch.mDraggableWindowBounds.top;
+        assertEquals(expectedX, mWindowManager.getLayoutParamsFromAttachedView().x);
+        assertEquals(expectedY, mWindowManager.getLayoutParamsFromAttachedView().y);
+    }
+
     private void assertModeUnchanged(int expectedMode) {
         final int actualMode = Settings.Secure.getInt(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
index 9621bed..8bb9d42 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
@@ -16,9 +16,6 @@
 
 package com.android.systemui.accessibility;
 
-import static android.view.WindowInsets.Type.systemGestures;
-
-import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.view.Display;
@@ -79,14 +76,11 @@
 
     @Override
     public WindowMetrics getCurrentWindowMetrics() {
-        final Insets systemGesturesInsets = Insets.of(0, 0, 0, 10);
-        final WindowInsets insets = new WindowInsets.Builder()
-                .setInsets(systemGestures(), systemGesturesInsets)
-                .build();
+        final WindowMetrics realMetrics = mWindowManager.getCurrentWindowMetrics();
         final WindowMetrics windowMetrics = new WindowMetrics(
-                mWindowBounds == null ? mWindowManager.getCurrentWindowMetrics().getBounds()
+                mWindowBounds == null ? realMetrics.getBounds()
                         : mWindowBounds,
-                mWindowInsets == null ? insets : mWindowInsets);
+                mWindowInsets == null ?  realMetrics.getWindowInsets() : mWindowInsets);
         return windowMetrics;
     }
 
@@ -106,10 +100,20 @@
         return (WindowManager.LayoutParams) mView.getLayoutParams();
     }
 
+    /**
+     * Sets the given window bounds to current window metrics.
+     *
+     * @param bounds the window bounds
+     */
     public void setWindowBounds(Rect bounds) {
         mWindowBounds = bounds;
     }
 
+    /**
+     * Sets the given window insets to the current window metics.
+     *
+     * @param insets the window insets.
+     */
     public void setWindowInsets(WindowInsets insets) {
         mWindowInsets = insets;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index f62069d..3291a7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility;
 
 import static android.view.Choreographer.FrameCallback;
+import static android.view.WindowInsets.Type.systemGestures;
 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
@@ -45,6 +46,8 @@
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.SystemClock;
@@ -55,6 +58,7 @@
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.View;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 
@@ -167,6 +171,29 @@
     }
 
     @Test
+    public void enableWindowMagnification_LargeScreen_windowSizeIsConstrained() {
+        final int screenSize = mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnification_max_frame_size) * 10;
+        mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+        //We need to initialize new one because the window size is determined when initialization.
+        final WindowMagnificationController controller = new WindowMagnificationController(mContext,
+                mHandler, mSfVsyncFrameProvider,
+                mMirrorWindowControl, mTransaction, mWindowMagnifierCallback, mSysUiState);
+
+        mInstrumentation.runOnMainSync(() -> {
+            controller.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        final int halfScreenSize = screenSize / 2;
+        WindowManager.LayoutParams params = mWindowManager.getLayoutParamsFromAttachedView();
+        // The frame size should be the half of smaller value of window height/width unless it
+        //exceed the max frame size.
+        assertTrue(params.width < halfScreenSize);
+        assertTrue(params.height < halfScreenSize);
+    }
+
+    @Test
     public void deleteWindowMagnification_destroyControl() {
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
@@ -184,6 +211,7 @@
     public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() {
         final WindowManager wm = mContext.getSystemService(WindowManager.class);
         final Rect bounds = wm.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
 
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
@@ -239,25 +267,38 @@
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
             mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
         });
     }
 
     @Test
-    public void onOrientationChanged_enabled_updateDisplayRotationAndLayout() {
+    public void onOrientationChanged_enabled_updateDisplayRotationAndCenterStayAtSamePosition() {
         final Display display = Mockito.spy(mContext.getDisplay());
-        when(display.getRotation()).thenReturn(Surface.ROTATION_90);
+        final int currentRotation = display.getRotation();
+        final int newRotation = (currentRotation + 1) % 4;
+        when(display.getRotation()).thenReturn(newRotation);
         when(mContext.getDisplay()).thenReturn(display);
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
                     Float.NaN);
         });
+        final PointF expectedCenter = new PointF(mWindowMagnificationController.getCenterY(),
+                mWindowMagnificationController.getCenterX());
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+        // Rotate the window clockwise 90 degree.
+        windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+                windowBounds.right);
+        mWindowManager.setWindowBounds(windowBounds);
 
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION);
         });
 
-        assertEquals(Surface.ROTATION_90, mWindowMagnificationController.mRotation);
-        // The first invocation is called when the surface is created.
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
+        final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(),
+                mWindowMagnificationController.getCenterY());
+        assertEquals(expectedCenter, actualCenter);
         verify(mWindowManager, times(2)).updateViewLayout(any(), any());
     }
 
@@ -275,6 +316,54 @@
     }
 
     @Test
+    public void onScreenSizeChanged_enabledAtTheCenterOfScreen_keepSameWindowSizeRatio() {
+        // The default position is at the center of the screen.
+        final float expectedRatio = 0.5f;
+        final Rect testWindowBounds = new Rect(
+                mWindowManager.getCurrentWindowMetrics().getBounds());
+        testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
+                testWindowBounds.right + 100, testWindowBounds.bottom + 100);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        mWindowManager.setWindowBounds(testWindowBounds);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        // The ratio of center to window size should be the same.
+        assertEquals(expectedRatio,
+                mWindowMagnificationController.getCenterX() / testWindowBounds.width(),
+                0);
+        assertEquals(expectedRatio,
+                mWindowMagnificationController.getCenterY() / testWindowBounds.height(),
+                0);
+    }
+    @Test
+    public void screenSizeIsChangedToLarge_enabled_windowSizeIsConstrained() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        final int screenSize = mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnification_max_frame_size) * 10;
+        mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        final int halfScreenSize = screenSize / 2;
+        WindowManager.LayoutParams params = mWindowManager.getLayoutParamsFromAttachedView();
+        // The frame size should be the half of smaller value of window height/width unless it
+        //exceed the max frame size.
+        assertTrue(params.width < halfScreenSize);
+        assertTrue(params.height < halfScreenSize);
+    }
+
+    @Test
     public void onDensityChanged_enabled_updateDimensionsAndResetWindowMagnification() {
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
@@ -419,9 +508,9 @@
     }
 
     @Test
-    public void moveWindowMagnificationToTheBottom_enabled_overlapFlagIsTrue() {
-        final WindowManager wm = mContext.getSystemService(WindowManager.class);
-        final Rect bounds = wm.getCurrentWindowMetrics().getBounds();
+    public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
                     Float.NaN);
@@ -447,4 +536,11 @@
     private boolean hasMagnificationOverlapFlag() {
         return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0;
     }
+
+    private void setSystemGestureInsets() {
+        final WindowInsets testInsets = new WindowInsets.Builder()
+                .setInsets(systemGestures(), Insets.of(0, 0, 0, 10))
+                .build();
+        mWindowManager.setWindowInsets(testInsets);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
index 06e27b5..7e9f84c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
@@ -16,10 +16,12 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.View.OVER_SCROLL_ALWAYS;
 import static android.view.View.OVER_SCROLL_NEVER;
+import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -38,6 +40,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.LayerDrawable;
 import android.testing.AndroidTestingRunner;
@@ -97,14 +100,15 @@
     private AccessibilityFloatingMenuView mMenuView;
     private RecyclerView mListView = new RecyclerView(mContext);
 
-    private int mScreenHeight;
     private int mMenuWindowHeight;
     private int mMenuHalfWidth;
     private int mMenuHalfHeight;
-    private int mScreenHalfWidth;
-    private int mScreenHalfHeight;
+    private int mDisplayHalfWidth;
+    private int mDisplayHalfHeight;
     private int mMaxWindowX;
     private int mMaxWindowY;
+    private final int mDisplayWindowWidth = 1080;
+    private final int mDisplayWindowHeight = 2340;
 
     @Before
     public void initMenuView() {
@@ -112,7 +116,10 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
-
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, mDisplayWindowWidth,
+                mDisplayWindowHeight));
+        when(mWindowMetrics.getWindowInsets()).thenReturn(fakeDisplayInsets());
         mMenuView = spy(
                 new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
     }
@@ -128,15 +135,16 @@
                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
         final int menuWidth = padding * 2 + iconWidthHeight;
         final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
-        final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
-        mScreenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
         mMenuHalfWidth = menuWidth / 2;
         mMenuHalfHeight = menuHeight / 2;
-        mScreenHalfWidth = screenWidth / 2;
-        mScreenHalfHeight = mScreenHeight / 2;
-        mMaxWindowX = screenWidth - margin - menuWidth;
+        mDisplayHalfWidth = mDisplayWindowWidth / 2;
+        mDisplayHalfHeight = mDisplayWindowHeight / 2;
+        int marginStartEnd =
+                mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT
+                        ? margin : 0;
+        mMaxWindowX = mDisplayWindowWidth - marginStartEnd - menuWidth;
         mMenuWindowHeight = menuHeight + margin * 2;
-        mMaxWindowY = mScreenHeight - mMenuWindowHeight;
+        mMaxWindowY = mDisplayWindowHeight - mMenuWindowHeight;
     }
 
     @Test
@@ -275,15 +283,15 @@
         final MotionEvent moveEvent =
                 mMotionEventHelper.obtainMotionEvent(2, 3,
                         MotionEvent.ACTION_MOVE,
-                        /* screenCenterX */mScreenHalfWidth
-                                - /* offsetXToScreenLeftHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */mDisplayHalfWidth
+                                - /* offsetXToDisplayLeftHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
         final MotionEvent upEvent =
                 mMotionEventHelper.obtainMotionEvent(4, 5,
                         MotionEvent.ACTION_UP,
-                        /* screenCenterX */ mScreenHalfWidth
-                                - /* offsetXToScreenLeftHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */ mDisplayHalfWidth
+                                - /* offsetXToDisplayLeftHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
 
         listView.dispatchTouchEvent(downEvent);
         listView.dispatchTouchEvent(moveEvent);
@@ -311,15 +319,15 @@
         final MotionEvent moveEvent =
                 mMotionEventHelper.obtainMotionEvent(2, 3,
                         MotionEvent.ACTION_MOVE,
-                        /* screenCenterX */mScreenHalfWidth
-                                + /* offsetXToScreenRightHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */mDisplayHalfWidth
+                                + /* offsetXToDisplayRightHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
         final MotionEvent upEvent =
                 mMotionEventHelper.obtainMotionEvent(4, 5,
                         MotionEvent.ACTION_UP,
-                        /* screenCenterX */ mScreenHalfWidth
-                                + /* offsetXToScreenRightHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */ mDisplayHalfWidth
+                                + /* offsetXToDisplayRightHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
 
         listView.dispatchTouchEvent(downEvent);
         listView.dispatchTouchEvent(moveEvent);
@@ -328,12 +336,12 @@
 
         assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
         assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
-                /* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight);
+                /* newWindowY = displayCenterY - offsetY */ mDisplayHalfHeight - mMenuHalfHeight);
     }
 
 
     @Test
-    public void tapOnAndDragMenuToScreenSide_transformShapeHalfOval() {
+    public void tapOnAndDragMenuToDisplaySide_transformShapeHalfOval() {
         final Position alignRightPosition = new Position(1.0f, 0.8f);
         final RecyclerView listView = new RecyclerView(mContext);
         final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
@@ -351,13 +359,13 @@
                 mMotionEventHelper.obtainMotionEvent(2, 3,
                         MotionEvent.ACTION_MOVE,
                         /* downX */(currentWindowX + mMenuHalfWidth)
-                                + /* offsetXToScreenRightSide */ mMenuHalfWidth,
+                                + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
                         /* downY */ (currentWindowY +  mMenuHalfHeight));
         final MotionEvent upEvent =
                 mMotionEventHelper.obtainMotionEvent(4, 5,
                         MotionEvent.ACTION_UP,
                         /* downX */(currentWindowX + mMenuHalfWidth)
-                                + /* offsetXToScreenRightSide */ mMenuHalfWidth,
+                                + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
                         /* downY */ (currentWindowY +  mMenuHalfHeight));
 
         listView.dispatchTouchEvent(downEvent);
@@ -419,7 +427,7 @@
     }
 
     @Test
-    public void showMenuAndIme_withHigherIme_alignScreenTopEdge() {
+    public void showMenuAndIme_withHigherIme_alignDisplayTopEdge() {
         final int offset = 99999;
 
         setupBasicMenuView(mMenuView);
@@ -471,10 +479,21 @@
     private WindowInsets fakeImeInsetWith(AccessibilityFloatingMenuView menuView, int offset) {
         // Ensure the keyboard has overlapped on the menu view.
         final int fakeImeHeight =
-                mScreenHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight) + offset;
+                mDisplayWindowHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight)
+                        + offset;
         return new WindowInsets.Builder()
-                .setVisible(ime() | navigationBars(), true)
-                .setInsets(ime() | navigationBars(), Insets.of(0, 0, 0, fakeImeHeight))
+                .setVisible(ime(), true)
+                .setInsets(ime(), Insets.of(0, 0, 0, fakeImeHeight))
+                .build();
+    }
+
+    private WindowInsets fakeDisplayInsets() {
+        final int fakeStatusBarHeight = 75;
+        final int fakeNavigationBarHeight = 125;
+        return new WindowInsets.Builder()
+                .setVisible(systemBars() | displayCutout(), true)
+                .setInsets(systemBars() | displayCutout(),
+                        Insets.of(0, fakeStatusBarHeight, 0, fakeNavigationBarHeight))
                 .build();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
index eb1f15b..3553a0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
@@ -23,12 +23,16 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.test.filters.SmallTest;
@@ -52,6 +56,9 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
+
     private AccessibilityFloatingMenuView mMenuView;
     private BaseTooltipView mToolTipView;
 
@@ -66,6 +73,9 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+        when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
 
         mMenuView = new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition);
         mToolTipView = new BaseTooltipView(mContext, mMenuView);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
index ca4e3e9..9eba49d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
@@ -21,12 +21,16 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.test.filters.SmallTest;
 
@@ -49,6 +53,9 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
+
     private AccessibilityFloatingMenuView mMenuView;
     private DockTooltipView mDockTooltipView;
     private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
@@ -62,6 +69,9 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+        when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
 
         mMenuView = spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition));
         mDockTooltipView = new DockTooltipView(mContext, mMenuView);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
index dae4364..ea104a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
@@ -22,12 +22,15 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -57,6 +60,8 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
     private RecyclerView mListView;
     private AccessibilityFloatingMenuView mMenuView;
     private ItemDelegateCompat mItemDelegateCompat;
@@ -69,6 +74,9 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+        when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
 
         mListView = new RecyclerView(mContext);
         mMenuView =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index 14f112b..d819fa2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -46,6 +46,7 @@
 @RunWithLooper
 class ActivityLaunchAnimatorTest : SysuiTestCase() {
     private val launchContainer = LinearLayout(mContext)
+    private val launchAnimator = LaunchAnimator(mContext, isForTesting = true)
     @Mock lateinit var callback: ActivityLaunchAnimator.Callback
     @Spy private val controller = TestLaunchAnimatorController(launchContainer)
     @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
@@ -56,7 +57,8 @@
 
     @Before
     fun setup() {
-        activityLaunchAnimator = ActivityLaunchAnimator(callback, mContext)
+        activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator)
+        activityLaunchAnimator.callback = callback
     }
 
     private fun startIntentWithAnimation(
@@ -120,7 +122,8 @@
     @Test
     fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
         `when`(callback.isOnKeyguard()).thenReturn(true)
-        val animator = ActivityLaunchAnimator(callback, context)
+        val animator = ActivityLaunchAnimator(launchAnimator)
+        animator.callback = callback
 
         val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
         var animationAdapter: RemoteAnimationAdapter? = null
@@ -196,7 +199,7 @@
         return RemoteAnimationTarget(
                 0, RemoteAnimationTarget.MODE_OPENING, SurfaceControl(), false, Rect(), Rect(), 0,
                 Point(), Rect(), bounds, WindowConfiguration(), false, SurfaceControl(), Rect(),
-                taskInfo
+                taskInfo, false
         )
     }
 }
@@ -208,7 +211,7 @@
 private class TestLaunchAnimatorController(
     override var launchContainer: ViewGroup
 ) : ActivityLaunchAnimator.Controller {
-    override fun createAnimatorState() = ActivityLaunchAnimator.State(
+    override fun createAnimatorState() = LaunchAnimator.State(
             top = 100,
             bottom = 200,
             left = 300,
@@ -232,7 +235,7 @@
     }
 
     override fun onLaunchAnimationProgress(
-        state: ActivityLaunchAnimator.State,
+        state: LaunchAnimator.State,
         progress: Float,
         linearProgress: Float
     ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
new file mode 100644
index 0000000..5bcf828
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -0,0 +1,186 @@
+package com.android.systemui.animation
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.ViewUtils
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class DialogLaunchAnimatorTest : SysuiTestCase() {
+    private val launchAnimator = LaunchAnimator(context, isForTesting = true)
+    private val hostDialogprovider = TestHostDialogProvider()
+    private val dialogLaunchAnimator =
+        DialogLaunchAnimator(context, launchAnimator, hostDialogprovider)
+
+    @Test
+    fun testShowDialogFromView() {
+        // Show the dialog. showFromView() must be called on the main thread with a dialog created
+        // on the main thread too.
+        val (dialog, hostDialog) = runOnMainThreadAndWaitForIdleSync {
+            val touchSurfaceRoot = LinearLayout(context)
+            val touchSurface = View(context)
+            touchSurfaceRoot.addView(touchSurface)
+
+            // We need to attach the root to the window manager otherwise the exit animation will
+            // be skipped
+            ViewUtils.attachView(touchSurfaceRoot)
+
+            val dialog = TestDialog(context)
+            val hostDialog =
+                dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog
+            dialog to hostDialog
+        }
+
+        // Only the host dialog is actually showing.
+        assertTrue(hostDialog.isShowing)
+        assertFalse(dialog.isShowing)
+
+        // The dialog onStart() method was called but not onStop().
+        assertTrue(dialog.onStartCalled)
+        assertFalse(dialog.onStopCalled)
+
+        // The dialog content has been stolen and is shown inside the host dialog.
+        val hostDialogContent = hostDialog.findViewById<ViewGroup>(android.R.id.content)
+        assertEquals(0, dialog.findViewById<ViewGroup>(android.R.id.content).childCount)
+        assertEquals(1, hostDialogContent.childCount)
+
+        val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup
+        assertEquals(1, hostDialogRoot.childCount)
+        assertEquals(dialog.contentView, hostDialogRoot.getChildAt(0))
+
+        // If we are dozing, the host dialog window also fades out.
+        runOnMainThreadAndWaitForIdleSync { dialogLaunchAnimator.onDozeAmountChanged(0.5f) }
+        assertTrue(hostDialog.window!!.decorView.alpha < 1f)
+
+        // Hiding/showing/dismissing the dialog should hide/show/dismiss the host dialog given that
+        // it's a ListenableDialog.
+        runOnMainThreadAndWaitForIdleSync { dialog.hide() }
+        assertFalse(hostDialog.isShowing)
+        assertFalse(dialog.isShowing)
+
+        runOnMainThreadAndWaitForIdleSync { dialog.show() }
+        assertTrue(hostDialog.isShowing)
+        assertFalse(dialog.isShowing)
+
+        assertFalse(dialog.onStopCalled)
+        runOnMainThreadAndWaitForIdleSync { dialog.dismiss() }
+        assertFalse(hostDialog.isShowing)
+        assertFalse(dialog.isShowing)
+        assertTrue(hostDialog.wasDismissed)
+        assertTrue(dialog.onStopCalled)
+    }
+
+    private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
+        lateinit var result: T
+        context.mainExecutor.execute {
+            result = f()
+        }
+        waitForIdleSync()
+        return result
+    }
+
+    private class TestHostDialogProvider : HostDialogProvider {
+        override fun createHostDialog(
+            context: Context,
+            theme: Int,
+            onCreateCallback: () -> Unit,
+            dismissOverride: (() -> Unit) -> Unit
+        ): Dialog = TestHostDialog(context, onCreateCallback, dismissOverride)
+    }
+
+    private class TestHostDialog(
+        context: Context,
+        private val onCreateCallback: () -> Unit,
+        private val dismissOverride: (() -> Unit) -> Unit
+    ) : Dialog(context) {
+        var wasDismissed = false
+
+        init {
+            // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw.
+            window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
+        }
+
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            onCreateCallback()
+        }
+
+        override fun dismiss() {
+            dismissOverride {
+                super.dismiss()
+                wasDismissed = true
+            }
+        }
+    }
+
+    private class TestDialog(context: Context) : Dialog(context), ListenableDialog {
+        private val listeners = hashSetOf<DialogListener>()
+        val contentView = View(context)
+        var onStartCalled = false
+        var onStopCalled = false
+
+        init {
+            // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw.
+            window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
+        }
+
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            setContentView(contentView)
+        }
+
+        override fun onStart() {
+            super.onStart()
+            onStartCalled = true
+        }
+
+        override fun onStop() {
+            super.onStart()
+            onStopCalled = true
+        }
+
+        override fun addListener(listener: DialogListener) {
+            listeners.add(listener)
+        }
+
+        override fun removeListener(listener: DialogListener) {
+            listeners.remove(listener)
+        }
+
+        override fun dismiss() {
+            super.dismiss()
+            notifyListeners { onDismiss() }
+        }
+
+        override fun hide() {
+            super.hide()
+            notifyListeners { onHide() }
+        }
+
+        override fun show() {
+            super.show()
+            notifyListeners { onShow() }
+        }
+
+        private fun notifyListeners(notify: DialogListener.() -> Unit) {
+            for (listener in HashSet(listeners)) {
+                listener.notify()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
index 8cba25d..58e0cb2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
@@ -32,7 +32,7 @@
     fun animatingOrphanViewDoesNotCrash() {
         val ghostedView = LinearLayout(mContext)
         val controller = GhostedViewLaunchAnimatorController(ghostedView)
-        val state = ActivityLaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
+        val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
 
         controller.onIntentStarted(willAnimate = true)
         controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
new file mode 100644
index 0000000..1d038a4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.battery;
+
+import static android.provider.Settings.System.SHOW_BATTERY_PERCENT;
+
+import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.eq;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.provider.Settings;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.tuner.TunerService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+public class BatteryMeterViewControllerTest extends SysuiTestCase {
+    @Mock
+    private BatteryMeterView mBatteryMeterView;
+
+    @Mock
+    private ConfigurationController mConfigurationController;
+    @Mock
+    private TunerService mTunerService;
+    @Mock
+    private BroadcastDispatcher mBroadcastDispatcher;
+    @Mock
+    private Handler mHandler;
+    @Mock
+    private ContentResolver mContentResolver;
+    @Mock
+    private BatteryController mBatteryController;
+
+    private BatteryMeterViewController mController;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mBatteryMeterView.getContext()).thenReturn(mContext);
+        when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
+
+        mController = new BatteryMeterViewController(
+                mBatteryMeterView,
+                mConfigurationController,
+                mTunerService,
+                mBroadcastDispatcher,
+                mHandler,
+                mContentResolver,
+                mBatteryController
+        );
+    }
+
+    @Test
+    public void onViewAttached_callbacksRegistered() {
+        mController.onViewAttached();
+
+        verify(mConfigurationController).addCallback(any());
+        verify(mTunerService).addTunable(any(), any());
+        verify(mContentResolver).registerContentObserver(
+                eq(Settings.System.getUriFor(SHOW_BATTERY_PERCENT)), anyBoolean(), any(), anyInt()
+        );
+        verify(mContentResolver).registerContentObserver(
+                eq(Settings.Global.getUriFor(Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME)),
+                anyBoolean(),
+                any()
+        );
+        verify(mBatteryController).addCallback(any());
+    }
+
+    @Test
+    public void onViewDetached_callbacksUnregistered() {
+        // Set everything up first.
+        mController.onViewAttached();
+
+        mController.onViewDetached();
+
+        verify(mConfigurationController).removeCallback(any());
+        verify(mTunerService).removeTunable(any());
+        verify(mContentResolver).unregisterContentObserver(any());
+        verify(mBatteryController).removeCallback(any());
+    }
+
+    @Test
+    public void ignoreTunerUpdates_afterOnViewAttached_callbackUnregistered() {
+        // Start out receiving tuner updates
+        mController.onViewAttached();
+
+        mController.ignoreTunerUpdates();
+
+        verify(mTunerService).removeTunable(any());
+    }
+
+    @Test
+    public void ignoreTunerUpdates_beforeOnViewAttached_callbackNeverRegistered() {
+        mController.ignoreTunerUpdates();
+
+        mController.onViewAttached();
+
+        verify(mTunerService, never()).addTunable(any(), any());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
new file mode 100644
index 0000000..b4ff2a5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.battery
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
+import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class BatteryMeterViewTest : SysuiTestCase() {
+
+    private lateinit var mBatteryMeterView: BatteryMeterView
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mBatteryMeterView = BatteryMeterView(mContext, null)
+    }
+
+    @Test
+    fun updatePercentText_estimateModeAndNotCharging_estimateFetched() {
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo(ESTIMATE)
+    }
+
+    @Test
+    fun updatePercentText_noBatteryEstimateFetcher_noCrash() {
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+
+        mBatteryMeterView.updatePercentText()
+        // No assert needed
+    }
+
+    private class Fetcher : BatteryEstimateFetcher {
+        override fun fetchBatteryTimeRemainingEstimate(
+                completion: EstimateFetchCompletion) {
+            completion.onBatteryRemainingEstimateRetrieved(ESTIMATE)
+        }
+    }
+
+    private companion object {
+        const val ESTIMATE = "2 hours 2 minutes"
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java
index f91c029..619d48d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java
@@ -29,6 +29,7 @@
 
 import android.content.Context;
 import android.hardware.biometrics.ComponentInfoInternal;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
@@ -47,6 +48,7 @@
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -165,6 +167,7 @@
     }
 
     @Test
+    @Ignore("flaky, b/189031816")
     public void testModeUpdated_onSoftError_whenSwitchToFingerprint() {
         mFaceToFpView.onDialogAnimatedIn();
         mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face");
@@ -181,6 +184,7 @@
     }
 
     @Test
+    @Ignore("flaky, b/189031816")
     public void testModeUpdated_onHardError_whenSwitchToFingerprint() {
         mFaceToFpView.onDialogAnimatedIn();
         mFaceToFpView.onError(TYPE_FACE, "oh no!");
@@ -254,9 +258,10 @@
                 componentInfo,
                 FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
                 true /* resetLockoutRequiresHardwareAuthToken */,
-                540 /* sensorLocationX */,
-                1600 /* sensorLocationY */,
-                100 /* sensorRadius */);
+                List.of(new SensorLocationInternal("" /* displayId */,
+                        540 /* sensorLocationX */,
+                        1600 /* sensorLocationY */,
+                        100 /* sensorRadius */)));
     }
 
     public class TestableView extends AuthBiometricFaceToFingerprintView {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
index bd518ff..f8e38e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java
@@ -45,6 +45,7 @@
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -199,6 +200,7 @@
     }
 
     @Test
+    @Ignore("flaky, b/189031816")
     public void testError_sendsActionError() {
         initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
         final String testError = "testError";
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
index f2f0029..2c4808a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
@@ -21,11 +21,12 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -34,6 +35,8 @@
 import com.android.systemui.statusbar.phone.StatusBar
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.leak.RotationUtils
+import org.junit.After
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -48,12 +51,15 @@
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
 import javax.inject.Provider
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class AuthRippleControllerTest : SysuiTestCase() {
+    private lateinit var staticMockSession: MockitoSession
+
     private lateinit var controller: AuthRippleController
     @Mock private lateinit var statusBar: StatusBar
     @Mock private lateinit var rippleView: AuthRippleView
@@ -74,6 +80,12 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        staticMockSession = mockitoSession()
+                .mockStatic(RotationUtils::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+
+        `when`(RotationUtils.getRotation(context)).thenReturn(RotationUtils.ROTATION_NONE)
         `when`(udfpsControllerProvider.get()).thenReturn(udfpsController)
 
         controller = AuthRippleController(
@@ -96,6 +108,11 @@
         `when`(statusBar.lightRevealScrim).thenReturn(lightRevealScrim)
     }
 
+    @After
+    fun tearDown() {
+        staticMockSession.finishMocking()
+    }
+
     @Test
     fun testFingerprintTrigger_Ripple() {
         // GIVEN fp exists, keyguard is visible, user doesn't need strong auth
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
index 977b05c..5fee7fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
@@ -16,6 +16,9 @@
 
 package com.android.systemui.biometrics
 
+import android.graphics.Rect
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
 import android.hardware.biometrics.SensorProperties
 import android.hardware.display.DisplayManager
 import android.hardware.display.DisplayManagerGlobal
@@ -30,8 +33,12 @@
 import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
 import android.view.DisplayInfo
 import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowInsets
 import android.view.WindowManager
+import android.view.WindowMetrics
 import androidx.test.filters.SmallTest
+import com.airbnb.lottie.LottieAnimationView
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -42,9 +49,13 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 
@@ -66,11 +77,13 @@
     @Mock
     lateinit var windowManager: WindowManager
     @Mock
-    lateinit var sidefpsView: SidefpsView
+    lateinit var sidefpsView: View
     @Mock
     lateinit var displayManager: DisplayManager
     @Mock
     lateinit var handler: Handler
+    @Captor
+    lateinit var overlayCaptor: ArgumentCaptor<View>
 
     private val executor = FakeExecutor(FakeSystemClock())
     private lateinit var overlayController: ISidefpsController
@@ -79,6 +92,8 @@
     @Before
     fun setup() {
         `when`(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sidefpsView)
+        `when`(sidefpsView.findViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
+            .thenReturn(mock(LottieAnimationView::class.java))
         `when`(fingerprintManager.sensorPropertiesInternal).thenReturn(
                 listOf(
                         FingerprintSensorPropertiesInternal(
@@ -99,6 +114,9 @@
                         DEFAULT_DISPLAY_ADJUSTMENTS
                 )
         )
+        `when`(windowManager.maximumWindowMetrics).thenReturn(
+            WindowMetrics(Rect(0, 0, 800, 800), WindowInsets.CONSUMED)
+        )
 
         sideFpsController = SidefpsController(
                 mContext, layoutInflater, fingerprintManager, windowManager, executor,
@@ -112,13 +130,61 @@
 
     @Test
     fun testSubscribesToOrientationChangesWhenShowingOverlay() {
-        overlayController.show()
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
         executor.runAllReady()
 
         verify(displayManager).registerDisplayListener(any(), eq(handler))
 
-        overlayController.hide()
+        overlayController.hide(SENSOR_ID)
         executor.runAllReady()
         verify(displayManager).unregisterDisplayListener(any())
     }
+
+    @Test
+    fun testShowsAndHides() {
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        verify(windowManager).addView(overlayCaptor.capture(), any())
+
+        reset(windowManager)
+        overlayController.hide(SENSOR_ID)
+        executor.runAllReady()
+
+        verify(windowManager, never()).addView(any(), any())
+        verify(windowManager).removeView(eq(overlayCaptor.value))
+    }
+
+    @Test
+    fun testShowsOnce() {
+        repeat(5) {
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+        }
+
+        verify(windowManager).addView(any(), any())
+        verify(windowManager, never()).removeView(any())
+    }
+
+    @Test
+    fun testHidesOnce() {
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        repeat(5) {
+            overlayController.hide(SENSOR_ID)
+            executor.runAllReady()
+        }
+
+        verify(windowManager).addView(any(), any())
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun testIgnoredForKeyguard() {
+        overlayController.show(SENSOR_ID, REASON_AUTH_KEYGUARD)
+        executor.runAllReady()
+
+        verify(windowManager, never()).addView(any(), any())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 04ebee89..3a728f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -32,6 +32,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.res.TypedArray;
+import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.display.DisplayManager;
@@ -141,12 +142,12 @@
     @Mock
     private KeyguardStateController mKeyguardStateController;
     @Mock
-    private KeyguardBypassController mKeyguardBypassController;
-    @Mock
     private DisplayManager mDisplayManager;
     @Mock
     private Handler mHandler;
     @Mock
+    private KeyguardBypassController mKeyguardBypassController;
+    @Mock
     private ConfigurationController mConfigurationController;
     @Mock
     private SystemClock mSystemClock;
@@ -218,7 +219,7 @@
                 mWindowManager,
                 mStatusBarStateController,
                 mFgExecutor,
-                mStatusBar,
+                Optional.of(mStatusBar),
                 mStatusBarKeyguardViewManager,
                 mDumpManager,
                 mKeyguardUpdateMonitor,
@@ -257,7 +258,7 @@
     @Test
     public void dozeTimeTick() throws RemoteException {
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         mUdfpsController.dozeTimeTick();
         verify(mUdfpsView).dozeTimeTick();
@@ -272,7 +273,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN ACTION_DOWN is received
@@ -295,7 +296,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN ACTION_DOWN is received
@@ -318,7 +319,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN ACTION_DOWN is received
@@ -341,7 +342,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN ACTION_DOWN is received
@@ -363,7 +364,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN ACTION_MOVE is received
@@ -385,7 +386,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN multiple touches are received
@@ -405,7 +406,7 @@
     @Test
     public void showUdfpsOverlay_addsViewToWindow() throws RemoteException {
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         verify(mWindowManager).addView(eq(mUdfpsView), any());
     }
@@ -413,7 +414,7 @@
     @Test
     public void hideUdfpsOverlay_removesViewFromWindow() throws RemoteException {
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID);
         mFgExecutor.runAllReady();
         verify(mWindowManager).removeView(eq(mUdfpsView));
@@ -423,7 +424,7 @@
     public void hideUdfpsOverlay_resetsAltAuthBouncerWhenShowing() throws RemoteException {
         // GIVEN overlay was showing and the udfps bouncer is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(true);
 
         // WHEN the overlay is hidden
@@ -437,7 +438,7 @@
     @Test
     public void testSubscribesToOrientationChangesWhenShowingOverlay() throws Exception {
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         verify(mDisplayManager).registerDisplayListener(any(), eq(mHandler));
@@ -456,7 +457,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         // WHEN ACTION_DOWN is received
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
@@ -480,7 +481,7 @@
     public void aodInterrupt() throws RemoteException {
         // GIVEN that the overlay is showing and screen is on and fp is running
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
@@ -498,7 +499,7 @@
     public void cancelAodInterrupt() throws RemoteException {
         // GIVEN AOD interrupt
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
@@ -514,7 +515,7 @@
     public void aodInterruptTimeout() throws RemoteException {
         // GIVEN AOD interrupt
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
@@ -531,7 +532,7 @@
     public void aodInterruptScreenOff() throws RemoteException {
         // GIVEN screen off
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOff();
         mFgExecutor.runAllReady();
 
@@ -547,7 +548,8 @@
     public void aodInterrupt_fingerprintNotRunning() throws RemoteException {
         // GIVEN showing overlay
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD,
+                mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
 
@@ -567,7 +569,7 @@
 
         // GIVEN that the overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN ACTION_DOWN is received
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
index 88b4039..27755ede 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.hardware.biometrics.ComponentInfoInternal;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
@@ -61,8 +62,9 @@
                 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
                 componentInfo,
                 FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
-                true /* resetLockoutRequiresHardwareAuthToken */, sensorLocationX, sensorLocationY,
-                sensorRadius);
+                true /* resetLockoutRequiresHardwareAuthToken */,
+                List.of(new SensorLocationInternal("" /* displayId */,
+                        sensorLocationX, sensorLocationY, sensorRadius)));
 
         assertEquals(970,
                 UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForPortrait(
@@ -125,8 +127,9 @@
                 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
                 componentInfo,
                 FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
-                true /* resetLockoutRequiresHardwareAuthToken */, sensorLocationX, sensorLocationY,
-                sensorRadius);
+                true /* resetLockoutRequiresHardwareAuthToken */,
+                List.of(new SensorLocationInternal("" /* displayId */,
+                        sensorLocationX, sensorLocationY, sensorRadius)));
 
         assertEquals(1205,
                 UdfpsDialogMeasureAdapter.calculateHorizontalSpacerWidthForLandscape(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index 4b35de1..2821f3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -57,9 +57,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
 import java.util.List;
 
-
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
@@ -121,7 +121,7 @@
         mController = new UdfpsKeyguardViewController(
                 mView,
                 mStatusBarStateController,
-                mStatusBar,
+                Optional.of(mStatusBar),
                 mStatusBarKeyguardViewManager,
                 mKeyguardUpdateMonitor,
                 mDumpManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java b/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java
index 41747f4..15e5e1c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java
@@ -35,6 +35,7 @@
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.colorextraction.types.Tonal;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import org.junit.Before;
@@ -60,6 +61,8 @@
 
     @Mock
     private WallpaperManager mWallpaperManager;
+    @Mock
+    private DumpManager mDumpManager;
     private ColorExtractor.GradientColors mColors;
     private SysuiColorExtractor mColorExtractor;
 
@@ -69,13 +72,18 @@
         mColors = new ColorExtractor.GradientColors();
         mColors.setMainColor(Color.RED);
         mColors.setSecondaryColor(Color.RED);
-        mColorExtractor = new SysuiColorExtractor(getContext(),
+        mColorExtractor = new SysuiColorExtractor(
+                getContext(),
                 (inWallpaperColors, outGradientColorsNormal, outGradientColorsDark,
                         outGradientColorsExtraDark) -> {
                     outGradientColorsNormal.set(mColors);
                     outGradientColorsDark.set(mColors);
                     outGradientColorsExtraDark.set(mColors);
-                }, mock(ConfigurationController.class), mWallpaperManager, true /* immediately */);
+                },
+                mock(ConfigurationController.class),
+                mWallpaperManager,
+                mDumpManager,
+                true /* immediately */);
     }
 
     @Test
@@ -111,8 +119,13 @@
     public void onUiModeChanged_reloadsColors() {
         Tonal tonal = mock(Tonal.class);
         ConfigurationController configurationController = mock(ConfigurationController.class);
-        SysuiColorExtractor sysuiColorExtractor = new SysuiColorExtractor(getContext(),
-                tonal, configurationController, mWallpaperManager, true /* immediately */);
+        SysuiColorExtractor sysuiColorExtractor = new SysuiColorExtractor(
+                getContext(),
+                tonal,
+                configurationController,
+                mWallpaperManager,
+                mDumpManager,
+                true /* immediately */);
         verify(configurationController).addCallback(eq(sysuiColorExtractor));
 
         reset(tonal);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
index d6226aa..364b5d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
@@ -40,7 +40,7 @@
         when(params.doubleTapReportsTouchCoordinates()).thenReturn(false);
         when(params.getDisplayNeedsBlanking()).thenReturn(false);
         when(params.getSelectivelyRegisterSensorsUsingProx()).thenReturn(false);
-        when(params.singleTapUsesProx()).thenReturn(true);
+        when(params.singleTapUsesProx(anyInt())).thenReturn(true);
         when(params.longPressUsesProx()).thenReturn(true);
         when(params.getQuickPickupAodDuration()).thenReturn(500);
 
@@ -61,14 +61,14 @@
         when(config.getWakeLockScreenDebounce()).thenReturn(0L);
 
         when(config.doubleTapSensorType()).thenReturn(null);
-        when(config.tapSensorType()).thenReturn(null);
         when(config.longPressSensorType()).thenReturn(null);
         when(config.udfpsLongPressSensorType()).thenReturn(null);
         when(config.quickPickupSensorType()).thenReturn(null);
 
         when(config.tapGestureEnabled(anyInt())).thenReturn(true);
         when(config.tapSensorAvailable()).thenReturn(true);
-        when(config.tapSensorType()).thenReturn(FakeSensorManager.TAP_SENSOR_TYPE);
+        when(config.tapSensorTypeMapping()).thenReturn(
+                new String[]{FakeSensorManager.TAP_SENSOR_TYPE});
 
         when(config.dozePickupSensorAvailable()).thenReturn(false);
         when(config.wakeScreenGestureAvailable()).thenReturn(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
index b8d465a..886f84e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
@@ -51,7 +51,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.concurrency.FakeThreadFactory;
 import com.android.systemui.util.sensors.AsyncSensorManager;
@@ -61,6 +61,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -77,6 +78,7 @@
 
     private DozeServiceFake mServiceFake;
     private FakeSensorManager.FakeGenericSensor mSensor;
+    private FakeSensorManager.FakeGenericSensor mSensorInner;
     private AsyncSensorManager mSensorManager;
     private AlwaysOnDisplayPolicy mAlwaysOnDisplayPolicy;
     @Mock
@@ -88,7 +90,9 @@
     @Mock
     DockManager mDockManager;
     @Mock
-    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    DevicePostureController mDevicePostureController;
+    @Mock
+    DozeLog mDozeLog;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeThreadFactory mFakeThreadFactory = new FakeThreadFactory(mFakeExecutor);
 
@@ -114,10 +118,19 @@
         mAlwaysOnDisplayPolicy.dimBrightness = DIM_BRIGHTNESS;
         mAlwaysOnDisplayPolicy.dimmingScrimArray = SENSOR_TO_OPACITY;
         mSensor = fakeSensorManager.getFakeLightSensor();
-        mScreen = new DozeScreenBrightness(mContext, mServiceFake, mSensorManager,
-                Optional.of(mSensor.getSensor()), mDozeHost, null /* handler */,
-                mAlwaysOnDisplayPolicy, mWakefulnessLifecycle, mDozeParameters, mDockManager,
-                mUnlockedScreenOffAnimationController);
+        mSensorInner = fakeSensorManager.getFakeLightSensor2();
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{Optional.of(mSensor.getSensor())},
+                mDozeHost,
+                null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog);
     }
 
     @Test
@@ -155,7 +168,7 @@
 
     @Test
     public void doze_doesNotUseLightSensor() {
-        // GIVEN the device is docked and the display state changes to ON
+        // GIVEN the device is DOZE and the display state changes to ON
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
         waitForSensorManager();
@@ -170,7 +183,7 @@
 
     @Test
     public void aod_usesLightSensor() {
-        // GIVEN the device is docked and the display state changes to ON
+        // GIVEN the device is DOZE_AOD and the display state changes to ON
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
         waitForSensorManager();
@@ -213,10 +226,17 @@
 
     @Test
     public void testPulsing_withoutLightSensor_setsAoDDimmingScrimTransparent() throws Exception {
-        mScreen = new DozeScreenBrightness(mContext, mServiceFake, mSensorManager,
-                Optional.empty() /* sensor */, mDozeHost, null /* handler */,
-                mAlwaysOnDisplayPolicy, mWakefulnessLifecycle, mDozeParameters, mDockManager,
-                mUnlockedScreenOffAnimationController);
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[] {Optional.empty()} /* sensor */,
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog);
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
         reset(mDozeHost);
@@ -243,10 +263,17 @@
 
     @Test
     public void testNullSensor() throws Exception {
-        mScreen = new DozeScreenBrightness(mContext, mServiceFake, mSensorManager,
-                Optional.empty() /* sensor */, mDozeHost, null /* handler */,
-                mAlwaysOnDisplayPolicy, mWakefulnessLifecycle, mDozeParameters, mDockManager,
-                mUnlockedScreenOffAnimationController);
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{Optional.empty()} /* sensor */,
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -255,6 +282,130 @@
     }
 
     @Test
+    public void testSensorsSupportPostures_closed() throws Exception {
+        // GIVEN the device is CLOSED
+        when(mDevicePostureController.getDevicePosture()).thenReturn(
+                DevicePostureController.DEVICE_POSTURE_CLOSED);
+
+        // GIVEN closed and opened postures use different light sensors
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{
+                        Optional.empty() /* unknown */,
+                        Optional.of(mSensor.getSensor()) /* closed */,
+                        Optional.of(mSensorInner.getSensor()) /* half-opened */,
+                        Optional.of(mSensorInner.getSensor()) /* opened */,
+                        Optional.empty() /* flipped */
+                },
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog);
+
+        // GIVEN the device is in AOD
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN new different events are sent from the inner and outer sensors
+        mSensor.sendSensorEvent(3); // CLOSED sensor
+        mSensorInner.sendSensorEvent(4); // OPENED sensor
+
+        // THEN brightness is updated according to the sensor for CLOSED
+        assertEquals(3, mServiceFake.screenBrightness);
+    }
+
+    @Test
+    public void testSensorsSupportPostures_open() throws Exception {
+        // GIVEN the device is OPENED
+        when(mDevicePostureController.getDevicePosture()).thenReturn(
+                DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // GIVEN closed and opened postures use different light sensors
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{
+                        Optional.empty() /* unknown */,
+                        Optional.of(mSensor.getSensor()) /* closed */,
+                        Optional.of(mSensorInner.getSensor()) /* half-opened */,
+                        Optional.of(mSensorInner.getSensor()) /* opened */,
+                        Optional.empty() /* flipped */
+                },
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog);
+
+        // GIVEN device is in AOD
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN new different events are sent from the inner and outer sensors
+        mSensorInner.sendSensorEvent(4); // OPENED sensor
+        mSensor.sendSensorEvent(3); // CLOSED sensor
+
+        // THEN brightness is updated according to the sensor for OPENED
+        assertEquals(4, mServiceFake.screenBrightness);
+    }
+
+    @Test
+    public void testSensorsSupportPostures_swapPostures() throws Exception {
+        ArgumentCaptor<DevicePostureController.Callback> postureCallbackCaptor =
+                ArgumentCaptor.forClass(DevicePostureController.Callback.class);
+        reset(mDevicePostureController);
+
+        // GIVEN the device starts up AOD OPENED
+        when(mDevicePostureController.getDevicePosture()).thenReturn(
+                DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // GIVEN closed and opened postures use different light sensors
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{
+                        Optional.empty() /* unknown */,
+                        Optional.of(mSensor.getSensor()) /* closed */,
+                        Optional.of(mSensorInner.getSensor()) /* half-opened */,
+                        Optional.of(mSensorInner.getSensor()) /* opened */,
+                        Optional.empty() /* flipped */
+                },
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog);
+        verify(mDevicePostureController).addCallback(postureCallbackCaptor.capture());
+
+        // GIVEN device is in AOD
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN the posture changes to CLOSED
+        postureCallbackCaptor.getValue().onPostureChanged(
+                DevicePostureController.DEVICE_POSTURE_CLOSED);
+        waitForSensorManager();
+
+        // WHEN new different events are sent from the inner and outer sensors
+        mSensor.sendSensorEvent(3); // CLOSED sensor
+        mSensorInner.sendSensorEvent(4); // OPENED sensor
+
+        // THEN brightness is updated according to the sensor for CLOSED
+        assertEquals(3, mServiceFake.screenBrightness);
+    }
+
+    @Test
     public void testNoBrightnessDeliveredAfterFinish() throws Exception {
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -319,14 +470,13 @@
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
         when(mDozeParameters.shouldControlUnlockedScreenOff()).thenReturn(true);
-        when(mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()).thenReturn(true);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
 
         // If we're dozing after a timeout, and playing the unlocked screen animation, we should
-        // stay at or below dim brightness, because the screen dims just before timeout.
-        assertTrue(mServiceFake.screenBrightness <= DIM_BRIGHTNESS);
+        // stay at dim brightness, because the screen dims just before timeout.
+        assertEquals(mServiceFake.screenBrightness, DIM_BRIGHTNESS);
     }
 
     @Test
@@ -334,7 +484,6 @@
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
         when(mDozeParameters.shouldControlUnlockedScreenOff()).thenReturn(true);
-        when(mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()).thenReturn(true);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
@@ -349,7 +498,6 @@
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
         when(mDozeParameters.shouldControlUnlockedScreenOff()).thenReturn(false);
-        when(mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()).thenReturn(false);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStateTest.java
index 3e19cc4..150ab77 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenStateTest.java
@@ -82,8 +82,6 @@
     private UdfpsController mUdfpsController;
     @Mock
     private DozeLog mDozeLog;
-    @Mock
-    private DozeScreenBrightness mDozeScreenBrightness;
 
     @Before
     public void setUp() throws Exception {
@@ -98,8 +96,7 @@
         mHandlerFake = new FakeHandler(Looper.getMainLooper());
         mWakeLock = new WakeLockFake();
         mScreen = new DozeScreenState(mServiceFake, mHandlerFake, mDozeHost, mDozeParameters,
-                mWakeLock, mAuthController, mUdfpsControllerProvider, mDozeLog,
-                mDozeScreenBrightness);
+                mWakeLock, mAuthController, mUdfpsControllerProvider, mDozeLog);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index 5c4c27c..f525fee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -19,6 +19,7 @@
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_TAP;
 import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -47,6 +48,7 @@
 import com.android.systemui.doze.DozeSensors.TriggerSensor;
 import com.android.systemui.plugins.SensorManagerPlugin;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
 import com.android.systemui.util.sensors.ProximitySensor;
 import com.android.systemui.util.settings.FakeSettings;
@@ -58,6 +60,11 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Consumer;
 
 @RunWith(AndroidTestingRunner.class)
@@ -84,6 +91,8 @@
     @Mock
     private AuthController mAuthController;
     @Mock
+    private DevicePostureController mDevicePostureController;
+    @Mock
     private ProximitySensor mProximitySensor;
     private FakeSettings mFakeSettings = new FakeSettings();
     private SensorManagerPlugin.SensorEventListener mWakeLockScreenListener;
@@ -95,6 +104,8 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mTestableLooper = TestableLooper.get(this);
+        when(mAmbientDisplayConfiguration.tapSensorTypeMapping())
+                .thenReturn(new String[]{"tapSEnsor"});
         when(mAmbientDisplayConfiguration.getWakeLockScreenDebounce()).thenReturn(5000L);
         when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true);
         doAnswer(invocation -> {
@@ -117,14 +128,14 @@
 
         mWakeLockScreenListener.onSensorChanged(mock(SensorManagerPlugin.SensorEvent.class));
         mTestableLooper.processAllMessages();
-        verify(mCallback).onSensorPulse(eq(DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN),
+        verify(mCallback).onSensorPulse(eq(DozeLog.PULSE_REASON_SENSOR_WAKE_REACH),
                 anyFloat(), anyFloat(), eq(null));
 
         mDozeSensors.requestTemporaryDisable();
         reset(mCallback);
         mWakeLockScreenListener.onSensorChanged(mock(SensorManagerPlugin.SensorEvent.class));
         mTestableLooper.processAllMessages();
-        verify(mCallback, never()).onSensorPulse(eq(DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN),
+        verify(mCallback, never()).onSensorPulse(eq(DozeLog.PULSE_REASON_SENSOR_WAKE_REACH),
                 anyFloat(), anyFloat(), eq(null));
     }
 
@@ -157,7 +168,7 @@
         // GIVEN we only should register sensors using prox when not in low-powered mode / off
         // and the single tap sensor uses the proximity sensor
         when(mDozeParameters.getSelectivelyRegisterSensorsUsingProx()).thenReturn(true);
-        when(mDozeParameters.singleTapUsesProx()).thenReturn(true);
+        when(mDozeParameters.singleTapUsesProx(anyInt())).thenReturn(true);
         TestableDozeSensors dozeSensors = new TestableDozeSensors();
 
         // THEN on initialization, the tap sensor isn't requested
@@ -258,13 +269,120 @@
         assertTrue(triggerSensor.mRegistered);
     }
 
-    private class TestableDozeSensors extends DozeSensors {
+    @Test
+    public void testPostureStartStateClosed_registersCorrectSensor() throws Exception {
+        // GIVEN doze sensor that supports postures
+        Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
+        Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+                new Sensor[] {
+                        null /* unknown */,
+                        closedSensor,
+                        null /* half-opened */,
+                        openedSensor},
+                DevicePostureController.DEVICE_POSTURE_CLOSED);
 
+        // WHEN trigger sensor requests listening
+        triggerSensor.setListening(true);
+
+        // THEN the correct sensor is registered
+        verify(mSensorManager).requestTriggerSensor(eq(triggerSensor), eq(closedSensor));
+        verify(mSensorManager, never()).requestTriggerSensor(eq(triggerSensor), eq(openedSensor));
+    }
+
+    @Test
+    public void testPostureChange_registersCorrectSensor() throws Exception {
+        // GIVEN doze sensor that supports postures
+        Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
+        Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+                new Sensor[] {
+                        null /* unknown */,
+                        closedSensor,
+                        null /* half-opened */,
+                        openedSensor},
+                DevicePostureController.DEVICE_POSTURE_CLOSED);
+
+        // GIVEN sensor is listening
+        when(mSensorManager.requestTriggerSensor(any(), any())).thenReturn(true);
+        triggerSensor.setListening(true);
+        reset(mSensorManager);
+        assertTrue(triggerSensor.mRegistered);
+
+        // WHEN posture changes
+        boolean sensorChanged =
+                triggerSensor.setPosture(DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // THEN the correct sensor is registered
+        assertTrue(sensorChanged);
+        verify(mSensorManager).requestTriggerSensor(eq(triggerSensor), eq(openedSensor));
+        verify(mSensorManager, never()).requestTriggerSensor(eq(triggerSensor), eq(closedSensor));
+    }
+
+    @Test
+    public void testPostureChange_noSensorChange() throws Exception {
+        // GIVEN doze sensor that supports postures
+        Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
+        Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+                new Sensor[] {
+                        null /* unknown */,
+                        closedSensor,
+                        openedSensor /* half-opened uses the same sensor as opened*/,
+                        openedSensor},
+                DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
+
+        // GIVEN sensor is listening
+        when(mSensorManager.requestTriggerSensor(any(), any())).thenReturn(true);
+        triggerSensor.setListening(true);
+        reset(mSensorManager);
+
+        // WHEN posture changes
+        boolean sensorChanged =
+                triggerSensor.setPosture(DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // THEN no change in sensor
+        assertFalse(sensorChanged);
+        verify(mSensorManager, never()).requestTriggerSensor(eq(triggerSensor), any());
+    }
+
+    @Test
+    public void testFindSensor() throws Exception {
+        // GIVEN a prox sensor
+        List<Sensor> sensors = new ArrayList<>();
+        Sensor proxSensor =
+                createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY);
+        sensors.add(proxSensor);
+
+        when(mSensorManager.getSensorList(anyInt())).thenReturn(sensors);
+
+        // WHEN we try to find the prox sensor with the same type and name
+        // THEN we find the added sensor
+        assertEquals(
+                proxSensor,
+                DozeSensors.findSensor(
+                        mSensorManager,
+                        Sensor.STRING_TYPE_PROXIMITY,
+                        proxSensor.getName()));
+
+        // WHEN we try to find a prox sensor with a different name
+        // THEN no sensor is found
+        assertEquals(
+                null,
+                DozeSensors.findSensor(
+                        mSensorManager,
+                        Sensor.STRING_TYPE_PROXIMITY,
+                        "some other name"));
+    }
+
+
+    private class TestableDozeSensors extends DozeSensors {
         TestableDozeSensors() {
             super(getContext(), mSensorManager, mDozeParameters,
                     mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
-                    mProximitySensor, mFakeSettings, mAuthController);
-            for (TriggerSensor sensor : mSensors) {
+                    mProximitySensor, mFakeSettings, mAuthController,
+                    mDevicePostureController);
+            for (TriggerSensor sensor : mTriggerSensors) {
                 if (sensor instanceof PluginSensor
                         && ((PluginSensor) sensor).mPluginSensor.getType()
                         == TYPE_WAKE_LOCK_SCREEN) {
@@ -273,7 +391,7 @@
                     mSensorTap = sensor;
                 }
             }
-            mSensors = new TriggerSensor[] {mTriggerSensor, mSensorTap};
+            mTriggerSensors = new TriggerSensor[] {mTriggerSensor, mSensorTap};
         }
 
         public TriggerSensor createDozeSensor(Sensor sensor, boolean settingEnabled,
@@ -284,8 +402,44 @@
                     /* configured */ true,
                     /* pulseReason*/ 0,
                     /* reportsTouchCoordinate*/ false,
-                    requiresTouchScreen,
-                    mDozeLog);
+                    /* requiresTouchscreen */ false,
+                    /* ignoresSetting */ false,
+                    requiresTouchScreen);
         }
+
+        /**
+         * create a doze sensor that supports postures and is enabled
+         */
+        public TriggerSensor createDozeSensor(Sensor[] sensors, int posture) {
+            return new TriggerSensor(/* sensor */ sensors,
+                    /* setting name */ "test_setting",
+                    /* settingDefault */ true,
+                    /* configured */ true,
+                    /* pulseReason*/ 0,
+                    /* reportsTouchCoordinate*/ false,
+                    /* requiresTouchscreen */ false,
+                    /* ignoresSetting */ true,
+                    /* requiresProx */false,
+                    posture);
+        }
+    }
+
+    public static void setSensorType(Sensor sensor, int type, String strType) throws Exception {
+        Method setter = Sensor.class.getDeclaredMethod("setType", Integer.TYPE);
+        setter.setAccessible(true);
+        setter.invoke(sensor, type);
+        if (strType != null) {
+            Field f = sensor.getClass().getDeclaredField("mStringType");
+            f.setAccessible(true);
+            f.set(sensor, strType);
+        }
+    }
+
+    public static Sensor createSensor(int type, String strType) throws Exception {
+        Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor();
+        constr.setAccessible(true);
+        Sensor sensor = constr.newInstance();
+        setSensorType(sensor, type, strType);
+        return sensor;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 9577c7a..35dca7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -45,6 +45,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.DozeTriggers.DozingUpdateUiEvent;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.concurrency.FakeThreadFactory;
@@ -52,7 +53,8 @@
 import com.android.systemui.util.sensors.FakeProximitySensor;
 import com.android.systemui.util.sensors.FakeSensorManager;
 import com.android.systemui.util.sensors.FakeThresholdSensor;
-import com.android.systemui.util.sensors.ProximitySensor;
+import com.android.systemui.util.sensors.ProximityCheck;
+import com.android.systemui.util.sensors.ThresholdSensorEvent;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.wakelock.WakeLock;
@@ -79,13 +81,15 @@
     @Mock
     private DockManager mDockManager;
     @Mock
-    private ProximitySensor.ProximityCheck mProximityCheck;
+    private ProximityCheck mProximityCheck;
     @Mock
     private AuthController mAuthController;
     @Mock
     private UiEventLogger mUiEventLogger;
     @Mock
     private KeyguardStateController mKeyguardStateController;
+    @Mock
+    private DevicePostureController mDevicePostureController;
 
     private DozeTriggers mTriggers;
     private FakeSensorManager mSensors;
@@ -117,7 +121,8 @@
         mTriggers = new DozeTriggers(mContext, mHost, config, dozeParameters,
                 asyncSensorManager, wakeLock, mDockManager, mProximitySensor,
                 mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(),
-                mAuthController, mExecutor, mUiEventLogger, mKeyguardStateController);
+                mAuthController, mExecutor, mUiEventLogger, mKeyguardStateController,
+                mDevicePostureController);
         mTriggers.setDozeMachine(mMachine);
         waitForSensorManager();
     }
@@ -132,14 +137,14 @@
         mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE);
         clearInvocations(mMachine);
 
-        mProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 1));
+        mProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 1));
         captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
         mProximitySensor.alertListeners();
 
         verify(mMachine, never()).requestState(any());
         verify(mMachine, never()).requestPulse(anyInt());
 
-        mProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(false, 2));
+        mProximitySensor.setLastEvent(new ThresholdSensorEvent(false, 2));
         mProximitySensor.alertListeners();
         waitForSensorManager();
         captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
@@ -199,7 +204,7 @@
     public void testProximitySensorNotAvailablel() {
         mProximitySensor.setSensorAvailable(false);
         mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, 100, 100, null);
-        mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN, 100, 100,
+        mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_WAKE_REACH, 100, 100,
                 new float[]{1});
         mTriggers.onSensor(DozeLog.REASON_SENSOR_TAP, 100, 100, null);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java
new file mode 100644
index 0000000..172dcda
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+public class FeatureFlagManagerTest extends SysuiTestCase {
+    FeatureFlagManager mFeatureFlagManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mFeatureFlagManager = new FeatureFlagManager();
+    }
+
+    @Test
+    public void testIsEnabled() {
+        mFeatureFlagManager.setEnabled(1, true);
+        // Again, nothing changes.
+        assertThat(mFeatureFlagManager.isEnabled(1, false)).isFalse();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagReaderTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagReaderTest.java
index 223714c..fc6f3fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagReaderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagReaderTest.java
@@ -32,6 +32,8 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.util.wrapper.BuildInfo;
 
 import org.junit.Before;
@@ -43,7 +45,9 @@
 public class FeatureFlagReaderTest extends SysuiTestCase {
     @Mock private Resources mResources;
     @Mock private BuildInfo mBuildInfo;
+    @Mock private DumpManager mDumpManager;
     @Mock private SystemPropertiesHelper mSystemPropertiesHelper;
+    @Mock private FlagReader mFlagReader;
 
     private FeatureFlagReader mReader;
 
@@ -63,7 +67,8 @@
     private void initialize(boolean isDebuggable, boolean isOverrideable) {
         when(mBuildInfo.isDebuggable()).thenReturn(isDebuggable);
         when(mResources.getBoolean(R.bool.are_flags_overrideable)).thenReturn(isOverrideable);
-        mReader = new FeatureFlagReader(mResources, mBuildInfo, mSystemPropertiesHelper);
+        mReader = new FeatureFlagReader(
+                mResources, mBuildInfo, mDumpManager, mSystemPropertiesHelper, mFlagReader);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsTest.java
new file mode 100644
index 0000000..a850f70
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+public class FeatureFlagsTest extends SysuiTestCase {
+
+    @Mock FeatureFlagReader mFeatureFlagReader;
+
+    private FeatureFlags mFeatureFlags;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mFeatureFlags = new FeatureFlags(mFeatureFlagReader, getContext());
+    }
+
+    @Test
+    public void testAddListener() {
+        Flag<?> flag = new BooleanFlag(1);
+        mFeatureFlags.addFlag(flag);
+
+        // Assert and capture that a plugin listener was added.
+        ArgumentCaptor<FlagReader.Listener> pluginListenerCaptor =
+                ArgumentCaptor.forClass(FlagReader.Listener.class);
+        verify(mFeatureFlagReader).addListener(pluginListenerCaptor.capture());
+        FlagReader.Listener pluginListener = pluginListenerCaptor.getValue();
+
+        // Signal a change. No listeners, so no real effect.
+        pluginListener.onFlagChanged(flag.getId());
+
+        // Add a listener for the flag
+        final Flag<?>[] changedFlag = {null};
+        FeatureFlags.Listener listener = f -> changedFlag[0] = f;
+        mFeatureFlags.addFlagListener(flag, listener);
+
+        // No changes seen yet.
+        assertThat(changedFlag[0]).isNull();
+
+        // Signal a change.
+        pluginListener.onFlagChanged(flag.getId());
+
+        // Assert that the change was for the correct flag.
+        assertThat(changedFlag[0]).isEqualTo(flag);
+    }
+
+    @Test
+    public void testRemoveListener() {
+        Flag<?> flag = new BooleanFlag(1);
+        mFeatureFlags.addFlag(flag);
+
+        // Assert and capture that a plugin listener was added.
+        ArgumentCaptor<FlagReader.Listener> pluginListenerCaptor =
+                ArgumentCaptor.forClass(FlagReader.Listener.class);
+        verify(mFeatureFlagReader).addListener(pluginListenerCaptor.capture());
+        FlagReader.Listener pluginListener = pluginListenerCaptor.getValue();
+
+        // Add a listener for the flag
+        final Flag<?>[] changedFlag = {null};
+        FeatureFlags.Listener listener = f -> changedFlag[0] = f;
+        mFeatureFlags.addFlagListener(flag, listener);
+
+        // Signal a change.
+        pluginListener.onFlagChanged(flag.getId());
+
+        // Assert that the change was for the correct flag.
+        assertThat(changedFlag[0]).isEqualTo(flag);
+
+        changedFlag[0] = null;
+
+        // Now remove the listener.
+        mFeatureFlags.removeFlagListener(flag, listener);
+        // Signal a change.
+        pluginListener.onFlagChanged(flag.getId());
+        // Assert that the change was not triggered
+        assertThat(changedFlag[0]).isNull();
+
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.java
new file mode 100644
index 0000000..25c3028
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@SmallTest
+public class FlagsTest extends SysuiTestCase {
+
+    @Test
+    public void testDuplicateFlagIdCheckWorks() {
+        List<Pair<String, Flag<?>>> flags = collectFlags(DuplicateFlagContainer.class);
+        Map<Integer, List<String>> duplicates = groupDuplicateFlags(flags);
+
+        assertWithMessage(generateAssertionMessage(duplicates))
+                .that(duplicates.size()).isEqualTo(2);
+    }
+
+    @Test
+    public void testNoDuplicateFlagIds() {
+        List<Pair<String, Flag<?>>> flags = collectFlags(Flags.class);
+        Map<Integer, List<String>> duplicates = groupDuplicateFlags(flags);
+
+        assertWithMessage(generateAssertionMessage(duplicates))
+                .that(duplicates.size()).isEqualTo(0);
+    }
+
+    private String generateAssertionMessage(Map<Integer, List<String>> duplicates) {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("Duplicate flag keys found: {");
+        for (int id : duplicates.keySet()) {
+            stringBuilder
+                    .append(" ")
+                    .append(id)
+                    .append(": [")
+                    .append(String.join(", ", duplicates.get(id)))
+                    .append("]");
+        }
+        stringBuilder.append(" }");
+
+        return stringBuilder.toString();
+    }
+
+    private List<Pair<String, Flag<?>>> collectFlags(Class<?> clz) {
+        List<Pair<String, Flag<?>>> flags = new ArrayList<>();
+
+        Field[] fields = clz.getFields();
+
+        for (Field field : fields) {
+            Class<?> t = field.getType();
+            if (Flag.class.isAssignableFrom(t)) {
+                try {
+                    flags.add(Pair.create(field.getName(), (Flag<?>) field.get(null)));
+                } catch (IllegalAccessException e) {
+                    // no-op
+                }
+            }
+        }
+
+        return flags;
+    }
+
+    private Map<Integer, List<String>> groupDuplicateFlags(List<Pair<String, Flag<?>>> flags) {
+        Map<Integer, List<String>> grouping = new HashMap<>();
+
+        for (Pair<String, Flag<?>> flag : flags) {
+            grouping.putIfAbsent(flag.second.getId(), new ArrayList<>());
+            grouping.get(flag.second.getId()).add(flag.first);
+        }
+
+        Map<Integer, List<String>> result = new HashMap<>();
+        for (Integer id : grouping.keySet()) {
+            if (grouping.get(id).size() > 1) {
+                result.put(id, grouping.get(id));
+            }
+        }
+
+        return result;
+    }
+
+    private static class DuplicateFlagContainer {
+        public static final BooleanFlag A_FLAG = new BooleanFlag(0);
+        public static final BooleanFlag B_FLAG = new BooleanFlag(0);
+        public static final StringFlag C_FLAG = new StringFlag(0);
+
+        public static final BooleanFlag D_FLAG = new BooleanFlag(1);
+
+        public static final DoubleFlag E_FLAG = new DoubleFlag(3);
+        public static final DoubleFlag F_FLAG = new DoubleFlag(3);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index 509ef82..3b1c5f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -76,6 +76,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Executor;
 
 @SmallTest
@@ -106,7 +107,6 @@
     @Mock private IWindowManager mWindowManager;
     @Mock private Executor mBackgroundExecutor;
     @Mock private UiEventLogger mUiEventLogger;
-    @Mock private GlobalActionsInfoProvider mInfoProvider;
     @Mock private RingerModeTracker mRingerModeTracker;
     @Mock private RingerModeLiveData mRingerModeLiveData;
     @Mock private SysUiState mSysUiState;
@@ -154,12 +154,11 @@
                 mWindowManager,
                 mBackgroundExecutor,
                 mUiEventLogger,
-                mInfoProvider,
                 mRingerModeTracker,
                 mSysUiState,
                 mHandler,
                 mPackageManager,
-                mStatusBar,
+                Optional.of(mStatusBar),
                 mKeyguardUpdateMonitor
         );
         mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting();
@@ -174,14 +173,14 @@
     public void testShouldLogShow() {
         mGlobalActionsDialogLite.onShow(null);
         mTestableLooper.processAllMessages();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_POWER_MENU_OPEN);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_POWER_MENU_OPEN);
     }
 
     @Test
     public void testShouldLogDismiss() {
         mGlobalActionsDialogLite.onDismiss(mGlobalActionsDialogLite.mDialog);
         mTestableLooper.processAllMessages();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_POWER_MENU_CLOSE);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_POWER_MENU_CLOSE);
     }
 
     @Test
@@ -191,16 +190,16 @@
         doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
         doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
         String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
         };
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
         GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
         dialog.onBackPressed();
         mTestableLooper.processAllMessages();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_CLOSE_BACK);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_BACK);
     }
 
     @Test
@@ -210,17 +209,17 @@
         doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
         doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
         String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
         };
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
         GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
 
         GestureDetector.SimpleOnGestureListener gestureListener = spy(dialog.mGestureListener);
         gestureListener.onSingleTapConfirmed(null);
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
     }
 
     @Test
@@ -231,10 +230,10 @@
         doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
         doReturn(true).when(mStatusBar).isKeyguardShowing();
         String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
         };
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
         GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
@@ -243,7 +242,7 @@
         MotionEvent start = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
         MotionEvent end = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 500, 0);
         gestureListener.onFling(start, end, 0, 1000);
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
         verify(mStatusBar).animateExpandSettingsPanel(null);
     }
 
@@ -255,10 +254,10 @@
         doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
         doReturn(false).when(mStatusBar).isKeyguardShowing();
         String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
         };
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
         GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
@@ -267,40 +266,40 @@
         MotionEvent start = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
         MotionEvent end = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 500, 0);
         gestureListener.onFling(start, end, 0, 1000);
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
         verify(mStatusBar).animateExpandNotificationsPanel();
     }
 
     @Test
     public void testShouldLogBugreportPress() throws InterruptedException {
-        GlobalActionsDialog.BugReportAction bugReportAction =
+        GlobalActionsDialogLite.BugReportAction bugReportAction =
                 mGlobalActionsDialogLite.makeBugReportActionForTesting();
         bugReportAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_BUGREPORT_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_BUGREPORT_PRESS);
     }
 
     @Test
     public void testShouldLogBugreportLongPress() {
-        GlobalActionsDialog.BugReportAction bugReportAction =
+        GlobalActionsDialogLite.BugReportAction bugReportAction =
                 mGlobalActionsDialogLite.makeBugReportActionForTesting();
         bugReportAction.onLongPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
     }
 
     @Test
     public void testShouldLogEmergencyDialerPress() {
-        GlobalActionsDialog.EmergencyDialerAction emergencyDialerAction =
+        GlobalActionsDialogLite.EmergencyDialerAction emergencyDialerAction =
                 mGlobalActionsDialogLite.makeEmergencyDialerActionForTesting();
         emergencyDialerAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_EMERGENCY_DIALER_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_EMERGENCY_DIALER_PRESS);
     }
 
     @Test
     public void testShouldLogScreenshotPress() {
-        GlobalActionsDialog.ScreenshotAction screenshotAction =
+        GlobalActionsDialogLite.ScreenshotAction screenshotAction =
                 mGlobalActionsDialogLite.makeScreenshotActionForTesting();
         screenshotAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_SCREENSHOT_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_SCREENSHOT_PRESS);
     }
 
     @Test
@@ -309,7 +308,7 @@
                 com.android.internal.R.integer.config_navBarInteractionMode,
                 WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON);
 
-        GlobalActionsDialog.ScreenshotAction screenshotAction =
+        GlobalActionsDialogLite.ScreenshotAction screenshotAction =
                 mGlobalActionsDialogLite.makeScreenshotActionForTesting();
         assertThat(screenshotAction.shouldShow()).isTrue();
     }
@@ -320,12 +319,12 @@
                 com.android.internal.R.integer.config_navBarInteractionMode,
                 WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON);
 
-        GlobalActionsDialog.ScreenshotAction screenshotAction =
+        GlobalActionsDialogLite.ScreenshotAction screenshotAction =
                 mGlobalActionsDialogLite.makeScreenshotActionForTesting();
         assertThat(screenshotAction.shouldShow()).isFalse();
     }
 
-    private void verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent event) {
+    private void verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent event) {
         mTestableLooper.processAllMessages();
         verify(mUiEventLogger, times(1))
                 .log(event);
@@ -346,19 +345,19 @@
         doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
         doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
         String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
         };
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
         mGlobalActionsDialogLite.createActionItems();
 
         assertItemsOfType(mGlobalActionsDialogLite.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.LockDownAction.class,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
+                GlobalActionsDialogLite.EmergencyAction.class,
+                GlobalActionsDialogLite.LockDownAction.class,
+                GlobalActionsDialogLite.ShutDownAction.class,
+                GlobalActionsDialogLite.RestartAction.class);
         assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty();
         assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty();
     }
@@ -370,19 +369,19 @@
         // make sure lockdown action will NOT be shown
         doReturn(false).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
         String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
                 // lockdown action not allowed
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
         };
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
         mGlobalActionsDialogLite.createActionItems();
 
         assertItemsOfType(mGlobalActionsDialogLite.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
+                GlobalActionsDialogLite.EmergencyAction.class,
+                GlobalActionsDialogLite.ShutDownAction.class,
+                GlobalActionsDialogLite.RestartAction.class);
         assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty();
         assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty();
     }
@@ -392,7 +391,7 @@
         GlobalActionsDialogLite.LockDownAction lockDownAction =
                 mGlobalActionsDialogLite.new LockDownAction();
         lockDownAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_LOCKDOWN_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_LOCKDOWN_PRESS);
     }
 
     @Test
@@ -400,7 +399,7 @@
         GlobalActionsDialogLite.ShutDownAction shutDownAction =
                 mGlobalActionsDialogLite.new ShutDownAction();
         shutDownAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_SHUTDOWN_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_SHUTDOWN_PRESS);
     }
 
     @Test
@@ -408,7 +407,7 @@
         GlobalActionsDialogLite.ShutDownAction shutDownAction =
                 mGlobalActionsDialogLite.new ShutDownAction();
         shutDownAction.onLongPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_SHUTDOWN_LONG_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_SHUTDOWN_LONG_PRESS);
     }
 
     @Test
@@ -416,7 +415,7 @@
         GlobalActionsDialogLite.RestartAction restartAction =
                 mGlobalActionsDialogLite.new RestartAction();
         restartAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_REBOOT_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_REBOOT_PRESS);
     }
 
     @Test
@@ -424,7 +423,7 @@
         GlobalActionsDialogLite.RestartAction restartAction =
                 mGlobalActionsDialogLite.new RestartAction();
         restartAction.onLongPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_REBOOT_LONG_PRESS);
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_REBOOT_LONG_PRESS);
     }
 
     @Test
@@ -436,10 +435,10 @@
         doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
         doReturn(false).when(mStatusBar).isKeyguardShowing();
         String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
         };
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
deleted file mode 100644
index 338bb30..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
+++ /dev/null
@@ -1,575 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.globalactions;
-
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.IActivityManager;
-import android.app.admin.DevicePolicyManager;
-import android.app.trust.TrustManager;
-import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.media.AudioManager;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.UserManager;
-import android.service.dreams.IDreamManager;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.IWindowManager;
-import android.view.View;
-import android.view.WindowManagerPolicyConstants;
-import android.widget.FrameLayout;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.colorextraction.ColorExtractor;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.model.SysUiState;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.GlobalActions;
-import com.android.systemui.plugins.GlobalActionsPanelPlugin;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.telephony.TelephonyListenerManager;
-import com.android.systemui.util.RingerModeLiveData;
-import com.android.systemui.util.RingerModeTracker;
-import com.android.systemui.util.settings.GlobalSettings;
-import com.android.systemui.util.settings.SecureSettings;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.regex.Pattern;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class GlobalActionsDialogTest extends SysuiTestCase {
-    private static final long UI_TIMEOUT_MILLIS = 5000; // 5 sec
-    private static final Pattern CANCEL_BUTTON =
-            Pattern.compile("cancel", Pattern.CASE_INSENSITIVE);
-
-    private GlobalActionsDialog mGlobalActionsDialog;
-
-    @Mock private GlobalActions.GlobalActionsManager mWindowManagerFuncs;
-    @Mock private AudioManager mAudioManager;
-    @Mock private IDreamManager mDreamManager;
-    @Mock private DevicePolicyManager mDevicePolicyManager;
-    @Mock private LockPatternUtils mLockPatternUtils;
-    @Mock private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock private TelephonyListenerManager mTelephonyListenerManager;
-    @Mock private GlobalSettings mGlobalSettings;
-    @Mock private Resources mResources;
-    @Mock private ConfigurationController mConfigurationController;
-    @Mock private ActivityStarter mActivityStarter;
-    @Mock private KeyguardStateController mKeyguardStateController;
-    @Mock private UserManager mUserManager;
-    @Mock private TrustManager mTrustManager;
-    @Mock private IActivityManager mActivityManager;
-    @Mock private MetricsLogger mMetricsLogger;
-    @Mock private SysuiColorExtractor mColorExtractor;
-    @Mock private IStatusBarService mStatusBarService;
-    @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
-    @Mock private IWindowManager mWindowManager;
-    @Mock private Executor mBackgroundExecutor;
-    @Mock private UiEventLogger mUiEventLogger;
-    @Mock private RingerModeTracker mRingerModeTracker;
-    @Mock private RingerModeLiveData mRingerModeLiveData;
-    @Mock private SysUiState mSysUiState;
-    @Mock GlobalActionsPanelPlugin mWalletPlugin;
-    @Mock GlobalActionsPanelPlugin.PanelViewController mWalletController;
-    @Mock private Handler mHandler;
-    @Mock private UserTracker mUserTracker;
-    @Mock private PackageManager mPackageManager;
-    @Mock private SecureSettings mSecureSettings;
-    @Mock private StatusBar mStatusBar;
-    @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-
-    private TestableLooper mTestableLooper;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mTestableLooper = TestableLooper.get(this);
-        allowTestableLooperAsMainThread();
-
-        when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData);
-        when(mResources.getConfiguration()).thenReturn(
-                getContext().getResources().getConfiguration());
-
-        mGlobalActionsDialog = new GlobalActionsDialog(mContext,
-                mWindowManagerFuncs,
-                mAudioManager,
-                mDreamManager,
-                mDevicePolicyManager,
-                mLockPatternUtils,
-                mBroadcastDispatcher,
-                mTelephonyListenerManager,
-                mGlobalSettings,
-                mSecureSettings,
-                null,
-                mResources,
-                mConfigurationController,
-                mActivityStarter,
-                mKeyguardStateController,
-                mUserManager,
-                mTrustManager,
-                mActivityManager,
-                null,
-                mMetricsLogger,
-                mColorExtractor,
-                mStatusBarService,
-                mNotificationShadeWindowController,
-                mWindowManager,
-                mBackgroundExecutor,
-                mUiEventLogger,
-                mRingerModeTracker,
-                mSysUiState,
-                mHandler,
-                mPackageManager,
-                mStatusBar,
-                mKeyguardUpdateMonitor
-        );
-        mGlobalActionsDialog.setZeroDialogPressDelayForTesting();
-
-        ColorExtractor.GradientColors backdropColors = new ColorExtractor.GradientColors();
-        backdropColors.setMainColor(Color.BLACK);
-        when(mColorExtractor.getNeutralColors()).thenReturn(backdropColors);
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
-    }
-
-    @Test
-    public void testShouldLogShow() {
-        mGlobalActionsDialog.onShow(null);
-        mTestableLooper.processAllMessages();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_POWER_MENU_OPEN);
-    }
-
-    @Test
-    public void testShouldLogDismiss() {
-        mGlobalActionsDialog.onDismiss(mGlobalActionsDialog.mDialog);
-        mTestableLooper.processAllMessages();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_POWER_MENU_CLOSE);
-    }
-
-    @Test
-    public void testShouldLogBugreportPress() throws InterruptedException {
-        GlobalActionsDialog.BugReportAction bugReportAction =
-                mGlobalActionsDialog.makeBugReportActionForTesting();
-        bugReportAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_BUGREPORT_PRESS);
-    }
-
-    @Test
-    public void testShouldLogBugreportLongPress() {
-        GlobalActionsDialog.BugReportAction bugReportAction =
-                mGlobalActionsDialog.makeBugReportActionForTesting();
-        bugReportAction.onLongPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
-    }
-
-    @Test
-    public void testShouldLogEmergencyDialerPress() {
-        GlobalActionsDialog.EmergencyDialerAction emergencyDialerAction =
-                mGlobalActionsDialog.makeEmergencyDialerActionForTesting();
-        emergencyDialerAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_EMERGENCY_DIALER_PRESS);
-    }
-
-    @Test
-    public void testShouldLogScreenshotPress() {
-        GlobalActionsDialog.ScreenshotAction screenshotAction =
-                mGlobalActionsDialog.makeScreenshotActionForTesting();
-        screenshotAction.onPress();
-        verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent.GA_SCREENSHOT_PRESS);
-    }
-
-    @Test
-    public void testShouldShowScreenshot() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.integer.config_navBarInteractionMode,
-                WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON);
-
-        GlobalActionsDialog.ScreenshotAction screenshotAction =
-                mGlobalActionsDialog.makeScreenshotActionForTesting();
-        assertThat(screenshotAction.shouldShow()).isTrue();
-    }
-
-    @Test
-    public void testShouldNotShowScreenshot() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.integer.config_navBarInteractionMode,
-                WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON);
-
-        GlobalActionsDialog.ScreenshotAction screenshotAction =
-                mGlobalActionsDialog.makeScreenshotActionForTesting();
-        assertThat(screenshotAction.shouldShow()).isFalse();
-    }
-
-    private void verifyLogPosted(GlobalActionsDialog.GlobalActionsEvent event) {
-        mTestableLooper.processAllMessages();
-        verify(mUiEventLogger, times(1))
-                .log(event);
-    }
-
-    @SafeVarargs
-    private static <T> void assertItemsOfType(List<T> stuff, Class<? extends T>... classes) {
-        assertThat(stuff).hasSize(classes.length);
-        for (int i = 0; i < stuff.size(); i++) {
-            assertThat(stuff.get(i)).isInstanceOf(classes[i]);
-        }
-    }
-
-    @Test
-    public void testCreateActionItems_maxThree_noOverflow() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow 3 items to be shown
-        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        // ensure items are not blocked by keyguard or device provisioning
-        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
-        assertThat(mGlobalActionsDialog.mOverflowItems).isEmpty();
-        assertThat(mGlobalActionsDialog.mPowerItems).isEmpty();
-    }
-
-    @Test
-    public void testCreateActionItems_maxThree_condensePower() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow 3 items to be shown
-        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        // ensure items are not blocked by keyguard or device provisioning
-        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
-        // make sure lockdown action will be shown
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.LockDownAction.class,
-                GlobalActionsDialog.PowerOptionsAction.class);
-        assertThat(mGlobalActionsDialog.mOverflowItems).isEmpty();
-        assertItemsOfType(mGlobalActionsDialog.mPowerItems,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
-    }
-
-    @Test
-    public void testCreateActionItems_maxThree_condensePower_splitPower() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow 3 items to be shown
-        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        // make sure lockdown action will be shown
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
-        // make sure bugreport also shown
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayBugReport(any());
-        // ensure items are not blocked by keyguard or device provisioning
-        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_BUGREPORT,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.LockDownAction.class,
-                GlobalActionsDialog.PowerOptionsAction.class);
-        assertItemsOfType(mGlobalActionsDialog.mOverflowItems,
-                GlobalActionsDialog.BugReportAction.class);
-        assertItemsOfType(mGlobalActionsDialog.mPowerItems,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
-    }
-
-    @Test
-    public void testCreateActionItems_maxFour_condensePower() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow 3 items to be shown
-        doReturn(4).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        // make sure lockdown action will be shown
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
-        // ensure items are not blocked by keyguard or device provisioning
-        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_SCREENSHOT
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.LockDownAction.class,
-                GlobalActionsDialog.PowerOptionsAction.class,
-                GlobalActionsDialog.ScreenshotAction.class);
-        assertThat(mGlobalActionsDialog.mOverflowItems).isEmpty();
-        assertItemsOfType(mGlobalActionsDialog.mPowerItems,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
-    }
-
-    @Test
-    public void testCreateActionItems_maxThree_doNotCondensePower() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow 3 items to be shown
-        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        // make sure lockdown action will be shown
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
-        // make sure bugreport is also shown
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayBugReport(any());
-        // ensure items are not blocked by keyguard or device provisioning
-        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_BUGREPORT,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.BugReportAction.class);
-        assertItemsOfType(mGlobalActionsDialog.mOverflowItems,
-                GlobalActionsDialog.LockDownAction.class);
-        assertThat(mGlobalActionsDialog.mPowerItems).isEmpty();
-    }
-
-    @Test
-    public void testCreateActionItems_maxAny() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow any number of power menu items to be shown
-        doReturn(Integer.MAX_VALUE).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        // ensure items are not blocked by keyguard or device provisioning
-        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
-        // make sure lockdown action will be shown
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class,
-                GlobalActionsDialog.LockDownAction.class);
-        assertThat(mGlobalActionsDialog.mOverflowItems).isEmpty();
-        assertThat(mGlobalActionsDialog.mPowerItems).isEmpty();
-    }
-
-    @Test
-    public void testCreateActionItems_maxThree_lockdownDisabled_doesNotShowLockdown() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow only 3 items to be shown
-        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        // make sure lockdown action will NOT be shown
-        doReturn(false).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                // lockdown action not allowed
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
-        assertThat(mGlobalActionsDialog.mOverflowItems).isEmpty();
-        assertThat(mGlobalActionsDialog.mPowerItems).isEmpty();
-    }
-
-    @Test
-    public void testCreateActionItems_shouldShowAction_excludeBugReport() {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        // allow only 3 items to be shown
-        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
-        doReturn(true).when(mGlobalActionsDialog).shouldDisplayBugReport(any());
-        // exclude bugreport in shouldShowAction to demonstrate how any button can be removed
-        doAnswer(
-                invocation -> !(invocation.getArgument(0)
-                        instanceof GlobalActionsDialog.BugReportAction))
-                .when(mGlobalActionsDialog).shouldShowAction(any());
-
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                // bugreport action not allowed
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_BUGREPORT,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-        mGlobalActionsDialog.createActionItems();
-
-        assertItemsOfType(mGlobalActionsDialog.mItems,
-                GlobalActionsDialog.EmergencyAction.class,
-                GlobalActionsDialog.ShutDownAction.class,
-                GlobalActionsDialog.RestartAction.class);
-        assertThat(mGlobalActionsDialog.mOverflowItems).isEmpty();
-        assertThat(mGlobalActionsDialog.mPowerItems).isEmpty();
-    }
-
-    @Test
-    public void testShouldShowLockScreenMessage() throws RemoteException {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        mGlobalActionsDialog.mDialog = null;
-        when(mKeyguardStateController.isUnlocked()).thenReturn(false);
-        when(mActivityManager.getCurrentUser()).thenReturn(newUserInfo());
-        when(mLockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED);
-        mGlobalActionsDialog.mShowLockScreenCards = false;
-        setupDefaultActions();
-        when(mWalletPlugin.onPanelShown(any(), anyBoolean())).thenReturn(mWalletController);
-        when(mWalletController.getPanelContent()).thenReturn(new FrameLayout(mContext));
-
-        mGlobalActionsDialog.showOrHideDialog(false, true, mWalletPlugin);
-
-        GlobalActionsDialog.ActionsDialog dialog =
-                (GlobalActionsDialog.ActionsDialog) mGlobalActionsDialog.mDialog;
-        assertThat(dialog).isNotNull();
-        assertThat(dialog.mLockMessageContainer.getVisibility()).isEqualTo(View.VISIBLE);
-
-        // Dismiss the dialog so that it does not pollute other tests
-        mGlobalActionsDialog.showOrHideDialog(false, true, mWalletPlugin);
-    }
-
-    @Test
-    public void testShouldNotShowLockScreenMessage_whenWalletShownOnLockScreen()
-            throws RemoteException {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        mGlobalActionsDialog.mDialog = null;
-        when(mKeyguardStateController.isUnlocked()).thenReturn(false);
-        when(mActivityManager.getCurrentUser()).thenReturn(newUserInfo());
-        when(mLockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED);
-        mGlobalActionsDialog.mShowLockScreenCards = true;
-        setupDefaultActions();
-        when(mWalletPlugin.onPanelShown(any(), anyBoolean())).thenReturn(mWalletController);
-        when(mWalletController.getPanelContent()).thenReturn(new FrameLayout(mContext));
-
-        mGlobalActionsDialog.showOrHideDialog(false, true, mWalletPlugin);
-
-        GlobalActionsDialog.ActionsDialog dialog =
-                (GlobalActionsDialog.ActionsDialog) mGlobalActionsDialog.mDialog;
-        assertThat(dialog).isNotNull();
-        assertThat(dialog.mLockMessageContainer.getVisibility()).isEqualTo(View.GONE);
-
-        // Dismiss the dialog so that it does not pollute other tests
-        mGlobalActionsDialog.showOrHideDialog(false, true, mWalletPlugin);
-    }
-
-    @Test
-    public void testShouldNotShowLockScreenMessage_whenWalletBothDisabled()
-            throws RemoteException {
-        mGlobalActionsDialog = spy(mGlobalActionsDialog);
-        mGlobalActionsDialog.mDialog = null;
-        when(mKeyguardStateController.isUnlocked()).thenReturn(false);
-
-        when(mActivityManager.getCurrentUser()).thenReturn(newUserInfo());
-        when(mLockPatternUtils.getStrongAuthForUser(anyInt())).thenReturn(STRONG_AUTH_NOT_REQUIRED);
-        mGlobalActionsDialog.mShowLockScreenCards = true;
-        setupDefaultActions();
-        when(mWalletPlugin.onPanelShown(any(), anyBoolean())).thenReturn(mWalletController);
-        when(mWalletController.getPanelContent()).thenReturn(null);
-
-        mGlobalActionsDialog.showOrHideDialog(false, true, mWalletPlugin);
-
-        GlobalActionsDialog.ActionsDialog dialog =
-                (GlobalActionsDialog.ActionsDialog) mGlobalActionsDialog.mDialog;
-        assertThat(dialog).isNotNull();
-        assertThat(dialog.mLockMessageContainer.getVisibility()).isEqualTo(View.GONE);
-
-        // Dismiss the dialog so that it does not pollute other tests
-        mGlobalActionsDialog.showOrHideDialog(false, true, mWalletPlugin);
-    }
-
-    private UserInfo newUserInfo() {
-        return new UserInfo(0, null, null, UserInfo.FLAG_PRIMARY, null);
-    }
-
-    private void setupDefaultActions() {
-        String[] actions = {
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-        };
-        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsInfoProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsInfoProviderTest.kt
deleted file mode 100644
index 302a8d3..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsInfoProviderTest.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.android.systemui.globalactions
-
-import android.content.Context
-import android.content.SharedPreferences
-import android.content.res.Configuration
-import android.content.res.Resources
-import android.service.quickaccesswallet.QuickAccessWalletClient
-import android.testing.AndroidTestingRunner
-import android.view.ViewGroup
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.controls.controller.ControlsController
-import com.android.systemui.globalactions.GlobalActionsInfoProvider
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.SysuiTestCase
-import junit.framework.Assert.assertFalse
-import junit.framework.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyObject
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import org.mockito.Mockito.`when` as whenever
-
-private const val PREFERENCE = "global_actions_info_prefs"
-private const val KEY_VIEW_COUNT = "view_count"
-
-private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class GlobalActionsInfoProviderTest : SysuiTestCase() {
-
-    @Mock private lateinit var walletClient: QuickAccessWalletClient
-    @Mock private lateinit var controlsController: ControlsController
-    @Mock private lateinit var activityStarter: ActivityStarter
-    @Mock private lateinit var mockContext: Context
-    @Mock private lateinit var mockResources: Resources
-    @Mock private lateinit var sharedPrefs: SharedPreferences
-    @Mock private lateinit var sharedPrefsEditor: SharedPreferences.Editor
-
-    private lateinit var infoProvider: GlobalActionsInfoProvider
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        mockContext = spy(context)
-        mockResources = spy(context.resources)
-        whenever(mockContext.resources).thenReturn(mockResources)
-        whenever(mockResources.getBoolean(R.bool.global_actions_show_change_info))
-                .thenReturn(true)
-        whenever(mockContext.getSharedPreferences(eq(PREFERENCE), anyInt()))
-                .thenReturn(sharedPrefs)
-        whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
-        whenever(sharedPrefsEditor.putInt(anyString(), anyInt())).thenReturn(sharedPrefsEditor)
-        whenever(sharedPrefsEditor.putBoolean(anyString(), anyBoolean()))
-                .thenReturn(sharedPrefsEditor)
-
-        infoProvider = GlobalActionsInfoProvider(
-                mockContext,
-                walletClient,
-                controlsController,
-                activityStarter
-        )
-    }
-
-    @Test
-    fun testIsEligible_noCards() {
-        whenever(sharedPrefs.contains(eq(KEY_VIEW_COUNT))).thenReturn(false)
-        whenever(walletClient.isWalletFeatureAvailable).thenReturn(false)
-
-        assertFalse(infoProvider.shouldShowMessage())
-    }
-
-    @Test
-    fun testIsEligible_hasCards() {
-        whenever(sharedPrefs.contains(eq(KEY_VIEW_COUNT))).thenReturn(false)
-        whenever(walletClient.isWalletFeatureAvailable).thenReturn(true)
-
-        assertTrue(infoProvider.shouldShowMessage())
-    }
-
-    @Test
-    fun testNotEligible_shouldNotShow() {
-        whenever(mockResources.getBoolean(R.bool.global_actions_show_change_info))
-                .thenReturn(false)
-
-        assertFalse(infoProvider.shouldShowMessage())
-    }
-
-    @Test
-    fun testTooManyButtons_doesNotAdd() {
-        val configuration = Configuration()
-        configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
-        whenever(mockResources.configuration).thenReturn(configuration)
-
-        val parent = mock(ViewGroup::class.java)
-        infoProvider.addPanel(mockContext, parent, 5, { })
-
-        verify(parent, never()).addView(anyObject(), anyInt())
-    }
-
-    @Test
-    fun testLimitTimesShown() {
-        whenever(sharedPrefs.getInt(eq(KEY_VIEW_COUNT), anyInt())).thenReturn(4)
-
-        assertFalse(infoProvider.shouldShowMessage())
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 31d70f5..1bb660e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -34,12 +34,14 @@
 import android.app.trust.TrustManager;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
+import android.os.RemoteException;
 import android.telephony.TelephonyManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.policy.IKeyguardDrawnCallback;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardDisplayManager;
 import com.android.keyguard.KeyguardUpdateMonitor;
@@ -55,6 +57,8 @@
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
+import com.android.systemui.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.DeviceConfigProxyFake;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -63,6 +67,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -85,11 +90,14 @@
     private @Mock NavigationModeController mNavigationModeController;
     private @Mock KeyguardDisplayManager mKeyguardDisplayManager;
     private @Mock DozeParameters mDozeParameters;
+    private @Mock UnfoldTransitionConfig mUnfoldTransitionConfig;
+    private @Mock UnfoldLightRevealOverlayAnimation mUnfoldAnimation;
     private @Mock SysuiStatusBarStateController mStatusBarStateController;
     private @Mock KeyguardStateController mKeyguardStateController;
     private @Mock NotificationShadeDepthController mNotificationShadeDepthController;
     private @Mock KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private @Mock UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    private @Mock IKeyguardDrawnCallback mKeyguardDrawnCallback;
     private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake();
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
 
@@ -120,6 +128,8 @@
                 mNavigationModeController,
                 mKeyguardDisplayManager,
                 mDozeParameters,
+                mUnfoldTransitionConfig,
+                () -> mUnfoldAnimation,
                 mStatusBarStateController,
                 mKeyguardStateController,
                 () -> mKeyguardUnlockAnimationController,
@@ -148,6 +158,33 @@
     }
 
     @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void testUnfoldTransitionEnabledDrawnTasksReady_onScreenTurningOn_callsDrawnCallback()
+            throws RemoteException {
+        when(mUnfoldTransitionConfig.isEnabled()).thenReturn(true);
+
+        mViewMediator.onScreenTurningOn(mKeyguardDrawnCallback);
+        TestableLooper.get(this).processAllMessages();
+        onUnfoldOverlayReady();
+
+        // Should be called when both unfold overlay and keyguard drawn ready
+        verify(mKeyguardDrawnCallback).onDrawn();
+    }
+
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_callsDrawnCallback()
+            throws RemoteException {
+        when(mUnfoldTransitionConfig.isEnabled()).thenReturn(false);
+
+        mViewMediator.onScreenTurningOn(mKeyguardDrawnCallback);
+        TestableLooper.get(this).processAllMessages();
+
+        // Should be called when only keyguard drawn
+        verify(mKeyguardDrawnCallback).onDrawn();
+    }
+
+    @Test
     public void testIsAnimatingScreenOff() {
         when(mDozeParameters.shouldControlUnlockedScreenOff()).thenReturn(true);
 
@@ -187,4 +224,11 @@
         // then make sure it comes back
         verify(mStatusBarKeyguardViewManager, atLeast(1)).show(null);
     }
+
+    private void onUnfoldOverlayReady() {
+        ArgumentCaptor<Runnable> overlayReadyCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mUnfoldAnimation).onScreenTurningOn(overlayReadyCaptor.capture());
+        overlayReadyCaptor.getValue().run();
+        TestableLooper.get(this).processAllMessages();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
index 9356c54..c5436ef6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
@@ -16,37 +16,50 @@
 
 package com.android.systemui.keyguard;
 
+import static com.android.keyguard.LockIconView.ICON_LOCK;
+import static com.android.keyguard.LockIconView.ICON_UNLOCK;
+
 import static junit.framework.Assert.assertEquals;
 
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.PointF;
+import android.graphics.drawable.AnimatedStateListDrawable;
+import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Vibrator;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.util.DisplayMetrics;
 import android.util.Pair;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.KeyguardViewController;
 import com.android.keyguard.LockIconView;
 import com.android.keyguard.LockIconViewController;
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.AuthRippleController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -68,7 +81,10 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class LockIconViewControllerTest extends SysuiTestCase {
+    private static final String UNLOCKED_LABEL = "unlocked";
+
     private @Mock LockIconView mLockIconView;
+    private @Mock AnimatedStateListDrawable mIconDrawable;
     private @Mock Context mContext;
     private @Mock Resources mResources;
     private @Mock DisplayMetrics mDisplayMetrics;
@@ -85,6 +101,7 @@
     private @Mock Vibrator mVibrator;
     private @Mock AuthRippleController mAuthRippleController;
     private @Mock LottieAnimationView mAodFp;
+    private @Mock LayoutInflater mLayoutInflater;
 
     private LockIconViewController mLockIconViewController;
 
@@ -93,9 +110,22 @@
             ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
     private View.OnAttachStateChangeListener mAttachListener;
 
+    @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
+            ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+    private KeyguardStateController.Callback mKeyguardStateCallback;
+
+    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
+            ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+    private StatusBarStateController.StateListener mStatusBarStateListener;
+
     @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
     private AuthController.Callback mAuthControllerCallback;
 
+    @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback>
+            mKeyguardUpdateMonitorCallbackCaptor =
+            ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+    private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
+
     @Captor private ArgumentCaptor<PointF> mPointCaptor;
 
     @Before
@@ -104,9 +134,16 @@
 
         when(mLockIconView.getResources()).thenReturn(mResources);
         when(mLockIconView.getContext()).thenReturn(mContext);
+        when(mLockIconView.findViewById(R.layout.udfps_aod_lock_icon)).thenReturn(mAodFp);
         when(mContext.getResources()).thenReturn(mResources);
         when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
-        when(mLockIconView.findViewById(anyInt())).thenReturn(mAodFp);
+        when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
+        when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
+
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
 
         mLockIconViewController = new LockIconViewController(
                 mLockIconView,
@@ -121,11 +158,42 @@
                 mConfigurationController,
                 mDelayableExecutor,
                 mVibrator,
-                mAuthRippleController
+                mAuthRippleController,
+                mResources,
+                mLayoutInflater
         );
     }
 
     @Test
+    public void testIgnoreUdfpsWhenNotSupported() {
+        // GIVEN Udpfs sensor is NOT available
+        mLockIconViewController.init();
+        captureAttachListener();
+
+        // WHEN the view is attached
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+
+        // THEN lottie animation should NOT be inflated
+        verify(mLayoutInflater, never()).inflate(eq(R.layout.udfps_aod_lock_icon), any());
+    }
+
+    @Test
+    public void testInflateUdfpsWhenSupported() {
+        // GIVEN Udpfs sensor is available
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+        mLockIconViewController.init();
+        captureAttachListener();
+
+        // WHEN the view is attached
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+
+        // THEN lottie animation should be inflated
+        verify(mLayoutInflater).inflate(eq(R.layout.udfps_aod_lock_icon), any());
+    }
+
+    @Test
     public void testUpdateFingerprintLocationOnInit() {
         // GIVEN fp sensor location is available pre-attached
         Pair<Integer, PointF> udfps = setupUdfps();
@@ -191,6 +259,109 @@
         verify(mLockIconView).setUseBackground(false);
     }
 
+    @Test
+    public void testUnlockIconShows_biometricUnlockedTrue() {
+        // GIVEN UDFPS sensor location is available
+        setupUdfps();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+        captureKeyguardUpdateMonitorCallback();
+
+        // GIVEN user has unlocked with a biometric auth (ie: face auth)
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
+        reset(mLockIconView);
+
+        // WHEN face auth's biometric running state changes
+        mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
+                BiometricSourceType.FACE);
+
+        // THEN the unlock icon is shown
+        verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
+    }
+
+    @Test
+    public void testLockIconStartState() {
+        // GIVEN lock icon state
+        setupShowLockIcon();
+
+        // WHEN lock icon controller is initialized
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+
+        // THEN the lock icon should show
+        verify(mLockIconView).updateIcon(ICON_LOCK, false);
+    }
+
+    @Test
+    public void testLockIcon_updateToUnlock() {
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+        captureKeyguardStateCallback();
+        reset(mLockIconView);
+
+        // WHEN the unlocked state changes to canDismissLockScreen=true
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+        mKeyguardStateCallback.onUnlockedChanged();
+
+        // THEN the unlock should show
+        verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
+    }
+
+    @Test
+    public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
+        // GIVEN udfps not enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
+
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN the dozing state changes
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+        // THEN the icon is cleared
+        verify(mLockIconView).clearIcon();
+    }
+
+    @Test
+    public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
+        // GIVEN udfps enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN the dozing state changes
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+        // THEN the AOD lock icon should show
+        verify(mLockIconView).updateIcon(ICON_LOCK, true);
+    }
+
     private Pair<Integer, PointF> setupUdfps() {
         final PointF udfpsLocation = new PointF(50, 75);
         final int radius = 33;
@@ -202,13 +373,23 @@
                         /* component info */ new ArrayList<>(),
                         /* sensorType */ 3,
                         /* resetLockoutRequiresHwToken */ false,
-                        (int) udfpsLocation.x, (int) udfpsLocation.y, radius);
+                        List.of(new SensorLocationInternal("" /* displayId */,
+                                (int) udfpsLocation.x, (int) udfpsLocation.y, radius)));
         when(mAuthController.getUdfpsSensorLocation()).thenReturn(udfpsLocation);
         when(mAuthController.getUdfpsProps()).thenReturn(List.of(fpProps));
 
         return new Pair(radius, udfpsLocation);
     }
 
+    private void setupShowLockIcon() {
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
+        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+    }
+
     private void captureAuthControllerCallback() {
         verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
         mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
@@ -218,4 +399,20 @@
         verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture());
         mAttachListener = mAttachCaptor.getValue();
     }
+
+    private void captureKeyguardStateCallback() {
+        verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
+        mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
+    }
+
+    private void captureStatusBarStateListener() {
+        verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
+        mStatusBarStateListener = mStatusBarStateCaptor.getValue();
+    }
+
+    private void captureKeyguardUpdateMonitorCallback() {
+        verify(mKeyguardUpdateMonitor).registerCallback(
+                mKeyguardUpdateMonitorCallbackCaptor.capture());
+        mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java
index 06e597e..2d1b258 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java
@@ -26,6 +26,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,7 +44,7 @@
 
     @Before
     public void setUp() throws Exception {
-        mScreen = new ScreenLifecycle();
+        mScreen = new ScreenLifecycle(mock(DumpManager.class));
         mScreenObserverMock = mock(ScreenLifecycle.Observer.class);
         mScreen.addObserver(mScreenObserverMock);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
index 910b381..e453ff2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
@@ -29,6 +29,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -49,7 +50,8 @@
     @Before
     public void setUp() throws Exception {
         mWallpaperManager = mock(IWallpaperManager.class);
-        mWakefulness = new WakefulnessLifecycle(mContext, mWallpaperManager);
+        mWakefulness =
+                new WakefulnessLifecycle(mContext, mWallpaperManager, mock(DumpManager.class));
         mWakefulnessObserver = mock(WakefulnessLifecycle.Observer.class);
         mWakefulness.addObserver(mWakefulnessObserver);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
index 25ae67b..8cc2776 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
@@ -16,13 +16,12 @@
 
 package com.android.systemui.media
 
+import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
-import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.FeatureFlags
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -52,8 +51,7 @@
     private lateinit var statusBarStateController: SysuiStatusBarStateController
     @Mock
     private lateinit var configurationController: ConfigurationController
-    @Mock
-    private lateinit var featureFlags: FeatureFlags
+
     @Mock
     private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
     @JvmField @Rule
@@ -71,17 +69,17 @@
         whenever(notificationLockscreenUserManager.shouldShowLockscreenNotifications())
                 .thenReturn(true)
         whenever(mediaHost.hostView).thenReturn(hostView)
-
+        hostView.layoutParams = FrameLayout.LayoutParams(100, 100)
         keyguardMediaController = KeyguardMediaController(
             mediaHost,
             bypassController,
             statusBarStateController,
             notificationLockscreenUserManager,
-            featureFlags,
             context,
             configurationController
         )
         keyguardMediaController.attachSinglePaneContainer(mediaHeaderView)
+        keyguardMediaController.useSplitShade = false
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
new file mode 100644
index 0000000..175ec87f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import javax.inject.Provider
+
+private val DATA = MediaData(
+    userId = -1,
+    initialized = false,
+    backgroundColor = 0,
+    app = null,
+    appIcon = null,
+    artist = null,
+    song = null,
+    artwork = null,
+    actions = emptyList(),
+    actionsToShowInCompact = emptyList(),
+    packageName = "INVALID",
+    token = null,
+    clickIntent = null,
+    device = null,
+    active = true,
+    resumeAction = null)
+
+private val SMARTSPACE_KEY = "smartspace"
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaCarouselControllerTest : SysuiTestCase() {
+
+    @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
+    @Mock lateinit var panel: MediaControlPanel
+    @Mock lateinit var visualStabilityManager: VisualStabilityManager
+    @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
+    @Mock lateinit var activityStarter: ActivityStarter
+    @Mock @Main private lateinit var executor: DelayableExecutor
+    @Mock lateinit var mediaDataManager: MediaDataManager
+    @Mock lateinit var configurationController: ConfigurationController
+    @Mock lateinit var falsingCollector: FalsingCollector
+    @Mock lateinit var falsingManager: FalsingManager
+    @Mock lateinit var dumpManager: DumpManager
+
+    private val clock = FakeSystemClock()
+    private lateinit var mediaCarouselController: MediaCarouselController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        mediaCarouselController = MediaCarouselController(
+            context,
+            mediaControlPanelFactory,
+            visualStabilityManager,
+            mediaHostStatesManager,
+            activityStarter,
+            clock,
+            executor,
+            mediaDataManager,
+            configurationController,
+            falsingCollector,
+            falsingManager,
+            dumpManager
+        )
+
+        MediaPlayerData.clear()
+    }
+
+    @Test
+    fun testPlayerOrdering() {
+        // Test values: key, data, last active time
+        val playingLocal = Triple("playing local",
+            DATA.copy(active = true, isPlaying = true, isLocalSession = true, resumption = false),
+            4500L)
+
+        val playingRemote = Triple("playing remote",
+            DATA.copy(active = true, isPlaying = true, isLocalSession = false, resumption = false),
+            5000L)
+
+        val pausedLocal = Triple("paused local",
+            DATA.copy(active = true, isPlaying = false, isLocalSession = true, resumption = false),
+            1000L)
+
+        val pausedRemote = Triple("paused remote",
+            DATA.copy(active = true, isPlaying = false, isLocalSession = false, resumption = false),
+            2000L)
+
+        val resume1 = Triple("resume 1",
+            DATA.copy(active = false, isPlaying = false, isLocalSession = true, resumption = true),
+            500L)
+
+        val resume2 = Triple("resume 2",
+            DATA.copy(active = false, isPlaying = false, isLocalSession = true, resumption = true),
+            1000L)
+
+        // Expected ordering for media players:
+        // Actively playing local sessions
+        // Actively playing remote sessions
+        // Paused sessions, by last active
+        // Resume controls, by last active
+
+        val expected = listOf(playingLocal, playingRemote, pausedRemote, pausedLocal, resume2,
+            resume1)
+
+        expected.forEach {
+            clock.setCurrentTimeMillis(it.third)
+            MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first),
+                panel, clock)
+        }
+
+        for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
+            assertEquals(expected.get(index).first, key.data.notificationKey)
+        }
+    }
+
+    @Test
+    fun testOrderWithSmartspace_prioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is prioritized
+        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
+            true, clock)
+
+        // Then it should be shown immediately after any actively playing controls
+        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
+    }
+
+    @Test
+    fun testOrderWithSmartspace_notPrioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is not prioritized
+        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
+            false, clock)
+
+        // Then it should be shown at the end of the carousel
+        val size = MediaPlayerData.playerKeys().size
+        assertTrue(MediaPlayerData.playerKeys().elementAt(size - 1).isSsMediaRec)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 42629f5..bf5a6e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.dialog.MediaOutputDialogFactory
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -95,6 +96,7 @@
     @Mock private lateinit var collapsedSet: ConstraintSet
     @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
     @Mock private lateinit var mediaCarouselController: MediaCarouselController
+    @Mock private lateinit var falsingManager: FalsingManager
     private lateinit var appIcon: ImageView
     private lateinit var albumView: ImageView
     private lateinit var titleText: TextView
@@ -131,8 +133,8 @@
         whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
 
         player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
-                seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
-                mediaOutputDialogFactory, mediaCarouselController)
+            seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
+            mediaOutputDialogFactory, mediaCarouselController, falsingManager)
         whenever(seekBarViewModel.progress).thenReturn(seekBarData)
 
         // Mock out a view holder for the player to attach to.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
index 28aed20..a435e79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
@@ -86,6 +86,7 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        MediaPlayerData.clear()
         mediaDataFilter = MediaDataFilter(context, broadcastDispatcher, mediaResumeListener,
                 lockscreenUserManager, executor, clock)
         mediaDataFilter.mediaDataManager = mediaDataManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index 47c5545..2b2fc51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -255,6 +255,7 @@
             .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
                     eq(false))
         assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
     }
 
     @Test
@@ -405,6 +406,26 @@
     }
 
     @Test
+    fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() {
+        val recommendationExtras = Bundle().apply {
+            putString("package_name", PACKAGE_NAME)
+            putParcelable("dismiss_intent", null)
+        }
+        whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
+        whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
+        whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+
+        verify(listener).onSmartspaceMediaDataLoaded(
+            eq(KEY_MEDIA_SMARTSPACE),
+            eq(EMPTY_SMARTSPACE_MEDIA_DATA
+                .copy(targetId = KEY_MEDIA_SMARTSPACE, isActive = true,
+                    isValid = false, dismissIntent = null)),
+            eq(false))
+    }
+
+    @Test
     fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
         smartspaceMediaDataProvider.onTargetsAvailable(listOf())
         verify(listener, never())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
index e9e965e..8dc9eff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
@@ -127,7 +127,7 @@
         val players = MediaPlayerData.players()
         assertThat(players).hasSize(6)
         assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote,
-            playerIsStoppedAndLocal, playerCanResume, playerIsStoppedAndRemote,
+            playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerCanResume,
             playerUndetermined).inOrder()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
index 7d8728e..e77802f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
@@ -76,6 +76,7 @@
         assertThat(seekBarView.getThumb().getAlpha()).isEqualTo(0)
         assertThat(elapsedTimeView.getText()).isEqualTo("")
         assertThat(totalTimeView.getText()).isEqualTo("")
+        assertThat(seekBarView.contentDescription).isEqualTo("")
         assertThat(seekBarView.maxHeight).isEqualTo(disabledHeight)
     }
 
@@ -102,6 +103,9 @@
         assertThat(seekBarView.max).isEqualTo(120000)
         assertThat(elapsedTimeView.getText()).isEqualTo("00:03")
         assertThat(totalTimeView.getText()).isEqualTo("02:00")
+
+        val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
+        assertThat(seekBarView.contentDescription).isEqualTo(desc)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 2c68661..750600ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -54,6 +54,7 @@
 
     // Mock
     private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
+    private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class);
     private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
     private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
     private Icon mIcon = mock(Icon.class);
@@ -65,7 +66,7 @@
 
     @Before
     public void setUp() {
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
 
@@ -118,7 +119,6 @@
         assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
@@ -180,7 +180,6 @@
         when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(mMediaDevices);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
@@ -189,7 +188,6 @@
         when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(new ArrayList<>());
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
     }
 
@@ -197,7 +195,6 @@
     public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
@@ -214,7 +211,6 @@
         when(mMediaDevice2.isConnected()).thenReturn(false);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
@@ -230,7 +226,6 @@
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 9bd07b8..053851e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -39,6 +39,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.ShadeController;
@@ -63,6 +64,7 @@
     private NotificationEntryManager mNotificationEntryManager =
             mock(NotificationEntryManager.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
+    private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
 
     private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
     private MediaOutputController mMediaOutputController;
@@ -75,7 +77,7 @@
     public void setUp() {
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
         mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
                 mMediaOutputController);
         mMediaOutputBaseDialogImpl.onCreate(new Bundle());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index d1a617b..f7e60ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -49,6 +49,7 @@
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -91,6 +92,7 @@
     private NotificationEntryManager mNotificationEntryManager =
             mock(NotificationEntryManager.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
+    private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
 
     private Context mSpyContext;
     private MediaOutputController mMediaOutputController;
@@ -113,7 +115,7 @@
 
         mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
         mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -157,7 +159,7 @@
     public void start_withoutPackageName_verifyMediaControllerInit() {
         mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
 
         mMediaOutputController.start(mCb);
 
@@ -178,7 +180,7 @@
     public void stop_withoutPackageName_verifyMediaControllerDeinit() {
         mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
 
         mMediaOutputController.start(mCb);
 
@@ -449,7 +451,7 @@
     public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
         mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
 
         assertThat(mMediaOutputController.getNotificationIcon()).isNull();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 86f6bde..8a3ea56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -36,6 +36,7 @@
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.ShadeController;
@@ -65,6 +66,7 @@
     private final NotificationEntryManager mNotificationEntryManager =
             mock(NotificationEntryManager.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
+    private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
 
     private MediaOutputDialog mMediaOutputDialog;
     private MediaOutputController mMediaOutputController;
@@ -74,10 +76,11 @@
     public void setUp() {
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputDialog = new MediaOutputDialog(mContext, false,
                 mMediaOutputController, mUiEventLogger);
+        mMediaOutputDialog.show();
 
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
         when(mMediaDevice.getFeatures()).thenReturn(mFeatures);
@@ -123,6 +126,7 @@
     public void onCreate_ShouldLogVisibility() {
         MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false,
                 mMediaOutputController, mUiEventLogger);
+        testDialog.show();
 
         testDialog.dismissDialog();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
index 1f85112..ca5d570 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
@@ -99,7 +99,6 @@
         assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
@@ -114,7 +113,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
@@ -141,7 +139,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
@@ -167,7 +164,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
@@ -186,7 +182,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
index c296ff5..e8cd6c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
@@ -34,6 +34,7 @@
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.ShadeController;
@@ -64,6 +65,7 @@
     private NotificationEntryManager mNotificationEntryManager =
             mock(NotificationEntryManager.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
+    private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
 
     private MediaOutputGroupDialog mMediaOutputGroupDialog;
     private MediaOutputController mMediaOutputController;
@@ -73,10 +75,11 @@
     public void setUp() {
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
                 mMediaOutputController);
+        mMediaOutputGroupDialog.show();
         when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
new file mode 100644
index 0000000..bc86ef9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.monet;
+
+import android.app.WallpaperColors;
+import android.graphics.Color;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ColorSchemeTest extends SysuiTestCase {
+    @Test
+    public void testFilterTransparency() {
+        ColorScheme colorScheme = new ColorScheme(Color.TRANSPARENT, false /* darkTheme */);
+        Assert.assertEquals(colorScheme.getAllAccentColors(),
+                new ColorScheme(0xFF1b6ef3, false).getAllAccentColors());
+    }
+
+    @Test
+    public void testDontFilterOpaque() {
+        ColorScheme colorScheme = new ColorScheme(0xFFFF0000, false /* darkTheme */);
+        Assert.assertNotEquals(colorScheme.getAllAccentColors(),
+                new ColorScheme(0xFF1b6ef3, false).getAllAccentColors());
+    }
+
+    @Test
+    public void testUniqueColors() {
+        WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xffaec00a),
+                Color.valueOf(0xffaec00a), Color.valueOf(0xffaec00a));
+
+        List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
+        Assert.assertEquals(rankedSeedColors, List.of(0xffaec00a));
+    }
+
+
+    @Test
+    public void testFiltersInvalidColors() {
+        WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xff5e7ea2),
+                Color.valueOf(0xff5e7ea2), Color.valueOf(0xff000000));
+
+        List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
+        Assert.assertEquals(rankedSeedColors, List.of(0xff5e7ea2));
+    }
+
+    @Test
+    public void testInvalidColorBecomesGBlue() {
+        WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xff000000), null,
+                null);
+
+        List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
+        Assert.assertEquals(rankedSeedColors, List.of(0xFF1b6ef3));
+    }
+
+    @Test
+    public void testDontFilterRRGGBB() {
+        ColorScheme colorScheme = new ColorScheme(0xFF0000, false /* darkTheme */);
+        Assert.assertEquals(colorScheme.getAllAccentColors(),
+                new ColorScheme(0xFFFF0000, false).getAllAccentColors());
+    }
+
+    @Test
+    public void testNoPopulationSignal() {
+        WallpaperColors wallpaperColors = new WallpaperColors(Color.valueOf(0xffaec00a),
+                Color.valueOf(0xffbe0000), Color.valueOf(0xffcc040f));
+
+        List<Integer> rankedSeedColors = ColorScheme.getSeedColors(wallpaperColors);
+        Assert.assertEquals(rankedSeedColors, List.of(0xffaec00a, 0xffbe0000, 0xffcc040f));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
index d2527c6..4fc329f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
@@ -35,41 +35,24 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.SparseArray;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
-import com.android.systemui.accessibility.SystemActions;
-import com.android.systemui.assist.AssistManager;
-import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
-import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
-import com.android.wm.shell.pip.Pip;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-
-import java.util.Optional;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 /** atest NavigationBarControllerTest */
 @RunWith(AndroidTestingRunner.class)
@@ -77,42 +60,33 @@
 @SmallTest
 public class NavigationBarControllerTest extends SysuiTestCase {
 
+    private static final int SECONDARY_DISPLAY = 1;
+
     private NavigationBarController mNavigationBarController;
     private NavigationBar mDefaultNavBar;
     private NavigationBar mSecondaryNavBar;
 
-    private static final int SECONDARY_DISPLAY = 1;
+    @Mock
+    private CommandQueue mCommandQueue;
+    @Mock
+    private NavigationBar.Factory mNavigationBarFactory;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         mNavigationBarController = spy(
                 new NavigationBarController(mContext,
-                        mock(WindowManager.class),
-                        () -> mock(AssistManager.class),
-                        mock(AccessibilityManager.class),
-                        mock(AccessibilityManagerWrapper.class),
-                        mock(DeviceProvisionedController.class),
-                        mock(MetricsLogger.class),
                         mock(OverviewProxyService.class),
                         mock(NavigationModeController.class),
-                        mock(AccessibilityButtonModeObserver.class),
-                        mock(StatusBarStateController.class),
                         mock(SysUiState.class),
-                        mock(BroadcastDispatcher.class),
-                        mock(CommandQueue.class),
-                        Optional.of(mock(Pip.class)),
-                        Optional.of(mock(LegacySplitScreen.class)),
-                        Optional.of(mock(Recents.class)),
-                        () -> mock(StatusBar.class),
-                        mock(ShadeController.class),
-                        mock(NotificationRemoteInputManager.class),
-                        mock(NotificationShadeDepthController.class),
-                        mock(SystemActions.class),
+                        mCommandQueue,
                         Dependency.get(Dependency.MAIN_HANDLER),
-                        mock(UiEventLogger.class),
-                        mock(NavigationBarOverlayController.class),
                         mock(ConfigurationController.class),
-                        mock(UserTracker.class)));
+                        mock(NavigationBarA11yHelper.class),
+                        mock(TaskbarDelegate.class),
+                        mNavigationBarFactory,
+                        mock(DumpManager.class),
+                        mock(AutoHideController.class)));
         initializeNavigationBars();
     }
 
@@ -138,6 +112,8 @@
 
     @Test
     public void testCreateNavigationBarsIncludeDefaultTrue() {
+        // Tablets may be using taskbar and the logic is different
+        mNavigationBarController.mIsTablet = false;
         doNothing().when(mNavigationBarController).createNavigationBar(any(), any(), any());
 
         mNavigationBarController.createNavigationBars(true, null);
@@ -275,4 +251,9 @@
 
         verify(mSecondaryNavBar).disableAnimationsDuringHide(eq(500L));
     }
+
+    @Test
+    public void test3ButtonTaskbarFlagDisabledNoRegister() {
+        verify(mCommandQueue, never()).addCallback(any(TaskbarDelegate.class));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
index eac68f6..a6ff2e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
@@ -31,9 +31,6 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestableContext;
-import com.android.systemui.navigationbar.buttons.KeyButtonDrawable;
-import com.android.systemui.navigationbar.RotationButton;
-import com.android.systemui.navigationbar.RotationButtonController;
 import com.android.systemui.statusbar.policy.RotationLockController;
 
 import org.junit.Before;
@@ -46,7 +43,6 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class NavigationBarRotationContextTest extends SysuiTestCase {
-    static final int RES_UNDEF = 0;
     static final int DEFAULT_ROTATE = 0;
 
     @Rule
@@ -63,10 +59,18 @@
         final View view = new View(mContext);
         mRotationButton = mock(RotationButton.class);
         mRotationButtonController = new RotationButtonController(mContext, 0, 0);
-        mRotationButtonController.setRotationButton(mRotationButton, (visibility) -> {});
+        mRotationButtonController.setRotationButton(mRotationButton,
+                new RotationButton.RotationButtonUpdatesCallback() {
+                    @Override
+                    public void onVisibilityChanged(boolean isVisible) {
+                    }
+
+                    @Override
+                    public void onPositionChanged() {
+                    }
+                });
         // Due to a mockito issue, only spy the object after setting the initial state
         mRotationButtonController = spy(mRotationButtonController);
-        final KeyButtonDrawable kbd = mock(KeyButtonDrawable.class);
         doReturn(view).when(mRotationButton).getCurrentView();
         doReturn(true).when(mRotationButton).acceptRotationProposal();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 1968f7f..223ffbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -19,6 +19,7 @@
 import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
 import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
 import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
+import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
 import static android.inputmethodservice.InputMethodService.IME_VISIBLE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
@@ -28,7 +29,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -45,26 +45,27 @@
 import android.content.IntentFilter;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.telecom.TelecomManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.InputMethodManager;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestableContext;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
@@ -80,9 +81,10 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.phone.AutoHideController;
+import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
@@ -109,7 +111,13 @@
     private NavigationBar mExternalDisplayNavigationBar;
 
     private SysuiTestableContext mSysuiTestableContextExternal;
+    @Mock
     private OverviewProxyService mOverviewProxyService;
+    @Mock
+    private StatusBarStateController mStatusBarStateController;
+    @Mock
+    private NavigationModeController mNavigationModeController;
+    @Mock
     private CommandQueue mCommandQueue;
     private SysUiState mMockSysUiState;
     @Mock
@@ -122,11 +130,27 @@
     EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory;
     @Mock
     EdgeBackGestureHandler mEdgeBackGestureHandler;
+    @Mock
+    NavigationBarA11yHelper mNavigationBarA11yHelper;
+    @Mock
+    private LightBarController mLightBarController;
+    @Mock
+    private LightBarController.Factory mLightBarcontrollerFactory;
+    @Mock
+    private AutoHideController mAutoHideController;
+    @Mock
+    private AutoHideController.Factory mAutoHideControllerFactory;
+    @Mock
+    private WindowManager mWindowManager;
+    @Mock
+    private TelecomManager mTelecomManager;
+    @Mock
+    private InputMethodManager mInputMethodManager;
+    @Mock
+    private AssistManager mAssistManager;
 
     @Rule
     public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
-    private AccessibilityManagerWrapper mAccessibilityWrapper =
-            new AccessibilityManagerWrapper(mContext);
 
     @Before
     public void setup() throws Exception {
@@ -134,15 +158,19 @@
 
         when(mEdgeBackGestureHandlerFactory.create(any(Context.class)))
                 .thenReturn(mEdgeBackGestureHandler);
-        mCommandQueue = new CommandQueue(mContext);
+        when(mLightBarcontrollerFactory.create(any(Context.class))).thenReturn(mLightBarController);
+        when(mAutoHideControllerFactory.create(any(Context.class))).thenReturn(mAutoHideController);
         setupSysuiDependency();
-        mDependency.injectMockDependency(AssistManager.class);
+        // This class inflates views that call Dependency.get, thus these injections are still
+        // necessary.
+        mDependency.injectTestDependency(AssistManager.class, mAssistManager);
         mDependency.injectMockDependency(KeyguardStateController.class);
-        mDependency.injectMockDependency(StatusBarStateController.class);
+        mDependency.injectTestDependency(StatusBarStateController.class, mStatusBarStateController);
         mDependency.injectMockDependency(NavigationBarController.class);
-        mOverviewProxyService = mDependency.injectMockDependency(OverviewProxyService.class);
         mDependency.injectTestDependency(EdgeBackGestureHandler.Factory.class,
                 mEdgeBackGestureHandlerFactory);
+        mDependency.injectTestDependency(OverviewProxyService.class, mOverviewProxyService);
+        mDependency.injectTestDependency(NavigationModeController.class, mNavigationModeController);
         TestableLooper.get(this).runWithLooper(() -> {
             mNavigationBar = createNavBar(mContext);
             mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
@@ -161,22 +189,21 @@
         mSysuiTestableContextExternal = (SysuiTestableContext) getContext().createDisplayContext(
                 display);
 
-        WindowManager windowManager = mock(WindowManager.class);
-        Display defaultDisplay = mContext.getSystemService(WindowManager.class).getDefaultDisplay();
-        when(windowManager.getDefaultDisplay()).thenReturn(
-                defaultDisplay);
+        Display defaultDisplay = mContext.getDisplay();
+        when(mWindowManager.getDefaultDisplay()).thenReturn(defaultDisplay);
         WindowMetrics metrics = mContext.getSystemService(WindowManager.class)
                 .getMaximumWindowMetrics();
-        when(windowManager.getMaximumWindowMetrics()).thenReturn(metrics);
-        doNothing().when(windowManager).addView(any(), any());
-        mContext.addMockSystemService(Context.WINDOW_SERVICE, windowManager);
-        mSysuiTestableContextExternal.addMockSystemService(Context.WINDOW_SERVICE, windowManager);
-
-        mDependency.injectTestDependency(Dependency.BG_LOOPER, Looper.getMainLooper());
-        mDependency.injectTestDependency(AccessibilityManagerWrapper.class, mAccessibilityWrapper);
-
+        when(mWindowManager.getMaximumWindowMetrics()).thenReturn(metrics);
+        WindowMetrics currentWindowMetrics = mContext.getSystemService(WindowManager.class)
+                .getCurrentWindowMetrics();
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(currentWindowMetrics);
+        doNothing().when(mWindowManager).addView(any(), any());
+        doNothing().when(mWindowManager).removeViewImmediate(any());
         mMockSysUiState = mock(SysUiState.class);
         when(mMockSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mMockSysUiState);
+
+        mContext.addMockSystemService(WindowManager.class, mWindowManager);
+        mSysuiTestableContextExternal.addMockSystemService(WindowManager.class, mWindowManager);
     }
 
     @Test
@@ -233,10 +260,8 @@
         defaultNavBar.createView(null);
         externalNavBar.createView(null);
 
-        // Set IME window status for default NavBar.
-        mCommandQueue.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
-                BACK_DISPOSITION_DEFAULT, true, false);
-        processAllMessages();
+        defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
+                BACK_DISPOSITION_DEFAULT, true);
 
         // Verify IME window state will be updated in default NavBar & external NavBar state reset.
         assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
@@ -244,11 +269,10 @@
         assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
         assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
 
-        // Set IME window status for external NavBar.
-        mCommandQueue.setImeWindowStatus(EXTERNAL_DISPLAY_ID, null,
-                IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true, false);
-        processAllMessages();
-
+        externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, null, IME_VISIBLE,
+                BACK_DISPOSITION_DEFAULT, true);
+        defaultNavBar.setImeWindowStatus(
+                DEFAULT_DISPLAY, null, IME_INVISIBLE, BACK_DISPOSITION_DEFAULT, false);
         // Verify IME window state will be updated in external NavBar & default NavBar state reset.
         assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
                 externalNavBar.getNavigationIconHints());
@@ -256,30 +280,40 @@
         assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
     }
 
+    @Test
+    public void testA11yEventAfterDetach() {
+        View v = mNavigationBar.createView(null);
+        mNavigationBar.onViewAttachedToWindow(v);
+        verify(mNavigationBarA11yHelper).registerA11yEventListener(any(
+                NavigationBarA11yHelper.NavA11yEventListener.class));
+        mNavigationBar.onViewDetachedFromWindow(v);
+        verify(mNavigationBarA11yHelper).removeA11yEventListener(any(
+                NavigationBarA11yHelper.NavA11yEventListener.class));
+
+        // Should be safe even though the internal view is now null.
+        mNavigationBar.updateAccessibilityServicesState();
+    }
+
     private NavigationBar createNavBar(Context context) {
         DeviceProvisionedController deviceProvisionedController =
                 mock(DeviceProvisionedController.class);
         when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
-        assertNotNull(mAccessibilityWrapper);
-        return spy(new NavigationBar(context,
-                mock(WindowManager.class),
-                () -> mock(AssistManager.class),
+        NavigationBar.Factory factory = new NavigationBar.Factory(
+                () -> mAssistManager,
                 mock(AccessibilityManager.class),
-                context.getDisplayId() == DEFAULT_DISPLAY ? mAccessibilityWrapper
-                        : mock(AccessibilityManagerWrapper.class),
                 deviceProvisionedController,
                 new MetricsLogger(),
                 mOverviewProxyService,
-                mock(NavigationModeController.class),
+                mNavigationModeController,
                 mock(AccessibilityButtonModeObserver.class),
-                mock(StatusBarStateController.class),
+                mStatusBarStateController,
                 mMockSysUiState,
                 mBroadcastDispatcher,
                 mCommandQueue,
                 Optional.of(mock(Pip.class)),
                 Optional.of(mock(LegacySplitScreen.class)),
                 Optional.of(mock(Recents.class)),
-                () -> mock(StatusBar.class),
+                () -> Optional.of(mock(StatusBar.class)),
                 mock(ShadeController.class),
                 mock(NotificationRemoteInputManager.class),
                 mock(NotificationShadeDepthController.class),
@@ -287,7 +321,15 @@
                 mHandler,
                 mock(NavigationBarOverlayController.class),
                 mUiEventLogger,
-                mock(UserTracker.class)));
+                mNavigationBarA11yHelper,
+                mock(UserTracker.class),
+                mLightBarController,
+                mLightBarcontrollerFactory,
+                mAutoHideController,
+                mAutoHideControllerFactory,
+                Optional.of(mTelecomManager),
+                mInputMethodManager);
+        return spy(factory.create(context));
     }
 
     private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
new file mode 100644
index 0000000..0a20001
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
@@ -0,0 +1,127 @@
+package com.android.systemui.navigationbar.gestural
+
+import android.view.Gravity
+import android.view.Surface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.navigationbar.gestural.FloatingRotationButtonPositionCalculator.Position
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+internal class FloatingRotationButtonPositionCalculatorTest(private val testCase: TestCase)
+    : SysuiTestCase() {
+
+    private val calculator = FloatingRotationButtonPositionCalculator(
+        MARGIN_DEFAULT, MARGIN_TASKBAR_LEFT, MARGIN_TASKBAR_BOTTOM
+    )
+
+    @Test
+    fun calculatePosition() {
+        val position = calculator.calculatePosition(
+            testCase.rotation,
+            testCase.taskbarVisible,
+            testCase.taskbarStashed
+        )
+
+        assertThat(position).isEqualTo(testCase.expectedPosition)
+    }
+
+    internal class TestCase(
+        val rotation: Int,
+        val taskbarVisible: Boolean,
+        val taskbarStashed: Boolean,
+        val expectedPosition: Position
+    ) {
+        override fun toString(): String =
+            "when rotation = $rotation, " +
+                "taskbarVisible = $taskbarVisible, " +
+                "taskbarStashed = $taskbarStashed - " +
+                "expected $expectedPosition"
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<TestCase> =
+            listOf(
+                TestCase(
+                    rotation = Surface.ROTATION_0,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.LEFT,
+                        translationX = MARGIN_DEFAULT,
+                        translationY = -MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    rotation = Surface.ROTATION_90,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.RIGHT,
+                        translationX = -MARGIN_DEFAULT,
+                        translationY = -MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    rotation = Surface.ROTATION_180,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.TOP or Gravity.RIGHT,
+                        translationX = -MARGIN_DEFAULT,
+                        translationY = MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    rotation = Surface.ROTATION_270,
+                    taskbarVisible = false,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.TOP or Gravity.LEFT,
+                        translationX = MARGIN_DEFAULT,
+                        translationY = MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    rotation = Surface.ROTATION_0,
+                    taskbarVisible = true,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.LEFT,
+                        translationX = MARGIN_TASKBAR_LEFT,
+                        translationY = -MARGIN_TASKBAR_BOTTOM
+                    )
+                ),
+                TestCase(
+                    rotation = Surface.ROTATION_0,
+                    taskbarVisible = true,
+                    taskbarStashed = true,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.LEFT,
+                        translationX = MARGIN_DEFAULT,
+                        translationY = -MARGIN_DEFAULT
+                    )
+                ),
+                TestCase(
+                    rotation = Surface.ROTATION_90,
+                    taskbarVisible = true,
+                    taskbarStashed = false,
+                    expectedPosition = Position(
+                        gravity = Gravity.BOTTOM or Gravity.RIGHT,
+                        translationX = -MARGIN_TASKBAR_LEFT,
+                        translationY = -MARGIN_TASKBAR_BOTTOM
+                    )
+                )
+            )
+
+        private const val MARGIN_DEFAULT = 10
+        private const val MARGIN_TASKBAR_LEFT = 20
+        private const val MARGIN_TASKBAR_BOTTOM = 30
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
index 3562032..e73e5ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
@@ -58,6 +58,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.time.Duration;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 import dagger.Lazy;
@@ -89,7 +90,7 @@
     private IThermalEventListener mSkinThermalEventListener;
     @Mock private BroadcastDispatcher mBroadcastDispatcher;
     @Mock private CommandQueue mCommandQueue;
-    @Mock private Lazy<StatusBar> mStatusBarLazy;
+    @Mock private Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
     @Mock private StatusBar mStatusBar;
 
     @Before
@@ -98,7 +99,7 @@
         mMockWarnings = mDependency.injectMockDependency(WarningsUI.class);
         mEnhancedEstimates = mDependency.injectMockDependency(EnhancedEstimates.class);
 
-        when(mStatusBarLazy.get()).thenReturn(mStatusBar);
+        when(mStatusBarOptionalLazy.get()).thenReturn(Optional.of(mStatusBar));
 
         mContext.addMockSystemService(Context.POWER_SERVICE, mPowerManager);
 
@@ -688,7 +689,8 @@
     }
 
     private void createPowerUi() {
-        mPowerUI = new PowerUI(mContext, mBroadcastDispatcher, mCommandQueue, mStatusBarLazy);
+        mPowerUI = new PowerUI(
+                mContext, mBroadcastDispatcher, mCommandQueue, mStatusBarOptionalLazy);
         mPowerUI.mThermalService = mThermalServiceMock;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
new file mode 100644
index 0000000..e54a6ec
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
@@ -0,0 +1,93 @@
+package com.android.systemui.qs
+
+import com.android.systemui.R
+import android.os.UserManager
+import android.view.LayoutInflater
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.testing.FakeMetricsLogger
+import com.android.systemui.Dependency
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.globalactions.GlobalActionsDialogLite
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.FooterActionsController.ExpansionState
+import com.android.systemui.statusbar.phone.MultiUserSwitchController
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.UserInfoController
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.utils.leaks.FakeTunerService
+import com.android.systemui.utils.leaks.LeakCheckedTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+class FooterActionsControllerTest : LeakCheckedTest() {
+    @Mock
+    private lateinit var userManager: UserManager
+    @Mock
+    private lateinit var activityStarter: ActivityStarter
+    @Mock
+    private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock
+    private lateinit var userInfoController: UserInfoController
+    @Mock
+    private lateinit var qsPanelController: QSPanelController
+    @Mock
+    private lateinit var multiUserSwitchController: MultiUserSwitchController
+    @Mock
+    private lateinit var globalActionsDialog: GlobalActionsDialogLite
+    @Mock
+    private lateinit var uiEventLogger: UiEventLogger
+    @Mock
+    private lateinit var controller: FooterActionsController
+
+    private val metricsLogger: MetricsLogger = FakeMetricsLogger()
+    private lateinit var view: FooterActionsView
+    private val falsingManager: FalsingManagerFake = FalsingManagerFake()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        injectLeakCheckedDependencies(*LeakCheckedTest.ALL_SUPPORTED_CLASSES)
+        val fakeTunerService = Dependency.get(TunerService::class.java) as FakeTunerService
+
+        view = LayoutInflater.from(context)
+                .inflate(R.layout.footer_actions, null) as FooterActionsView
+
+        controller = FooterActionsController(view, qsPanelController, activityStarter,
+                userManager, userInfoController, multiUserSwitchController,
+                deviceProvisionedController, falsingManager, metricsLogger, fakeTunerService,
+                globalActionsDialog, uiEventLogger, showPMLiteButton = true,
+                buttonsVisibleState = ExpansionState.EXPANDED)
+        controller.init()
+        controller.onViewAttached()
+    }
+
+    @Test
+    fun testLogPowerMenuClick() {
+        controller.expanded = true
+        falsingManager.setFalseTap(false)
+
+        view.findViewById<View>(R.id.pm_lite).performClick()
+        // Verify clicks are logged
+        verify(uiEventLogger, Mockito.times(1))
+                .log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
+    }
+
+    @Test
+    fun testSettings_UserNotSetup() {
+        whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
+        view.findViewById<View>(R.id.settings_button).performClick()
+        // Verify Settings wasn't launched.
+        verify<ActivityStarter>(activityStarter, Mockito.never()).startActivity(any(), anyBoolean())
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterViewControllerTest.java
index 6f7bf3b..8b19c50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterViewControllerTest.java
@@ -18,16 +18,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.ClipData;
 import android.content.ClipboardManager;
-import android.os.UserManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
@@ -35,21 +30,8 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.testing.FakeMetricsLogger;
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.globalactions.GlobalActionsDialogLite;
-import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.phone.MultiUserSwitchController;
-import com.android.systemui.statusbar.phone.SettingsButton;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.UserInfoController;
-import com.android.systemui.tuner.TunerService;
-import com.android.systemui.utils.leaks.FakeTunerService;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
 import org.junit.Before;
@@ -67,14 +49,6 @@
     @Mock
     private QSFooterView mView;
     @Mock
-    private UserManager mUserManager;
-    @Mock
-    private ActivityStarter mActivityStarter;
-    @Mock
-    private DeviceProvisionedController mDeviceProvisionedController;
-    @Mock
-    private UserInfoController mUserInfoController;
-    @Mock
     private UserTracker mUserTracker;
     @Mock
     private QSPanelController mQSPanelController;
@@ -82,36 +56,19 @@
     private ClipboardManager mClipboardManager;
     @Mock
     private QuickQSPanelController mQuickQSPanelController;
-    private FakeTunerService mFakeTunerService;
-    private MetricsLogger mMetricsLogger = new FakeMetricsLogger();
-    private FalsingManagerFake mFalsingManager;
-
-    @Mock
-    private SettingsButton mSettingsButton;
     @Mock
     private TextView mBuildText;
     @Mock
-    private View mEdit;
-    @Mock
-    private MultiUserSwitchController mMultiUserSwitchController;
-    @Mock
-    private View mPowerMenuLiteView;
-    @Mock
-    private GlobalActionsDialogLite mGlobalActionsDialog;
-    @Mock
-    private UiEventLogger mUiEventLogger;
+    private FooterActionsController mFooterActionsController;
 
     private QSFooterViewController mController;
 
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mFalsingManager = new FalsingManagerFake();
 
         injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
 
-        mFakeTunerService = (FakeTunerService) Dependency.get(TunerService.class);
-
         mContext.addMockSystemService(ClipboardManager.class, mClipboardManager);
 
         when(mView.getContext()).thenReturn(mContext);
@@ -119,16 +76,10 @@
         when(mUserTracker.getUserContext()).thenReturn(mContext);
 
         when(mView.isAttachedToWindow()).thenReturn(true);
-        when(mView.findViewById(R.id.settings_button)).thenReturn(mSettingsButton);
         when(mView.findViewById(R.id.build)).thenReturn(mBuildText);
-        when(mView.findViewById(android.R.id.edit)).thenReturn(mEdit);
-        when(mView.findViewById(R.id.pm_lite)).thenReturn(mPowerMenuLiteView);
 
-        mController = new QSFooterViewController(mView, mUserManager, mUserInfoController,
-                mActivityStarter, mDeviceProvisionedController, mUserTracker, mQSPanelController,
-                mMultiUserSwitchController, mQuickQSPanelController, mFakeTunerService,
-                mMetricsLogger, mFalsingManager, false, mGlobalActionsDialog,
-                mUiEventLogger);
+        mController = new QSFooterViewController(mView, mUserTracker, mQSPanelController,
+                mQuickQSPanelController, mFooterActionsController);
 
         mController.init();
     }
@@ -148,40 +99,4 @@
         verify(mClipboardManager).setPrimaryClip(captor.capture());
         assertThat(captor.getValue().getItemAt(0).getText()).isEqualTo(text);
     }
-
-    @Test
-    public void testSettings_UserNotSetup() {
-        ArgumentCaptor<View.OnClickListener> onClickCaptor =
-                ArgumentCaptor.forClass(View.OnClickListener.class);
-        verify(mSettingsButton).setOnClickListener(onClickCaptor.capture());
-
-        when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(false);
-
-        onClickCaptor.getValue().onClick(mSettingsButton);
-        // Verify Settings wasn't launched.
-        verify(mActivityStarter, never()).startActivity(any(), anyBoolean());
-    }
-
-    @Test
-    public void testLogPowerMenuClick() {
-        // Enable power menu button
-        mController = new QSFooterViewController(mView, mUserManager, mUserInfoController,
-                mActivityStarter, mDeviceProvisionedController, mUserTracker, mQSPanelController,
-                mMultiUserSwitchController, mQuickQSPanelController, mFakeTunerService,
-                mMetricsLogger, new FalsingManagerFake(), true, mGlobalActionsDialog,
-                mUiEventLogger);
-        mController.init();
-        mController.setExpanded(true);
-        mFalsingManager.setFalseTap(false);
-
-        ArgumentCaptor<View.OnClickListener> onClickCaptor =
-                ArgumentCaptor.forClass(View.OnClickListener.class);
-        verify(mPowerMenuLiteView).setOnClickListener(onClickCaptor.capture());
-
-        onClickCaptor.getValue().onClick(mPowerMenuLiteView);
-
-        // Verify clicks are logged
-        verify(mUiEventLogger, times(1))
-                .log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS);
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 7f89b26..12c0d53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -53,7 +54,6 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -95,9 +95,9 @@
     @Mock
     private KeyguardBypassController mBypassController;
     @Mock
-    private FeatureFlags mFeatureFlags;
-    @Mock
     private FalsingManager mFalsingManager;
+    @Mock
+    private FeatureFlags mFeatureFlags;
 
     public QSFragmentTest() {
         super(QSFragment.class);
@@ -189,7 +189,6 @@
                 mQQSMediaHost,
                 mBypassController,
                 mQsComponentFactory,
-                mFeatureFlags,
                 mFalsingManager,
                 mock(DumpManager.class));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index faef870..2e1fb07 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -50,7 +50,6 @@
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.util.animation.DisappearParameters;
 
 import org.junit.Before;
@@ -94,8 +93,6 @@
     @Mock
     PagedTileLayout mPagedTileLayout;
     @Mock
-    FeatureFlags mFeatureFlags;
-    @Mock
     Resources mResources;
     @Mock
     Configuration mConfiguration;
@@ -109,9 +106,9 @@
         protected TestableQSPanelControllerBase(QSPanel view, QSTileHost host,
                 QSCustomizerController qsCustomizerController, MediaHost mediaHost,
                 MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
-                DumpManager dumpManager, FeatureFlags featureFlags) {
+                DumpManager dumpManager) {
             super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger,
-                    qsLogger, dumpManager, featureFlags);
+                    qsLogger, dumpManager);
         }
 
         @Override
@@ -141,7 +138,7 @@
 
         mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost,
                 mQSCustomizerController, mMediaHost,
-                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mFeatureFlags);
+                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
 
         mController.init();
         reset(mQSTileRevealController);
@@ -153,7 +150,7 @@
 
         QSPanelControllerBase<QSPanel> controller = new TestableQSPanelControllerBase(mQSPanel,
                 mQSTileHost, mQSCustomizerController, mMediaHost,
-                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mFeatureFlags) {
+                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager) {
             @Override
             protected QSTileRevealController createTileRevealController() {
                 return mQSTileRevealController;
@@ -242,21 +239,20 @@
         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
         when(mMediaHost.getVisible()).thenReturn(true);
 
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(false);
+        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false);
         when(mQSPanel.getDumpableTag()).thenReturn("QSPanelLandscape");
         mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost,
                 mQSCustomizerController, mMediaHost,
-                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mFeatureFlags);
+                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
         mController.init();
 
         assertThat(mController.shouldUseHorizontalLayout()).isTrue();
 
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(true);
         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(true);
         when(mQSPanel.getDumpableTag()).thenReturn("QSPanelPortrait");
         mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost,
                 mQSCustomizerController, mMediaHost,
-                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mFeatureFlags);
+                mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
         mController.init();
 
         assertThat(mController.shouldUseHorizontalLayout()).isFalse();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.java
index 35ebacb..06a4ae0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.java
@@ -44,7 +44,7 @@
 import com.android.systemui.settings.brightness.BrightnessController;
 import com.android.systemui.settings.brightness.BrightnessSlider;
 import com.android.systemui.settings.brightness.ToggleSlider;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.animation.DisappearParameters;
 
@@ -97,9 +97,9 @@
     QSTileView mQSTileView;
     @Mock
     PagedTileLayout mPagedTileLayout;
-    FalsingManagerFake mFalsingManager = new FalsingManagerFake();
     @Mock
-    FeatureFlags mFeatureFlags;
+    CommandQueue mCommandQueue;
+    FalsingManagerFake mFalsingManager = new FalsingManagerFake();
     @Mock
     Resources mResources;
     @Mock
@@ -131,7 +131,7 @@
                 mQSTileHost, mQSCustomizerController, true, mMediaHost,
                 mQSTileRevealControllerFactory, mDumpManager, mMetricsLogger, mUiEventLogger,
                 mQSLogger, mBrightnessControllerFactory, mToggleSliderViewControllerFactory,
-                mFalsingManager, mFeatureFlags
+                mFalsingManager, mCommandQueue
         );
 
         mController.init();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
deleted file mode 100644
index 16d4ddd..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.qs;
-
-import static junit.framework.Assert.assertEquals;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.res.Configuration;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableLooper.RunWithLooper;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.qs.QSTileView;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.util.animation.UniqueObjectHostView;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Collections;
-
-@RunWith(AndroidTestingRunner.class)
-@RunWithLooper
-@SmallTest
-public class QSPanelTest extends SysuiTestCase {
-
-    private TestableLooper mTestableLooper;
-    private QSPanel mQsPanel;
-    @Mock
-    private QSTileHost mHost;
-    @Mock
-    private QSTileImpl dndTile;
-    @Mock
-    private QSPanelControllerBase.TileRecord mDndTileRecord;
-    private ViewGroup mParentView;
-    @Mock
-    private QSDetail.Callback mCallback;
-    @Mock
-    private QSTileView mQSTileView;
-
-    private UniqueObjectHostView mMediaView;
-    private View mSecurityFooter;
-    @Mock
-    private FrameLayout mHeaderContainer;
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mTestableLooper = TestableLooper.get(this);
-
-        mDndTileRecord.tile = dndTile;
-        mDndTileRecord.tileView = mQSTileView;
-
-        mMediaView = new UniqueObjectHostView(mContext);
-        mSecurityFooter = new View(mContext);
-
-        mTestableLooper.runWithLooper(() -> {
-            mQsPanel = new QSPanel(mContext, null);
-            mQsPanel.initialize();
-            mQsPanel.onFinishInflate();
-            // Provides a parent with non-zero size for QSPanel
-            mParentView = new FrameLayout(mContext);
-            mParentView.addView(mQsPanel);
-
-            when(dndTile.getTileSpec()).thenReturn("dnd");
-            when(mHost.getTiles()).thenReturn(Collections.emptyList());
-            when(mHost.createTileView(any(), any(), anyBoolean())).thenReturn(mQSTileView);
-            mQsPanel.addTile(mDndTileRecord);
-            mQsPanel.setCallback(mCallback);
-            mQsPanel.setHeaderContainer(mHeaderContainer);
-        });
-    }
-
-    @Test
-    public void testOpenDetailsWithExistingTile_NoException() {
-        mTestableLooper.processAllMessages();
-        mQsPanel.openDetails(dndTile);
-        mTestableLooper.processAllMessages();
-
-        verify(mCallback).onShowingDetail(any(), anyInt(), anyInt());
-    }
-
-    @Test
-    public void testOpenDetailsWithNullParameter_NoException() {
-        mTestableLooper.processAllMessages();
-        mQsPanel.openDetails(null);
-        mTestableLooper.processAllMessages();
-
-        verify(mCallback, never()).onShowingDetail(any(), anyInt(), anyInt());
-    }
-
-    @Test
-    public void testSecurityFooterAtEndNoMedia_portrait() {
-        mTestableLooper.processAllMessages();
-
-        mContext.getResources().getConfiguration().orientation = Configuration.ORIENTATION_PORTRAIT;
-
-        mQsPanel.setSecurityFooter(mSecurityFooter);
-
-        int children = mQsPanel.getChildCount();
-        assertEquals(children - 1, mQsPanel.indexOfChild(mSecurityFooter));
-    }
-
-    @Test
-    public void testSecurityFooterRightBeforeMedia_portrait() {
-        mTestableLooper.processAllMessages();
-
-        mContext.getResources().getConfiguration().orientation = Configuration.ORIENTATION_PORTRAIT;
-
-        mQsPanel.addView(mMediaView);
-
-        mQsPanel.setSecurityFooter(mSecurityFooter);
-
-        int securityFooterIndex = mQsPanel.indexOfChild(mSecurityFooter);
-        int mediaIndex = mQsPanel.indexOfChild(mMediaView);
-
-        assertEquals(mediaIndex - 1, securityFooterIndex);
-    }
-
-    @Test
-    public void testSecurityFooterRightBeforeMedia_portrait_configChange() {
-        mTestableLooper.processAllMessages();
-
-        mContext.getResources().getConfiguration().orientation = Configuration.ORIENTATION_PORTRAIT;
-
-        mQsPanel.addView(mMediaView);
-
-        mQsPanel.setSecurityFooter(mSecurityFooter);
-
-        mQsPanel.onConfigurationChanged(mContext.getResources().getConfiguration());
-
-        int securityFooterIndex = mQsPanel.indexOfChild(mSecurityFooter);
-        int mediaIndex = mQsPanel.indexOfChild(mMediaView);
-
-        assertEquals(mediaIndex - 1, securityFooterIndex);
-    }
-
-    @Test
-    public void testSecurityFooterInHeader_landscape() {
-        mTestableLooper.processAllMessages();
-
-        mContext.getResources().getConfiguration().orientation =
-                Configuration.ORIENTATION_LANDSCAPE;
-
-        mQsPanel.setSecurityFooter(mSecurityFooter);
-
-        verify(mHeaderContainer).addView(mSecurityFooter, 0);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
new file mode 100644
index 0000000..3500c18
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.qs
+
+import android.content.res.Configuration
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.qs.QSTileView
+import com.android.systemui.qs.QSPanelControllerBase.TileRecord
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+@SmallTest
+class QSPanelTest : SysuiTestCase() {
+    private lateinit var mTestableLooper: TestableLooper
+    private lateinit var mQsPanel: QSPanel
+
+    @Mock
+    private lateinit var mHost: QSTileHost
+
+    @Mock
+    private lateinit var dndTile: QSTileImpl<*>
+
+    @Mock
+    private lateinit var mDndTileRecord: TileRecord
+
+    @Mock
+    private lateinit var mQSLogger: QSLogger
+    private lateinit var mParentView: ViewGroup
+
+    @Mock
+    private lateinit var mCallback: QSDetail.Callback
+
+    @Mock
+    private lateinit var mQSTileView: QSTileView
+
+    private lateinit var mFooter: View
+
+    @Before
+    @Throws(Exception::class)
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mTestableLooper = TestableLooper.get(this)
+
+        mDndTileRecord.tile = dndTile
+        mDndTileRecord.tileView = mQSTileView
+        mTestableLooper.runWithLooper {
+            mQsPanel = QSPanel(mContext, null)
+            mQsPanel.initialize()
+            // QSPanel inflates a footer inside of it, mocking it here
+            mFooter = LinearLayout(mContext).apply { id = R.id.qs_footer }
+            mQsPanel.addView(mFooter)
+            mQsPanel.onFinishInflate()
+            mQsPanel.setSecurityFooter(View(mContext), false)
+            mQsPanel.setHeaderContainer(LinearLayout(mContext))
+            // Provides a parent with non-zero size for QSPanel
+            mParentView = FrameLayout(mContext).apply {
+                addView(mQsPanel)
+            }
+
+            whenever(dndTile.tileSpec).thenReturn("dnd")
+            whenever(mHost.tiles).thenReturn(emptyList())
+            whenever(mHost.createTileView(any(), any(), anyBoolean())).thenReturn(mQSTileView)
+            mQsPanel.addTile(mDndTileRecord)
+            mQsPanel.setCallback(mCallback)
+        }
+    }
+
+    @Test
+    fun testOpenDetailsWithExistingTile_NoException() {
+        mTestableLooper.runWithLooper {
+            mQsPanel.openDetails(dndTile)
+        }
+
+        verify(mCallback).onShowingDetail(any(), anyInt(), anyInt())
+    }
+
+    @Test
+    fun testOpenDetailsWithNullParameter_NoException() {
+        mTestableLooper.runWithLooper {
+            mQsPanel.openDetails(null)
+        }
+
+        verify(mCallback, never()).onShowingDetail(any(), anyInt(), anyInt())
+    }
+
+    @Test
+    fun testSecurityFooter_appearsOnBottomOnSplitShade() {
+        mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_LANDSCAPE))
+        mQsPanel.switchSecurityFooter(true)
+
+        mTestableLooper.runWithLooper {
+            mQsPanel.isExpanded = true
+        }
+
+        // After mFooter
+        assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(
+                mQsPanel.indexOfChild(mFooter) + 1
+        )
+    }
+
+    @Test
+    fun testSecurityFooter_appearsOnBottomIfPortrait() {
+        mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_PORTRAIT))
+        mQsPanel.switchSecurityFooter(false)
+
+        mTestableLooper.runWithLooper {
+            mQsPanel.isExpanded = true
+        }
+
+        // After mFooter
+        assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(
+                mQsPanel.indexOfChild(mFooter) + 1
+        )
+    }
+
+    @Test
+    fun testSecurityFooter_appearsOnTopIfSmallScreenAndLandscape() {
+        mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_LANDSCAPE))
+        mQsPanel.switchSecurityFooter(false)
+
+        mTestableLooper.runWithLooper {
+            mQsPanel.isExpanded = true
+        }
+
+        // -1 means that it is part of the mHeaderContainer
+        assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(-1)
+    }
+
+    private fun getNewOrientationConfig(@Configuration.Orientation newOrientation: Int) =
+            context.resources.configuration.apply { orientation = newOrientation }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 0b67c9c..6bb2b9d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -49,6 +49,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -60,7 +61,6 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSBrightnessControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSBrightnessControllerTest.kt
new file mode 100644
index 0000000..de1d86b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSBrightnessControllerTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.brightness.BrightnessController
+import com.android.systemui.statusbar.policy.BrightnessMirrorController
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.never
+import org.mockito.Mockito.mock
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+class QuickQSBrightnessControllerTest : SysuiTestCase() {
+
+    @Mock
+    lateinit var brightnessController: BrightnessController
+    @get:Rule
+    val mockito = MockitoJUnit.rule()
+
+    lateinit var quickQSBrightnessController: QuickQSBrightnessController
+
+    @Before
+    fun setUp() {
+        quickQSBrightnessController = QuickQSBrightnessController(
+                brightnessControllerFactory = { brightnessController })
+    }
+
+    @Test
+    fun testSliderIsShownWhenInitializedInSplitShade() {
+        quickQSBrightnessController.init(shouldUseSplitNotificationShade = true)
+
+        verify(brightnessController).showSlider()
+    }
+
+    @Test
+    fun testSliderIsShownWhenRefreshedInSplitShade() {
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = true)
+
+        verify(brightnessController, times(1)).showSlider()
+    }
+
+    @Test
+    fun testSliderIsHiddenWhenRefreshedInNonSplitShade() {
+        // needs to be shown first
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = true)
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = false)
+
+        verify(brightnessController).hideSlider()
+    }
+
+    @Test
+    fun testSliderChangesVisibilityWhenRotating() {
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = true)
+        verify(brightnessController, times(1)).showSlider()
+
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = false)
+        verify(brightnessController, times(1)).hideSlider()
+    }
+
+    @Test
+    fun testCallbacksAreRegisteredOnlyOnce() {
+        // this flow simulates expanding shade in portrait...
+        quickQSBrightnessController.setListening(true)
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = false)
+        // ... and rotating to landscape/split shade where slider is visible
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = true)
+
+        verify(brightnessController, times(1)).registerCallbacks()
+    }
+
+    @Test
+    fun testCallbacksAreRegisteredOnlyOnceWhenRotatingPhone() {
+        quickQSBrightnessController.setListening(true)
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = true)
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = false)
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = true)
+
+        verify(brightnessController, times(1)).registerCallbacks()
+    }
+
+    @Test
+    fun testCallbacksAreNotRegisteredWhenSliderNotVisible() {
+        quickQSBrightnessController.setListening(true)
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = false)
+
+        verify(brightnessController, never()).registerCallbacks()
+    }
+
+    @Test
+    fun testMirrorIsSetWhenSliderIsShown() {
+        val mirrorController = mock(BrightnessMirrorController::class.java)
+        quickQSBrightnessController.setMirror(mirrorController)
+        quickQSBrightnessController.refreshVisibility(shouldUseSplitNotificationShade = true)
+
+        verify(brightnessController).setMirror(mirrorController)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 0604e1b..59948d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -16,8 +16,9 @@
 
 package com.android.systemui.qs
 
+import android.content.res.Configuration
+import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
@@ -27,12 +28,13 @@
 import com.android.systemui.plugins.qs.QSTileView
 import com.android.systemui.qs.customize.QSCustomizerController
 import com.android.systemui.qs.logging.QSLogger
-import com.android.systemui.statusbar.FeatureFlags
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
@@ -65,7 +67,11 @@
     @Mock
     private lateinit var tileView: QSTileView
     @Mock
-    private lateinit var featureFlags: FeatureFlags
+    private lateinit var quickQsBrightnessController: QuickQSBrightnessController
+    @Mock
+    private lateinit var footerActionsController: FooterActionsController
+    @Captor
+    private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
 
     private lateinit var controller: QuickQSPanelController
 
@@ -74,7 +80,9 @@
         MockitoAnnotations.initMocks(this)
 
         `when`(quickQSPanel.tileLayout).thenReturn(tileLayout)
+        `when`(quickQSPanel.isAttachedToWindow).thenReturn(true)
         `when`(quickQSPanel.dumpableTag).thenReturn("")
+        `when`(quickQSPanel.resources).thenReturn(mContext.resources)
         `when`(qsTileHost.createTileView(any(), any(), anyBoolean())).thenReturn(tileView)
 
         controller = QuickQSPanelController(
@@ -87,7 +95,8 @@
                 uiEventLogger,
                 qsLogger,
                 dumpManager,
-                featureFlags
+                quickQsBrightnessController,
+                footerActionsController
         )
 
         controller.init()
@@ -117,4 +126,16 @@
 
         verify(quickQSPanel, times(limit)).addTile(any())
     }
-}
\ No newline at end of file
+
+    @Test
+    fun testBrightnessAndFooterVisibilityRefreshedWhenConfigurationChanged() {
+        // times(2) because both controller and base controller are registering their listeners
+        verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
+
+        captor.allValues.forEach { it.onConfigurationChange(Configuration.EMPTY) }
+
+        verify(quickQsBrightnessController).refreshVisibility(anyBoolean())
+        // times(2) because footer visibility is also refreshed on controller init
+        verify(footerActionsController, times(2)).refreshVisibility(anyBoolean())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
index 8b7e20e..92b9f75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
@@ -23,8 +23,10 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.colorextraction.SysuiColorExtractor
 import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.privacy.PrivacyDialogController
@@ -32,7 +34,6 @@
 import com.android.systemui.privacy.logging.PrivacyLogger
 import com.android.systemui.qs.carrier.QSCarrierGroup
 import com.android.systemui.qs.carrier.QSCarrierGroupController
-import com.android.systemui.statusbar.FeatureFlags
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.policy.Clock
@@ -49,9 +50,9 @@
 import org.mockito.Answers
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -93,6 +94,8 @@
     @Mock
     private lateinit var variableDateViewController: VariableDateViewController
     @Mock
+    private lateinit var batteryMeterViewController: BatteryMeterViewController
+    @Mock
     private lateinit var clock: Clock
     @Mock
     private lateinit var variableDateView: VariableDateView
@@ -143,6 +146,7 @@
                 colorExtractor,
                 privacyDialogController,
                 qsExpansionPathInterpolator,
+                batteryMeterViewController,
                 featureFlags,
                 variableDateViewControllerFactory
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java
index 72c7ddd..8ae7100 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java
@@ -37,10 +37,10 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.CarrierTextManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 import com.android.systemui.utils.os.FakeHandler;
@@ -232,7 +232,7 @@
         MobileDataIndicators indicators = new MobileDataIndicators(
                 mock(NetworkController.IconState.class),
                 mock(NetworkController.IconState.class),
-                0, 0, true, true, "", "", "", true, 0, true, true);
+                0, 0, true, true, "", "", "", 0, true, true);
         mSignalCallback.setMobileDataIndicators(indicators);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index c5b6709..018806e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -53,12 +53,12 @@
 import com.android.internal.logging.InstanceId;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QSIconView;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
index 01fa222..e3045eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
@@ -43,12 +43,12 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSFactoryImpl;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
index e939411..f0bd0657 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
@@ -43,12 +43,12 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.CastController.CastDevice;
 import com.android.systemui.statusbar.policy.HotspotController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
index a70c2be..8922b43 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
@@ -228,7 +228,9 @@
         mTile.handleClick(null /* view */);
         mTestableLooper.processAllMessages();
 
-        verify(mSpiedContext).startActivity(mIntentCaptor.capture());
+        verify(mActivityStarter).startActivity(mIntentCaptor.capture(), eq(true) /* dismissShade */,
+                (ActivityLaunchAnimator.Controller) eq(null),
+                eq(true) /* showOverLockscreenWhenLocked */);
 
         Intent nextStartedIntent = mIntentCaptor.getValue();
         String walletClassName = "com.android.systemui.wallet.ui.WalletActivity";
@@ -246,7 +248,8 @@
         mTestableLooper.processAllMessages();
 
         verify(mActivityStarter).startActivity(mIntentCaptor.capture(), eq(true) /* dismissShade */,
-                (ActivityLaunchAnimator.Controller) eq(null));
+                (ActivityLaunchAnimator.Controller) eq(null),
+                eq(true) /* showOverLockscreenWhenLocked */);
 
         Intent nextStartedIntent = mIntentCaptor.getValue();
         String walletClassName = "com.android.systemui.wallet.ui.WalletActivity";
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
deleted file mode 100644
index 49ab777..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.qs.tiles;
-
-import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
-import static android.provider.Settings.Secure.CAMERA_AUTOROTATE;
-
-import static junit.framework.TestCase.assertEquals;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.when;
-
-import android.Manifest;
-import android.content.pm.PackageManager;
-import android.hardware.SensorPrivacyManager;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.text.TextUtils;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSTileHost;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.RotationLockController;
-import com.android.systemui.statusbar.policy.RotationLockControllerImpl;
-import com.android.systemui.util.settings.FakeSettings;
-import com.android.systemui.util.settings.SecureSettings;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@SmallTest
-public class RotationLockTileTest extends SysuiTestCase {
-
-    private static final String PACKAGE_NAME = "package_name";
-
-    @Mock
-    private PackageManager mPackageManager;
-    @Mock
-    private ActivityStarter mActivityStarter;
-    @Mock
-    private QSTileHost mHost;
-    @Mock
-    private MetricsLogger mMetricsLogger;
-    @Mock
-    private StatusBarStateController mStatusBarStateController;
-    @Mock
-    private QSLogger mQSLogger;
-    @Mock
-    private SensorPrivacyManager mPrivacyManager;
-    @Mock
-    private BatteryController mBatteryController;
-
-    private SecureSettings mSecureSettings;
-    private RotationLockController mController;
-    private TestableLooper mTestableLooper;
-    private RotationLockTile mLockTile;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mTestableLooper = TestableLooper.get(this);
-
-        when(mHost.getContext()).thenReturn(mContext);
-        when(mHost.getUserContext()).thenReturn(mContext);
-
-        mSecureSettings = new FakeSettings();
-        mController = new RotationLockControllerImpl(mContext, mSecureSettings);
-
-        mLockTile = new RotationLockTile(
-                mHost,
-                mTestableLooper.getLooper(),
-                new Handler(mTestableLooper.getLooper()),
-                new FalsingManagerFake(),
-                mMetricsLogger,
-                mStatusBarStateController,
-                mActivityStarter,
-                mQSLogger,
-                mController,
-                mPrivacyManager,
-                mBatteryController,
-                mSecureSettings
-        );
-
-        mLockTile.initialize();
-
-        // We are not setting the mocks to listening, so we trigger a first refresh state to
-        // set the initial state
-        mLockTile.refreshState();
-
-        mTestableLooper.processAllMessages();
-
-        mContext.setMockPackageManager(mPackageManager);
-        doReturn(PACKAGE_NAME).when(mPackageManager).getRotationResolverPackageName();
-        doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager).checkPermission(
-                Manifest.permission.CAMERA, PACKAGE_NAME);
-
-        when(mBatteryController.isPowerSave()).thenReturn(false);
-        when(mPrivacyManager.isSensorPrivacyEnabled(CAMERA)).thenReturn(false);
-        enableAutoRotation();
-        enableCameraBasedRotation();
-
-        mLockTile.refreshState();
-        mTestableLooper.processAllMessages();
-    }
-
-    @Test
-    public void testSecondaryString_cameraRotateOn_returnsFaceBased() {
-        assertEquals("On - Face-based", mLockTile.getState().secondaryLabel);
-    }
-
-    @Test
-    public void testSecondaryString_rotateOff_isEmpty() {
-        disableAutoRotation();
-
-        mLockTile.refreshState();
-        mTestableLooper.processAllMessages();
-
-        assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel));
-    }
-
-    @Test
-    public void testSecondaryString_cameraRotateOff_isEmpty() {
-        disableCameraBasedRotation();
-
-        mLockTile.refreshState();
-        mTestableLooper.processAllMessages();
-
-        assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel));
-    }
-
-    @Test
-    public void testSecondaryString_powerSaveEnabled_isEmpty() {
-        when(mBatteryController.isPowerSave()).thenReturn(true);
-
-        mLockTile.refreshState();
-        mTestableLooper.processAllMessages();
-
-        assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel));
-    }
-
-    @Test
-    public void testSecondaryString_cameraDisabled_isEmpty() {
-        when(mPrivacyManager.isSensorPrivacyEnabled(CAMERA)).thenReturn(true);
-
-        mLockTile.refreshState();
-        mTestableLooper.processAllMessages();
-
-        assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel));
-    }
-
-    @Test
-    public void testSecondaryString_noCameraPermission_isEmpty() {
-        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
-                Manifest.permission.CAMERA, PACKAGE_NAME);
-
-        mLockTile.refreshState();
-        mTestableLooper.processAllMessages();
-
-        assertTrue(TextUtils.isEmpty(mLockTile.getState().secondaryLabel));
-    }
-
-    private void enableAutoRotation() {
-        Settings.System.putIntForUser(mContext.getContentResolver(),
-                Settings.System.ACCELEROMETER_ROTATION, 1, UserHandle.USER_CURRENT);
-    }
-
-    private void disableAutoRotation() {
-        Settings.System.putIntForUser(mContext.getContentResolver(),
-                Settings.System.ACCELEROMETER_ROTATION, 0, UserHandle.USER_CURRENT);
-    }
-
-    private void enableCameraBasedRotation() {
-        mSecureSettings.putIntForUser(
-                CAMERA_AUTOROTATE, 1, UserHandle.USER_CURRENT);
-    }
-
-    private void disableCameraBasedRotation() {
-        mSecureSettings.putIntForUser(
-                CAMERA_AUTOROTATE, 0, UserHandle.USER_CURRENT);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
index f9d5be6..fe32839 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
@@ -48,9 +48,9 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.AccessPointController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.AccessPointController;
 import com.android.systemui.toast.SystemUIToast;
 import com.android.systemui.toast.ToastFactory;
 import com.android.systemui.util.CarrierConfigTracker;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
index 7cea430..c42b64a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
@@ -19,7 +19,6 @@
 import android.testing.TestableLooper;
 import android.view.View;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.filters.SmallTest;
@@ -74,7 +73,6 @@
     private LinearLayout mConnectedWifi;
     private RecyclerView mWifiList;
     private LinearLayout mSeeAll;
-    private LinearLayout mWifiScanNotify;
 
     @Before
     public void setUp() {
@@ -106,7 +104,6 @@
         mConnectedWifi = mDialogView.requireViewById(R.id.wifi_connected_layout);
         mWifiList = mDialogView.requireViewById(R.id.wifi_list_layout);
         mSeeAll = mDialogView.requireViewById(R.id.see_all_layout);
-        mWifiScanNotify = mDialogView.requireViewById(R.id.wifi_scan_notify_layout);
     }
 
     @After
@@ -267,50 +264,6 @@
     }
 
     @Test
-    public void updateDialog_wifiOn_hideWifiScanNotify() {
-        // The preconditions WiFi ON and Internet WiFi are already in setUp()
-
-        mInternetDialog.updateDialog();
-
-        assertThat(mWifiScanNotify.getVisibility()).isEqualTo(View.GONE);
-    }
-
-    @Test
-    public void updateDialog_wifiOffAndWifiScanOff_hideWifiScanNotify() {
-        when(mWifiManager.isWifiEnabled()).thenReturn(false);
-        when(mWifiManager.isScanAlwaysAvailable()).thenReturn(false);
-
-        mInternetDialog.updateDialog();
-
-        assertThat(mWifiScanNotify.getVisibility()).isEqualTo(View.GONE);
-    }
-
-    @Test
-    public void updateDialog_wifiOffAndWifiScanOnAndDeviceLocked_hideWifiScanNotify() {
-        when(mWifiManager.isWifiEnabled()).thenReturn(false);
-        when(mWifiManager.isScanAlwaysAvailable()).thenReturn(true);
-        when(mInternetDialogController.isDeviceLocked()).thenReturn(true);
-
-        mInternetDialog.updateDialog();
-
-        assertThat(mWifiScanNotify.getVisibility()).isEqualTo(View.GONE);
-    }
-
-    @Test
-    public void updateDialog_wifiOffAndWifiScanOnAndDeviceUnlocked_showWifiScanNotify() {
-        when(mWifiManager.isWifiEnabled()).thenReturn(false);
-        when(mWifiManager.isScanAlwaysAvailable()).thenReturn(true);
-        when(mInternetDialogController.isDeviceLocked()).thenReturn(false);
-
-        mInternetDialog.updateDialog();
-
-        assertThat(mWifiScanNotify.getVisibility()).isEqualTo(View.VISIBLE);
-        TextView wifiScanNotifyText = mDialogView.requireViewById(R.id.wifi_scan_notify_text);
-        assertThat(wifiScanNotifyText.getText().length()).isNotEqualTo(0);
-        assertThat(wifiScanNotifyText.getMovementMethod()).isNotNull();
-    }
-
-    @Test
     public void onClickSeeMoreButton_clickSeeAll_verifyLaunchNetworkSetting() {
         mSeeAll.performClick();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserDialogTest.kt
new file mode 100644
index 0000000..d5fe588
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserDialogTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.user
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class UserDialogTest : SysuiTestCase() {
+
+    private lateinit var dialog: UserDialog
+
+    @Before
+    fun setUp() {
+        dialog = UserDialog(mContext)
+    }
+
+    @After
+    fun tearDown() {
+        dialog.dismiss()
+    }
+
+    @Test
+    fun doneButtonExists() {
+        assertThat(dialog.doneButton).isInstanceOf(View::class.java)
+    }
+
+    @Test
+    fun settingsButtonExists() {
+        assertThat(dialog.settingsButton).isInstanceOf(View::class.java)
+    }
+
+    @Test
+    fun gridExistsAndIsViewGroup() {
+        assertThat(dialog.grid).isInstanceOf(ViewGroup::class.java)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
new file mode 100644
index 0000000..7e900c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.user
+
+import android.content.Intent
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.PseudoGridView
+import com.android.systemui.qs.tiles.UserDetailView
+import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatcher
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.util.function.Consumer
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class UserSwitchDialogControllerTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var dialog: UserDialog
+    @Mock
+    private lateinit var falsingManager: FalsingManager
+    @Mock
+    private lateinit var settingsView: View
+    @Mock
+    private lateinit var doneView: View
+    @Mock
+    private lateinit var activityStarter: ActivityStarter
+    @Mock
+    private lateinit var userDetailViewAdapter: UserDetailView.Adapter
+    @Mock
+    private lateinit var launchView: View
+    @Mock
+    private lateinit var gridView: PseudoGridView
+    @Mock
+    private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+    @Captor
+    private lateinit var clickCaptor: ArgumentCaptor<View.OnClickListener>
+
+    private lateinit var controller: UserSwitchDialogController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(dialog.settingsButton).thenReturn(settingsView)
+        `when`(dialog.doneButton).thenReturn(doneView)
+        `when`(dialog.grid).thenReturn(gridView)
+
+        `when`(launchView.context).thenReturn(mContext)
+
+        controller = UserSwitchDialogController(
+                { userDetailViewAdapter },
+                activityStarter,
+                falsingManager,
+                dialogLaunchAnimator,
+                { dialog }
+        )
+    }
+
+    @Test
+    fun showDialog_callsDialogShow() {
+        controller.showDialog(launchView)
+        verify(dialogLaunchAnimator).showFromView(dialog, launchView)
+    }
+
+    @Test
+    fun createCalledBeforeDoneButton() {
+        controller.showDialog(launchView)
+        val inOrder = inOrder(dialog)
+        inOrder.verify(dialog).create()
+        inOrder.verify(dialog).doneButton
+    }
+
+    @Test
+    fun createCalledBeforeSettingsButton() {
+        controller.showDialog(launchView)
+        val inOrder = inOrder(dialog)
+        inOrder.verify(dialog).create()
+        inOrder.verify(dialog).settingsButton
+    }
+
+    @Test
+    fun createCalledBeforeGrid() {
+        controller.showDialog(launchView)
+        val inOrder = inOrder(dialog)
+        inOrder.verify(dialog).create()
+        inOrder.verify(dialog).grid
+    }
+
+    @Test
+    fun dialog_showForAllUsers() {
+        controller.showDialog(launchView)
+        verify(dialog).setShowForAllUsers(true)
+    }
+
+    @Test
+    fun dialog_cancelOnTouchOutside() {
+        controller.showDialog(launchView)
+        verify(dialog).setCanceledOnTouchOutside(true)
+    }
+
+    @Test
+    fun adapterAndGridLinked() {
+        controller.showDialog(launchView)
+        verify(userDetailViewAdapter).linkToViewGroup(gridView)
+    }
+
+    @Test
+    fun clickDoneButton_dismiss() {
+        controller.showDialog(launchView)
+
+        verify(doneView).setOnClickListener(capture(clickCaptor))
+
+        clickCaptor.value.onClick(doneView)
+
+        verify(activityStarter, never()).postStartActivityDismissingKeyguard(any(), anyInt())
+        verify(dialog).dismiss()
+    }
+
+    @Test
+    fun clickSettingsButton_noFalsing_opensSettingsAndDismisses() {
+        `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false)
+
+        controller.showDialog(launchView)
+
+        verify(settingsView).setOnClickListener(capture(clickCaptor))
+
+        clickCaptor.value.onClick(settingsView)
+
+        verify(activityStarter)
+                .postStartActivityDismissingKeyguard(
+                        argThat(IntentMatcher(Settings.ACTION_USER_SETTINGS)),
+                        eq(0)
+                )
+        verify(dialog).dismiss()
+    }
+
+    @Test
+    fun clickSettingsButton_Falsing_notOpensSettingsAndDismisses() {
+        `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true)
+
+        controller.showDialog(launchView)
+
+        verify(settingsView).setOnClickListener(capture(clickCaptor))
+
+        clickCaptor.value.onClick(settingsView)
+
+        verify(activityStarter, never()).postStartActivityDismissingKeyguard(any(), anyInt())
+        verify(dialog).dismiss()
+    }
+
+    @Test
+    fun callbackFromDetailView_dismissesDialog() {
+        val captor = argumentCaptor<Consumer<UserSwitcherController.UserRecord>>()
+
+        controller.showDialog(launchView)
+        verify(userDetailViewAdapter).injectCallback(capture(captor))
+
+        captor.value.accept(mock(UserSwitcherController.UserRecord::class.java))
+
+        verify(dialog).dismiss()
+    }
+
+    private class IntentMatcher(private val action: String) : ArgumentMatcher<Intent> {
+        override fun matches(argument: Intent?): Boolean {
+            return argument?.action == action
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderTest.kt
index e0187bd..bceb928 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderTest.kt
@@ -108,15 +108,6 @@
     }
 
     @Test
-    fun testNullMirrorControllerNotTrackingTouch() {
-        mController.setMirrorControllerAndMirror(null)
-
-        verify(brightnessSliderView, never()).max
-        verify(brightnessSliderView, never()).value
-        verify(brightnessSliderView).setOnDispatchTouchEventListener(isNull())
-    }
-
-    @Test
     fun testNullMirrorNotTrackingTouch() {
         whenever(mirrorController.toggleSlider).thenReturn(null)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt
new file mode 100644
index 0000000..6a68b71
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.shared.animation
+
+import android.graphics.Point
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_90
+import android.view.View
+import android.view.WindowManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class UnfoldMoveFromCenterAnimatorTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var windowManager: WindowManager
+
+    @get:Rule
+    val mockito = MockitoJUnit.rule()
+
+    private lateinit var animator: UnfoldMoveFromCenterAnimator
+
+    @Before
+    fun before() {
+        animator = UnfoldMoveFromCenterAnimator(windowManager)
+    }
+
+    @Test
+    fun testRegisterViewOnTheLeftOfVerticalFold_halfProgress_viewTranslatedToTheRight() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
+        val view = createView(x = 20, width = 10, height = 10)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+
+        animator.onTransitionProgress(0.5f)
+
+        // Positive translationX -> translated to the right
+        // 10x10 view center is 25px from the center,
+        // When progress is 0.5 it should be translated at:
+        // 25 * 0.3 * (1 - 0.5) = 3.75px
+        assertThat(view.translationX).isWithin(0.01f).of(3.75f)
+    }
+
+    @Test
+    fun testRegisterViewOnTheLeftOfVerticalFold_zeroProgress_viewTranslatedToTheRight() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
+        val view = createView(x = 20, width = 10, height = 10)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+
+        animator.onTransitionProgress(0f)
+
+        // Positive translationX -> translated to the right
+        // 10x10 view center is 25px from the center,
+        // When progress is 0 it should be translated at:
+        // 25 * 0.3 * (1 - 0) = 7.5px
+        assertThat(view.translationX).isWithin(0.01f).of(7.5f)
+    }
+
+    @Test
+    fun testRegisterViewOnTheLeftOfVerticalFold_fullProgress_viewTranslatedToTheOriginalPosition() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
+        val view = createView(x = 20, width = 10, height = 10)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+
+        animator.onTransitionProgress(1f)
+
+        // Positive translationX -> translated to the right
+        // 10x10 view center is 25px from the center,
+        // When progress is 1 it should be translated at:
+        // 25 * 0.3 * 0 = 0px
+        assertThat(view.translationX).isEqualTo(0f)
+    }
+
+    @Test
+    fun testViewOnTheLeftOfVerticalFoldWithTranslation_halfProgress_viewTranslatedToTheRight() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
+        val view = createView(x = 20, width = 10, height = 10, translationX = 100f)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+
+        animator.onTransitionProgress(0.5f)
+
+        // Positive translationX -> translated to the right, original translation is ignored
+        // 10x10 view center is 25px from the center,
+        // When progress is 0.5 it should be translated at:
+        // 25 * 0.3 * (1 - 0.5) = 3.75px
+        assertThat(view.translationX).isWithin(0.01f).of(3.75f)
+    }
+
+    @Test
+    fun testRegisterViewAndUnregister_halfProgress_viewIsNotUpdated() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
+        val view = createView(x = 20, width = 10, height = 10)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+        animator.clearRegisteredViews()
+
+        animator.onTransitionProgress(0.5f)
+
+        assertThat(view.translationX).isEqualTo(0f)
+    }
+
+    @Test
+    fun testRegisterViewUpdateProgressAndUnregister_halfProgress_viewIsNotUpdated() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
+        val view = createView(x = 20, width = 10, height = 10)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+        animator.onTransitionProgress(0.2f)
+        animator.clearRegisteredViews()
+
+        animator.onTransitionProgress(0.5f)
+
+        assertThat(view.translationX).isEqualTo(0f)
+    }
+
+    @Test
+    fun testRegisterViewOnTheTopOfHorizontalFold_halfProgress_viewTranslatedToTheBottom() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_90)
+        val view = createView(y = 20, width = 10, height = 10)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+
+        animator.onTransitionProgress(0.5f)
+
+        // Positive translationY -> translated to the bottom
+        assertThat(view.translationY).isWithin(0.01f).of(3.75f)
+    }
+
+    @Test
+    fun testUpdateViewPositions_viewOnTheLeftAndMovedToTheRight_viewTranslatedToTheLeft() {
+        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
+        val view = createView(x = 20)
+        animator.registerViewForAnimation(view)
+        animator.onTransitionStarted()
+        animator.onTransitionProgress(0.5f)
+        view.updateMock(x = 80) // view moved from the left side to the right
+
+        animator.updateViewPositions()
+
+        // Negative translationX -> translated to the left
+        assertThat(view.translationX).isWithin(0.1f).of(-5.25f)
+    }
+
+    private fun createView(
+        x: Int = 0,
+        y: Int = 0,
+        width: Int = 10,
+        height: Int = 10,
+        translationX: Float = 0f,
+        translationY: Float = 0f
+    ): View {
+        val view = spy(View(context))
+        doAnswer {
+            val location = (it.arguments[0] as IntArray)
+            location[0] = x
+            location[1] = y
+            Unit
+        }.`when`(view).getLocationOnScreen(any())
+
+        whenever(view.width).thenReturn(width)
+        whenever(view.height).thenReturn(height)
+
+        view.updateMock(x, y, width, height, translationX, translationY)
+
+        return view
+    }
+
+    private fun View.updateMock(
+        x: Int = 0,
+        y: Int = 0,
+        width: Int = 10,
+        height: Int = 10,
+        translationX: Float = 0f,
+        translationY: Float = 0f
+    ) {
+        doAnswer {
+            val location = (it.arguments[0] as IntArray)
+            location[0] = x
+            location[1] = y
+            Unit
+        }.`when`(this).getLocationOnScreen(any())
+
+        whenever(this.width).thenReturn(width)
+        whenever(this.height).thenReturn(height)
+
+        this.apply {
+            setTranslationX(translationX)
+            setTranslationY(translationY)
+        }
+    }
+
+    private fun givenScreen(
+        width: Int = 100,
+        height: Int = 100,
+        rotation: Int = ROTATION_0
+    ) {
+        val display = mock(Display::class.java)
+        whenever(display.getSize(any())).thenAnswer {
+            val size = (it.arguments[0] as Point)
+            size.set(width, height)
+            Unit
+        }
+        whenever(display.rotation).thenReturn(rotation)
+        whenever(windowManager.defaultDisplay).thenReturn(display)
+
+        animator.updateDisplayProperties()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java
new file mode 100644
index 0000000..05280fa
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.systemui.shared.plugins;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.SysuiTestableContext;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.annotations.Requires;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PluginActionManagerTest extends SysuiTestCase {
+
+    private static final String PRIVILEGED_PACKAGE = "com.android.systemui.shared.plugins";
+    private TestPlugin mMockPlugin;
+
+    private PackageManager mMockPm;
+    private PluginListener<TestPlugin> mMockListener;
+    private PluginActionManager<TestPlugin> mPluginActionManager;
+    private VersionInfo mMockVersionInfo;
+    private PluginEnabler mMockEnabler;
+    ComponentName mTestPluginComponentName =
+            new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName());
+    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+    NotificationManager mNotificationManager;
+    private PluginInstance<TestPlugin> mPluginInstance;
+    private PluginInstance.Factory mPluginInstanceFactory = new PluginInstance.Factory(
+            this.getClass().getClassLoader(),
+            new PluginInstance.InstanceFactory<>(), new PluginInstance.VersionChecker(),
+            Collections.emptyList(), false) {
+        @Override
+        public <T extends Plugin> PluginInstance<T> create(Context context, ApplicationInfo appInfo,
+                ComponentName componentName, Class<T> pluginClass) {
+            return (PluginInstance<T>) mPluginInstance;
+        }
+    };
+
+    private PluginActionManager.Factory mActionManagerFactory;
+
+    @Before
+    public void setup() throws Exception {
+        mContext = new MyContextWrapper(mContext);
+        mMockPm = mock(PackageManager.class);
+        mMockListener = mock(PluginListener.class);
+        mMockEnabler = mock(PluginEnabler.class);
+        mMockVersionInfo = mock(VersionInfo.class);
+        mNotificationManager = mock(NotificationManager.class);
+        mMockPlugin = mock(TestPlugin.class);
+        mPluginInstance = mock(PluginInstance.class);
+        when(mPluginInstance.getComponentName()).thenReturn(mTestPluginComponentName);
+        when(mPluginInstance.getPackage()).thenReturn(mTestPluginComponentName.getPackageName());
+        mActionManagerFactory = new PluginActionManager.Factory(getContext(), mMockPm,
+                mFakeExecutor, mFakeExecutor, mNotificationManager, mMockEnabler,
+                new ArrayList<>(), mPluginInstanceFactory);
+
+        mPluginActionManager = mActionManagerFactory.create("myAction", mMockListener,
+                TestPlugin.class, true, true);
+        when(mMockPlugin.getVersion()).thenReturn(1);
+    }
+
+    @Test
+    public void testNoPlugins() {
+        when(mMockPm.queryIntentServices(any(), anyInt())).thenReturn(
+                Collections.emptyList());
+        mPluginActionManager.loadAll();
+
+        mFakeExecutor.runAllReady();
+
+        verify(mMockListener, never()).onPluginConnected(any(), any());
+    }
+
+    @Test
+    public void testPluginCreate() throws Exception {
+        //Debug.waitForDebugger();
+        createPlugin();
+
+        // Verify startup lifecycle
+        verify(mPluginInstance).onCreate(mContext, mMockListener);
+    }
+
+    @Test
+    public void testPluginDestroy() throws Exception {
+        createPlugin(); // Get into valid created state.
+
+        mPluginActionManager.destroy();
+
+        mFakeExecutor.runAllReady();
+
+        // Verify shutdown lifecycle
+        verify(mPluginInstance).onDestroy(mMockListener);
+    }
+
+    @Test
+    public void testReloadOnChange() throws Exception {
+        createPlugin(); // Get into valid created state.
+
+        mPluginActionManager.reloadPackage(PRIVILEGED_PACKAGE);
+
+        mFakeExecutor.runAllReady();
+
+        // Verify the old one was destroyed.
+        verify(mPluginInstance).onDestroy(mMockListener);
+        verify(mPluginInstance, Mockito.times(2))
+                .onCreate(mContext, mMockListener);
+    }
+
+    @Test
+    public void testNonDebuggable() throws Exception {
+        // Create a version that thinks the build is not debuggable.
+        mPluginActionManager = mActionManagerFactory.create("myAction", mMockListener,
+                TestPlugin.class, true, false);
+        setupFakePmQuery();
+
+        mPluginActionManager.loadAll();
+
+        mFakeExecutor.runAllReady();
+
+        // Non-debuggable build should receive no plugins.
+        verify(mMockListener, never()).onPluginConnected(any(), any());
+    }
+
+    @Test
+    public void testNonDebuggable_privileged() throws Exception {
+        // Create a version that thinks the build is not debuggable.
+        PluginActionManager.Factory factory = new PluginActionManager.Factory(getContext(),
+                mMockPm, mFakeExecutor, mFakeExecutor, mNotificationManager,
+                mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE),
+                mPluginInstanceFactory);
+        mPluginActionManager = factory.create("myAction", mMockListener,
+                TestPlugin.class, true, false);
+        setupFakePmQuery();
+
+        mPluginActionManager.loadAll();
+
+        mFakeExecutor.runAllReady();
+
+        // Verify startup lifecycle
+        verify(mPluginInstance).onCreate(mContext, mMockListener);
+    }
+
+    @Test
+    public void testCheckAndDisable() throws Exception {
+        createPlugin(); // Get into valid created state.
+
+        // Start with an unrelated class.
+        boolean result = mPluginActionManager.checkAndDisable(Activity.class.getName());
+        assertFalse(result);
+        verify(mMockEnabler, never()).setDisabled(any(ComponentName.class), anyInt());
+
+        // Now hand it a real class and make sure it disables the plugin.
+        result = mPluginActionManager.checkAndDisable(TestPlugin.class.getName());
+        assertTrue(result);
+        verify(mMockEnabler).setDisabled(
+                mTestPluginComponentName, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
+    }
+
+    @Test
+    public void testDisableAll() throws Exception {
+        createPlugin(); // Get into valid created state.
+
+        mPluginActionManager.disableAll();
+
+        verify(mMockEnabler).setDisabled(
+                mTestPluginComponentName, PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
+    }
+
+    @Test
+    public void testDisablePrivileged() throws Exception {
+        PluginActionManager.Factory factory = new PluginActionManager.Factory(getContext(),
+                mMockPm, mFakeExecutor, mFakeExecutor, mNotificationManager,
+                mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE),
+                mPluginInstanceFactory);
+        mPluginActionManager = factory.create("myAction", mMockListener,
+                TestPlugin.class, true, false);
+
+        createPlugin(); // Get into valid created state.
+
+        mPluginActionManager.disableAll();
+
+        verify(mMockPm, never()).setComponentEnabledSetting(
+                ArgumentCaptor.forClass(ComponentName.class).capture(),
+                ArgumentCaptor.forClass(int.class).capture(),
+                ArgumentCaptor.forClass(int.class).capture());
+    }
+
+    private void setupFakePmQuery() throws Exception {
+        List<ResolveInfo> list = new ArrayList<>();
+        ResolveInfo info = new ResolveInfo();
+        info.serviceInfo = mock(ServiceInfo.class);
+        info.serviceInfo.packageName = mTestPluginComponentName.getPackageName();
+        info.serviceInfo.name = mTestPluginComponentName.getClassName();
+        when(info.serviceInfo.loadLabel(any())).thenReturn("Test Plugin");
+        list.add(info);
+        when(mMockPm.queryIntentServices(any(), Mockito.anyInt())).thenReturn(list);
+        when(mMockPm.getServiceInfo(any(), anyInt())).thenReturn(info.serviceInfo);
+
+        when(mMockPm.checkPermission(Mockito.anyString(), Mockito.anyString())).thenReturn(
+                PackageManager.PERMISSION_GRANTED);
+
+        when(mMockPm.getApplicationInfo(Mockito.anyString(), anyInt())).thenAnswer(
+                (Answer<ApplicationInfo>) invocation -> {
+                    ApplicationInfo appInfo = getContext().getApplicationInfo();
+                    appInfo.packageName = invocation.getArgument(0);
+                    return appInfo;
+                });
+        when(mMockEnabler.isEnabled(mTestPluginComponentName)).thenReturn(true);
+    }
+
+    private void createPlugin() throws Exception {
+        setupFakePmQuery();
+
+        mPluginActionManager.loadAll();
+
+        mFakeExecutor.runAllReady();
+    }
+
+    // Real context with no registering/unregistering of receivers.
+    private static class MyContextWrapper extends SysuiTestableContext {
+        MyContextWrapper(Context base) {
+            super(base);
+        }
+
+        @Override
+        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+            return null;
+        }
+
+        @Override
+        public void unregisterReceiver(BroadcastReceiver receiver) {
+        }
+
+        @Override
+        public void sendBroadcast(Intent intent) {
+            // Do nothing.
+        }
+    }
+
+    // This target class doesn't matter, it just needs to have a Requires to hit the flow where
+    // the mock version info is called.
+    @Requires(target = PluginManagerTest.class, version = 1)
+    public static class TestPlugin implements Plugin {
+        @Override
+        public int getVersion() {
+            return 1;
+        }
+
+        @Override
+        public void onCreate(Context sysuiContext, Context pluginContext) {
+        }
+
+        @Override
+        public void onDestroy() {
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceManagerTest.java
deleted file mode 100644
index 325d540..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceManagerTest.java
+++ /dev/null
@@ -1,338 +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.systemui.shared.plugins;
-
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.Activity;
-import android.app.NotificationManager;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.os.HandlerThread;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.Plugin;
-import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.plugins.annotations.Requires;
-import com.android.systemui.shared.plugins.PluginInstanceManager.PluginInfo;
-import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class PluginInstanceManagerTest extends SysuiTestCase {
-
-    private static final String WHITELISTED_PACKAGE = "com.android.systemui";
-    // Static since the plugin needs to be generated by the PluginInstanceManager using newInstance.
-    private static Plugin sMockPlugin;
-
-    private HandlerThread mHandlerThread;
-    private Context mContextWrapper;
-    private PackageManager mMockPm;
-    private PluginListener mMockListener;
-    private PluginInstanceManager mPluginInstanceManager;
-    private PluginManagerImpl mMockManager;
-    private VersionInfo mMockVersionInfo;
-    private PluginEnabler mMockEnabler;
-    ComponentName mTestPluginComponentName =
-            new ComponentName(WHITELISTED_PACKAGE, TestPlugin.class.getName());
-
-    @Before
-    public void setup() throws Exception {
-        mHandlerThread = new HandlerThread("test_thread");
-        mHandlerThread.start();
-        mContextWrapper = new MyContextWrapper(getContext());
-        mMockPm = mock(PackageManager.class);
-        mMockListener = mock(PluginListener.class);
-        mMockManager = mock(PluginManagerImpl.class);
-        when(mMockManager.getClassLoader(any())).thenReturn(getClass().getClassLoader());
-        mMockEnabler = mock(PluginEnabler.class);
-        when(mMockManager.getPluginEnabler()).thenReturn(mMockEnabler);
-        mMockVersionInfo = mock(VersionInfo.class);
-        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
-                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
-                mMockManager, true, new String[0]);
-        sMockPlugin = mock(Plugin.class);
-        when(sMockPlugin.getVersion()).thenReturn(1);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mHandlerThread.quit();
-        sMockPlugin = null;
-    }
-
-    @UiThreadTest
-    @Test
-    public void testGetPlugin() throws Exception {
-        setupFakePmQuery();
-        PluginInfo p = mPluginInstanceManager.getPlugin();
-        assertNotNull(p.mPlugin);
-        verify(sMockPlugin).onCreate(any(), any());
-    }
-
-    @Test
-    public void testNoPlugins() throws Exception {
-        when(mMockPm.queryIntentServices(any(), anyInt())).thenReturn(
-                Collections.emptyList());
-        mPluginInstanceManager.loadAll();
-
-        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
-        waitForIdleSync(mPluginInstanceManager.mMainHandler);
-
-        verify(mMockListener, never()).onPluginConnected(any(), any());
-    }
-
-    @Test
-    public void testPluginCreate() throws Exception {
-        createPlugin();
-
-        // Verify startup lifecycle
-        verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
-                ArgumentCaptor.forClass(Context.class).capture());
-        verify(mMockListener).onPluginConnected(any(), any());
-    }
-
-    @Test
-    public void testPluginDestroy() throws Exception {
-        createPlugin(); // Get into valid created state.
-
-        mPluginInstanceManager.destroy();
-
-        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
-        waitForIdleSync(mPluginInstanceManager.mMainHandler);
-
-        // Verify shutdown lifecycle
-        verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
-        verify(sMockPlugin).onDestroy();
-    }
-
-    @Test
-    public void testIncorrectVersion() throws Exception {
-        NotificationManager nm = mock(NotificationManager.class);
-        mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm);
-        setupFakePmQuery();
-        doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any());
-
-        mPluginInstanceManager.loadAll();
-
-        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
-        waitForIdleSync(mPluginInstanceManager.mMainHandler);
-
-        // Plugin shouldn't be connected because it is the wrong version.
-        verify(mMockListener, never()).onPluginConnected(any(), any());
-        verify(nm).notify(eq(SystemMessage.NOTE_PLUGIN), any());
-    }
-
-    @Test
-    public void testReloadOnChange() throws Exception {
-        createPlugin(); // Get into valid created state.
-
-        mPluginInstanceManager.onPackageChange("com.android.systemui");
-
-        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
-        waitForIdleSync(mPluginInstanceManager.mMainHandler);
-
-        // Verify the old one was destroyed.
-        verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
-        verify(sMockPlugin).onDestroy();
-        // Also verify we got a second onCreate.
-        verify(sMockPlugin, Mockito.times(2)).onCreate(
-                ArgumentCaptor.forClass(Context.class).capture(),
-                ArgumentCaptor.forClass(Context.class).capture());
-        verify(mMockListener, Mockito.times(2)).onPluginConnected(any(), any());
-    }
-
-    @Test
-    public void testNonDebuggable() throws Exception {
-        // Create a version that thinks the build is not debuggable.
-        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
-                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
-                mMockManager, false, new String[0]);
-        setupFakePmQuery();
-
-        mPluginInstanceManager.loadAll();
-
-        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
-        waitForIdleSync(mPluginInstanceManager.mMainHandler);;
-
-        // Non-debuggable build should receive no plugins.
-        verify(mMockListener, never()).onPluginConnected(any(), any());
-    }
-
-    @Test
-    public void testNonDebuggable_whitelist() throws Exception {
-        // Create a version that thinks the build is not debuggable.
-        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
-                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
-                mMockManager, false, new String[] {WHITELISTED_PACKAGE});
-        setupFakePmQuery();
-
-        mPluginInstanceManager.loadAll();
-
-        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
-        waitForIdleSync(mPluginInstanceManager.mMainHandler);
-
-        // Verify startup lifecycle
-        verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
-                ArgumentCaptor.forClass(Context.class).capture());
-        verify(mMockListener).onPluginConnected(any(), any());
-    }
-
-    @Test
-    public void testCheckAndDisable() throws Exception {
-        createPlugin(); // Get into valid created state.
-
-        // Start with an unrelated class.
-        boolean result = mPluginInstanceManager.checkAndDisable(Activity.class.getName());
-        assertFalse(result);
-        verify(mMockEnabler, never()).setDisabled(any(ComponentName.class), anyInt());
-
-        // Now hand it a real class and make sure it disables the plugin.
-        result = mPluginInstanceManager.checkAndDisable(TestPlugin.class.getName());
-        assertTrue(result);
-        verify(mMockEnabler).setDisabled(
-                mTestPluginComponentName, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
-    }
-
-    @Test
-    public void testDisableAll() throws Exception {
-        createPlugin(); // Get into valid created state.
-
-        mPluginInstanceManager.disableAll();
-
-        verify(mMockEnabler).setDisabled(
-                mTestPluginComponentName, PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
-    }
-
-    @Test
-    public void testDisableWhitelisted() throws Exception {
-        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
-                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
-                mMockManager, false, new String[] {WHITELISTED_PACKAGE});
-        createPlugin(); // Get into valid created state.
-
-        mPluginInstanceManager.disableAll();
-
-        verify(mMockPm, never()).setComponentEnabledSetting(
-                ArgumentCaptor.forClass(ComponentName.class).capture(),
-                ArgumentCaptor.forClass(int.class).capture(),
-                ArgumentCaptor.forClass(int.class).capture());
-    }
-
-    private void setupFakePmQuery() throws Exception {
-        List<ResolveInfo> list = new ArrayList<>();
-        ResolveInfo info = new ResolveInfo();
-        info.serviceInfo = mock(ServiceInfo.class);
-        info.serviceInfo.packageName = mTestPluginComponentName.getPackageName();
-        info.serviceInfo.name = mTestPluginComponentName.getClassName();
-        when(info.serviceInfo.loadLabel(any())).thenReturn("Test Plugin");
-        list.add(info);
-        when(mMockPm.queryIntentServices(any(), Mockito.anyInt())).thenReturn(list);
-        when(mMockPm.getServiceInfo(any(), anyInt())).thenReturn(info.serviceInfo);
-
-        when(mMockPm.checkPermission(Mockito.anyString(), Mockito.anyString())).thenReturn(
-                PackageManager.PERMISSION_GRANTED);
-
-        ApplicationInfo appInfo = getContext().getApplicationInfo();
-        when(mMockPm.getApplicationInfo(Mockito.anyString(), Mockito.anyInt())).thenReturn(
-                appInfo);
-        when(mMockEnabler.isEnabled(mTestPluginComponentName)).thenReturn(true);
-    }
-
-    private void createPlugin() throws Exception {
-        setupFakePmQuery();
-
-        mPluginInstanceManager.loadAll();
-
-        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
-        waitForIdleSync(mPluginInstanceManager.mMainHandler);
-    }
-
-    // Real context with no registering/unregistering of receivers.
-    private static class MyContextWrapper extends ContextWrapper {
-        public MyContextWrapper(Context base) {
-            super(base);
-        }
-
-        @Override
-        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
-            return null;
-        }
-
-        @Override
-        public void unregisterReceiver(BroadcastReceiver receiver) {
-        }
-
-        @Override
-        public void sendBroadcast(Intent intent) {
-            // Do nothing.
-        }
-    }
-
-    // This target class doesn't matter, it just needs to have a Requires to hit the flow where
-    // the mock version info is called.
-    @Requires(target = PluginManagerTest.class, version = 1)
-    public static class TestPlugin implements Plugin {
-        @Override
-        public int getVersion() {
-            return sMockPlugin.getVersion();
-        }
-
-        @Override
-        public void onCreate(Context sysuiContext, Context pluginContext) {
-            sMockPlugin.onCreate(sysuiContext, pluginContext);
-        }
-
-        @Override
-        public void onDestroy() {
-            sMockPlugin.onDestroy();
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
new file mode 100644
index 0000000..bb9a1e9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.plugins;
+
+import static junit.framework.Assert.assertNotNull;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.annotations.Requires;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PluginInstanceTest extends SysuiTestCase {
+
+    private static final String PRIVILEGED_PACKAGE = "com.android.systemui.plugins";
+
+    @Mock
+    private TestPluginImpl mMockPlugin;
+    @Mock
+    private PluginListener<TestPlugin> mMockListener;
+    @Mock
+    private VersionInfo mVersionInfo;
+    ComponentName mTestPluginComponentName =
+            new ComponentName(PRIVILEGED_PACKAGE, TestPluginImpl.class.getName());
+    private PluginInstance<TestPlugin> mPluginInstance;
+    private PluginInstance.Factory mPluginInstanceFactory;
+
+    private ApplicationInfo mAppInfo;
+    private Context mPluginContext;
+    @Mock
+    private PluginInstance.VersionChecker mVersionChecker;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mAppInfo = mContext.getApplicationInfo();
+        mAppInfo.packageName = mTestPluginComponentName.getPackageName();
+        when(mVersionChecker.checkVersion(any(), any(), any())).thenReturn(mVersionInfo);
+
+        mPluginInstanceFactory = new PluginInstance.Factory(
+                this.getClass().getClassLoader(),
+                new PluginInstance.InstanceFactory<TestPlugin>() {
+                    @Override
+                    TestPlugin create(Class cls) {
+                        return mMockPlugin;
+                    }
+                },
+                mVersionChecker,
+                Collections.singletonList(PRIVILEGED_PACKAGE),
+                false);
+
+        mPluginInstance = mPluginInstanceFactory.create(
+                mContext, mAppInfo, mTestPluginComponentName, TestPlugin.class);
+        mPluginContext = mPluginInstance.getPluginContext();
+    }
+
+    @Test
+    public void testCorrectVersion() {
+        assertNotNull(mPluginInstance);
+    }
+
+    @Test(expected = VersionInfo.InvalidVersionException.class)
+    public void testIncorrectVersion() throws Exception {
+
+        ComponentName wrongVersionTestPluginComponentName =
+                new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName());
+
+        when(mVersionChecker.checkVersion(any(), any(), any())).thenThrow(
+                new VersionInfo.InvalidVersionException("test", true));
+
+        mPluginInstanceFactory.create(
+                mContext, mAppInfo, wrongVersionTestPluginComponentName, TestPlugin.class);
+    }
+
+    @Test
+    public void testOnCreate() {
+        mPluginInstance.onCreate(mContext, mMockListener);
+        verify(mMockPlugin).onCreate(mContext, mPluginContext);
+        verify(mMockListener).onPluginConnected(mMockPlugin, mPluginContext);
+    }
+
+    @Test
+    public void testOnDestroy() {
+        mPluginInstance.onDestroy(mMockListener);
+        verify(mMockListener).onPluginDisconnected(mMockPlugin);
+        verify(mMockPlugin).onDestroy();
+    }
+
+    // This target class doesn't matter, it just needs to have a Requires to hit the flow where
+    // the mock version info is called.
+    @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
+    public interface TestPlugin extends Plugin {
+        int VERSION = 1;
+        String ACTION = "testAction";
+    }
+
+    @Requires(target = TestPlugin.class, version = TestPlugin.VERSION)
+    public static class TestPluginImpl implements TestPlugin {
+        @Override
+        public void onCreate(Context sysuiContext, Context pluginContext) {
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java
index 536c043..1eadd52 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java
@@ -13,10 +13,9 @@
  */
 package com.android.systemui.shared.plugins;
 
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -30,19 +29,13 @@
 import android.net.Uri;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.Plugin;
-import com.android.systemui.plugins.PluginEnablerImpl;
-import com.android.systemui.plugins.PluginInitializerImpl;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.annotations.ProvidesInterface;
-import com.android.systemui.shared.plugins.PluginInstanceManager.PluginInfo;
-import com.android.systemui.shared.plugins.PluginManagerImpl.PluginInstanceManagerFactory;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,19 +44,24 @@
 import org.mockito.Mockito;
 
 import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Optional;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 public class PluginManagerTest extends SysuiTestCase {
 
-    private static final String WHITELISTED_PACKAGE = "com.android.systemui";
+    private static final String PRIVILEGED_PACKAGE = "com.android.systemui";
 
-    private PluginInstanceManagerFactory mMockFactory;
-    private PluginInstanceManager mMockPluginInstance;
+    private PluginActionManager.Factory mMockFactory;
+    private PluginActionManager<TestPlugin> mMockPluginInstance;
     private PluginManagerImpl mPluginManager;
-    private PluginListener mMockListener;
+    private PluginListener<TestPlugin> mMockListener;
     private PackageManager mMockPackageManager;
+    private PluginEnabler mPluginEnabler;
+    private PluginPrefs mPluginPrefs;
 
     private UncaughtExceptionHandler mRealExceptionHandler;
     private UncaughtExceptionHandler mMockExceptionHandler;
@@ -71,44 +69,25 @@
 
     @Before
     public void setup() throws Exception {
-        mDependency.injectTestDependency(Dependency.BG_LOOPER,
-                TestableLooper.get(this).getLooper());
         mRealExceptionHandler = Thread.getUncaughtExceptionPreHandler();
         mMockExceptionHandler = mock(UncaughtExceptionHandler.class);
-        mMockFactory = mock(PluginInstanceManagerFactory.class);
-        mMockPluginInstance = mock(PluginInstanceManager.class);
-        when(mMockFactory.createPluginInstanceManager(Mockito.any(), Mockito.any(), Mockito.any(),
-                Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()))
+        mMockFactory = mock(PluginActionManager.Factory.class);
+        mMockPluginInstance = mock(PluginActionManager.class);
+        mPluginEnabler = mock(PluginEnabler.class);
+        mPluginPrefs = mock(PluginPrefs.class);
+        when(mMockFactory.create(any(), any(), eq(TestPlugin.class), anyBoolean(), anyBoolean()))
                 .thenReturn(mMockPluginInstance);
 
         mMockPackageManager = mock(PackageManager.class);
         mPluginManager = new PluginManagerImpl(
                 getContext(), mMockFactory, true,
-                mMockExceptionHandler, new PluginInitializerImpl() {
-                    @Override
-                    public String[] getWhitelistedPlugins(Context context) {
-                        return new String[0];
-                    }
+                Optional.of(mMockExceptionHandler), mPluginEnabler,
+                mPluginPrefs, new ArrayList<>());
 
-                    @Override
-                    public PluginEnabler getPluginEnabler(Context context) {
-                        return new PluginEnablerImpl(context, mMockPackageManager);
-                    }
-        });
         resetExceptionHandler();
         mMockListener = mock(PluginListener.class);
     }
 
-    @RunWithLooper(setAsMainLooper = true)
-    @Test
-    public void testOneShot() {
-        Plugin mockPlugin = mock(Plugin.class);
-        when(mMockPluginInstance.getPlugin()).thenReturn(new PluginInfo(null, null, mockPlugin,
-                null, null));
-        Plugin result = mPluginManager.getOneShotPlugin("myAction", TestPlugin.class);
-        assertSame(mockPlugin, result);
-    }
-
     @Test
     public void testAddListener() {
         mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
@@ -126,53 +105,49 @@
 
     @Test
     @RunWithLooper(setAsMainLooper = true)
-    public void testNonDebuggable_noWhitelist() {
-        mPluginManager = new PluginManagerImpl(getContext(), mMockFactory, false,
-                mMockExceptionHandler, new PluginInitializerImpl() {
-            @Override
-            public String[] getWhitelistedPlugins(Context context) {
-                return new String[0];
-            }
-        });
+    public void testNonDebuggable_nonPrivileged() {
+        mPluginManager = new PluginManagerImpl(
+                getContext(), mMockFactory, false,
+                Optional.of(mMockExceptionHandler), mPluginEnabler,
+                mPluginPrefs, new ArrayList<>());
         resetExceptionHandler();
 
         String sourceDir = "myPlugin";
         ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.sourceDir = sourceDir;
-        applicationInfo.packageName = WHITELISTED_PACKAGE;
+        applicationInfo.packageName = PRIVILEGED_PACKAGE;
         mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
-        assertNull(mPluginManager.getOneShotPlugin(sourceDir, TestPlugin.class));
-        assertNull(mPluginManager.getClassLoader(applicationInfo));
+        verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(TestPlugin.class),
+                eq(false), eq(false));
+        verify(mMockPluginInstance).loadAll();
     }
 
     @Test
     @RunWithLooper(setAsMainLooper = true)
-    public void testNonDebuggable_whitelistedPkg() {
-        mPluginManager = new PluginManagerImpl(getContext(), mMockFactory, false,
-                mMockExceptionHandler, new PluginInitializerImpl() {
-            @Override
-            public String[] getWhitelistedPlugins(Context context) {
-                return new String[] {WHITELISTED_PACKAGE};
-            }
-        });
+    public void testNonDebuggable_privilegedPackage() {
+        mPluginManager = new PluginManagerImpl(
+                getContext(), mMockFactory, false,
+                Optional.of(mMockExceptionHandler), mPluginEnabler,
+                mPluginPrefs, Collections.singletonList(PRIVILEGED_PACKAGE));
         resetExceptionHandler();
 
         String sourceDir = "myPlugin";
-        ApplicationInfo whiteListedApplicationInfo = new ApplicationInfo();
-        whiteListedApplicationInfo.sourceDir = sourceDir;
-        whiteListedApplicationInfo.packageName = WHITELISTED_PACKAGE;
+        ApplicationInfo privilegedApplicationInfo = new ApplicationInfo();
+        privilegedApplicationInfo.sourceDir = sourceDir;
+        privilegedApplicationInfo.packageName = PRIVILEGED_PACKAGE;
         ApplicationInfo invalidApplicationInfo = new ApplicationInfo();
         invalidApplicationInfo.sourceDir = sourceDir;
         invalidApplicationInfo.packageName = "com.android.invalidpackage";
         mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
-        assertNotNull(mPluginManager.getClassLoader(whiteListedApplicationInfo));
-        assertNull(mPluginManager.getClassLoader(invalidApplicationInfo));
+        verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(TestPlugin.class),
+                eq(false), eq(false));
+        verify(mMockPluginInstance).loadAll();
     }
 
     @Test
     public void testExceptionHandler_foundPlugin() {
         mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
-        when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(true);
+        when(mMockPluginInstance.checkAndDisable(any())).thenReturn(true);
 
         mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
 
@@ -187,7 +162,7 @@
     @Test
     public void testExceptionHandler_noFoundPlugin() {
         mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
-        when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(false);
+        when(mMockPluginInstance.checkAndDisable(any())).thenReturn(false);
 
         mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
 
@@ -211,9 +186,7 @@
         intent.setData(Uri.parse("package://" + testComponent.flattenToString()));
         mPluginManager.onReceive(mContext, intent);
         verify(nm).cancel(eq(testComponent.getClassName()), eq(SystemMessage.NOTE_PLUGIN));
-        verify(mMockPackageManager).setComponentEnabledSetting(eq(testComponent),
-                eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
-                eq(PackageManager.DONT_KILL_APP));
+        verify(mPluginEnabler).setDisabled(testComponent, PluginEnabler.DISABLED_INVALID_VERSION);
     }
 
     private void resetExceptionHandler() {
@@ -223,8 +196,8 @@
     }
 
     @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
-    public static interface TestPlugin extends Plugin {
-        public static final String ACTION = "testAction";
-        public static final int VERSION = 1;
+    public interface TestPlugin extends Plugin {
+        String ACTION = "testAction";
+        int VERSION = 1;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index f3762c5..af624ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -15,6 +15,8 @@
 package com.android.systemui.statusbar;
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
+import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
@@ -33,6 +35,7 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.fingerprint.IUdfpsHbmListener;
 import android.os.Bundle;
+import android.view.InsetsVisibilities;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -124,24 +127,25 @@
     public void testOnSystemBarAttributesChanged() {
         doTestOnSystemBarAttributesChanged(DEFAULT_DISPLAY, 1,
                 new AppearanceRegion[]{new AppearanceRegion(2, new Rect())}, false,
-                BEHAVIOR_DEFAULT, false);
+                BEHAVIOR_DEFAULT, new InsetsVisibilities(), "test");
     }
 
     @Test
     public void testOnSystemBarAttributesChangedForSecondaryDisplay() {
         doTestOnSystemBarAttributesChanged(SECONDARY_DISPLAY, 1,
                 new AppearanceRegion[]{new AppearanceRegion(2, new Rect())}, false,
-                BEHAVIOR_DEFAULT, false);
+                BEHAVIOR_DEFAULT, new InsetsVisibilities(), "test");
     }
 
     private void doTestOnSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, boolean isFullscreen) {
+            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName) {
         mCommandQueue.onSystemBarAttributesChanged(displayId, appearance, appearanceRegions,
-                navbarColorManagedByIme, behavior, isFullscreen);
+                navbarColorManagedByIme, behavior, requestedVisibilities, packageName);
         waitForIdleSync();
         verify(mCallbacks).onSystemBarAttributesChanged(eq(displayId), eq(appearance),
-                eq(appearanceRegions), eq(navbarColorManagedByIme), eq(behavior), eq(isFullscreen));
+                eq(appearanceRegions), eq(navbarColorManagedByIme), eq(behavior),
+                eq(requestedVisibilities), eq(packageName));
     }
 
     @Test
@@ -186,8 +190,13 @@
 
     @Test
     public void testShowImeButtonForSecondaryDisplay() {
+        // First show in default display to update the "last updated ime display"
+        testShowImeButton();
+
         mCommandQueue.setImeWindowStatus(SECONDARY_DISPLAY, null, 1, 2, true, false);
         waitForIdleSync();
+        verify(mCallbacks).setImeWindowStatus(eq(DEFAULT_DISPLAY), eq(null), eq(IME_INVISIBLE),
+                eq(BACK_DISPOSITION_DEFAULT), eq(false));
         verify(mCallbacks).setImeWindowStatus(
                 eq(SECONDARY_DISPLAY), eq(null), eq(1), eq(2), eq(true));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/DisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DisableFlagsLoggerTest.kt
new file mode 100644
index 0000000..096efad
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DisableFlagsLoggerTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+@SmallTest
+class DisableFlagsLoggerTest : SysuiTestCase() {
+    private val disable1Flags = listOf(
+            DisableFlagsLogger.DisableFlag(0b100, 'A', 'a'),
+            DisableFlagsLogger.DisableFlag(0b010, 'B', 'b'),
+            DisableFlagsLogger.DisableFlag(0b001, 'C', 'c'),
+    )
+    private val disable2Flags = listOf(
+            DisableFlagsLogger.DisableFlag(0b10, 'M', 'm'),
+            DisableFlagsLogger.DisableFlag(0b01, 'N', 'n'),
+    )
+
+    private val disableFlagsLogger = DisableFlagsLogger(disable1Flags, disable2Flags)
+
+    @Test
+    fun getDisableFlagsString_oldAndNewSame_statesLoggedButDiffsNotLogged() {
+        val state = DisableFlagsLogger.DisableState(
+                0b111, // ABC
+                0b01 // mN
+        )
+
+        val result = disableFlagsLogger.getDisableFlagsString(state, state)
+
+        assertThat(result).contains("Old: ABC.mN")
+        assertThat(result).contains("New: ABC.mN")
+        assertThat(result).doesNotContain("(")
+        assertThat(result).doesNotContain(")")
+    }
+
+    @Test
+    fun getDisableFlagsString_oldAndNewDifferent_statesAndDiffLogged() {
+        val result = disableFlagsLogger.getDisableFlagsString(
+                DisableFlagsLogger.DisableState(
+                        0b111, // ABC
+                        0b01, // mN
+                ),
+                DisableFlagsLogger.DisableState(
+                        0b001, // abC
+                        0b10 // Mn
+                )
+        )
+
+        assertThat(result).contains("Old: ABC.mN")
+        assertThat(result).contains("New: abC.Mn")
+        assertThat(result).contains("(ab.Mn)")
+    }
+
+    @Test
+    fun getDisableFlagsString_onlyDisable2Different_diffLoggedCorrectly() {
+        val result = disableFlagsLogger.getDisableFlagsString(
+                DisableFlagsLogger.DisableState(
+                        0b001, // abC
+                        0b01, // mN
+                ),
+                DisableFlagsLogger.DisableState(
+                        0b001, // abC
+                        0b00 // mn
+                )
+        )
+
+        assertThat(result).contains("(.n)")
+    }
+
+    @Test
+    fun getDisableFlagsString_nullLocalModification_localModNotLogged() {
+        val result = disableFlagsLogger.getDisableFlagsString(
+                DisableFlagsLogger.DisableState(0, 0),
+                DisableFlagsLogger.DisableState(1, 1),
+                newAfterLocalModification = null
+        )
+
+        assertThat(result).doesNotContain("local modification")
+    }
+
+    @Test
+    fun getDisableFlagsString_newAfterLocalModificationSameAsNew_localModNotLogged() {
+        val newState =  DisableFlagsLogger.DisableState(
+                0b001, // abC
+                0b10 // mn
+        )
+
+        val result = disableFlagsLogger.getDisableFlagsString(
+                DisableFlagsLogger.DisableState(0, 0), newState, newState
+        )
+
+        assertThat(result).doesNotContain("local modification")
+    }
+
+    @Test
+    fun getDisableFlagsString_newAfterLocalModificationDifferent_localModAndDiffLogged() {
+        val result = disableFlagsLogger.getDisableFlagsString(
+                old = DisableFlagsLogger.DisableState(0, 0),
+                new = DisableFlagsLogger.DisableState(
+                        0b000, // abc
+                        0b00 // mn
+                ),
+                newAfterLocalModification = DisableFlagsLogger.DisableState(
+                        0b100, // Abc
+                        0b10 // Mn
+                )
+        )
+
+        assertThat(result).contains("local modification: Abc.Mn (A.M)")
+    }
+
+    @Test
+    fun constructor_defaultDisableFlags_noException() {
+        // Just creating the logger with the default params will trigger the exception if there
+        // is one.
+        DisableFlagsLogger()
+    }
+
+    @Test
+    fun constructor_disable1_FlagIsSetSymbolNotUnique_exception() {
+        assertThrows(IllegalArgumentException::class.java) {
+            DisableFlagsLogger(
+                    disable1FlagsList = listOf(
+                            DisableFlagsLogger.DisableFlag(0b100, 'A', 'a'),
+                            DisableFlagsLogger.DisableFlag(0b010, 'A', 'b'),
+                    ),
+                    listOf()
+            )
+        }
+    }
+
+    @Test
+    fun constructor_disable1_FlagNotSetSymbolNotUnique_exception() {
+        assertThrows(IllegalArgumentException::class.java) {
+            DisableFlagsLogger(
+                    disable1FlagsList = listOf(
+                            DisableFlagsLogger.DisableFlag(0b100, 'A', 'a'),
+                            DisableFlagsLogger.DisableFlag(0b010, 'B', 'a'),
+                    ),
+                    listOf()
+            )
+        }
+    }
+
+    @Test
+    fun constructor_disable2_FlagIsSetSymbolNotUnique_exception() {
+        assertThrows(IllegalArgumentException::class.java) {
+            DisableFlagsLogger(
+                    disable1FlagsList = listOf(),
+                    disable2FlagsList = listOf(
+                            DisableFlagsLogger.DisableFlag(0b100, 'A', 'a'),
+                            DisableFlagsLogger.DisableFlag(0b010, 'A', 'b'),
+                    ),
+            )
+        }
+    }
+
+    @Test
+    fun constructor_disable2_FlagNotSetSymbolNotUnique_exception() {
+        assertThrows(IllegalArgumentException::class.java) {
+            DisableFlagsLogger(
+                    disable1FlagsList = listOf(),
+                    disable2FlagsList = listOf(
+                            DisableFlagsLogger.DisableFlag(0b100, 'A', 'a'),
+                            DisableFlagsLogger.DisableFlag(0b010, 'B', 'a'),
+                    ),
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index f5cab1d..01f7fae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -166,7 +166,7 @@
     private BroadcastReceiver mBroadcastReceiver;
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
 
-    private KeyguardIndicationTextView mTextView;
+    private KeyguardIndicationTextView mTextView; // AOD text
 
     private KeyguardIndicationController mController;
     private WakeLockFake.Builder mWakeLockBuilder;
@@ -412,41 +412,32 @@
 
     @Test
     public void transientIndication_holdsWakeLock_whenDozing() {
+        // GIVEN animations are enabled and text is visible
+        mTextView.setAnimationsEnabled(true);
         createController();
+        mController.setVisible(true);
 
+        // WHEN transient text is shown
         mStatusBarStateListener.onDozingChanged(true);
         mController.showTransientIndication("Test");
 
-        assertTrue(mWakeLock.isHeld());
+        // THEN wake lock is held while the animation is running
+        assertTrue("WakeLock expected: HELD, was: RELEASED", mWakeLock.isHeld());
     }
 
     @Test
-    public void transientIndication_releasesWakeLock_afterHiding() {
+    public void transientIndication_releasesWakeLock_whenDozing() {
+        // GIVEN animations aren't enabled
+        mTextView.setAnimationsEnabled(false);
         createController();
+        mController.setVisible(true);
 
+        // WHEN we show the transient indication
         mStatusBarStateListener.onDozingChanged(true);
         mController.showTransientIndication("Test");
-        mController.hideTransientIndication();
 
-        assertFalse(mWakeLock.isHeld());
-    }
-
-    @Test
-    public void transientIndication_releasesWakeLock_afterHidingDelayed() throws Throwable {
-        mInstrumentation.runOnMainSync(() -> {
-            createController();
-
-            mStatusBarStateListener.onDozingChanged(true);
-            mController.showTransientIndication("Test");
-            mController.hideTransientIndicationDelayed(0);
-        });
-        mInstrumentation.waitForIdleSync();
-
-        Boolean[] held = new Boolean[1];
-        mInstrumentation.runOnMainSync(() -> {
-            held[0] = mWakeLock.isHeld();
-        });
-        assertFalse("WakeLock expected: RELEASED, was: HELD", held[0]);
+        // THEN wake lock is RELEASED, not held
+        assertFalse("WakeLock expected: RELEASED, was: HELD", mWakeLock.isHeld());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 18cf1c8..7938511 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -1,10 +1,10 @@
 package com.android.systemui.statusbar
 
+import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import android.util.DisplayMetrics
-import androidx.test.filters.SmallTest
 import com.android.systemui.ExpandHelper
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
@@ -67,7 +67,6 @@
     @Mock lateinit var falsingManager: FalsingManager
     @Mock lateinit var notificationPanelController: NotificationPanelViewController
     @Mock lateinit var nsslController: NotificationStackScrollLayoutController
-    @Mock lateinit var featureFlags: FeatureFlags
     @Mock lateinit var depthController: NotificationShadeDepthController
     @Mock lateinit var stackscroller: NotificationStackScrollLayout
     @Mock lateinit var expandHelperCallback: ExpandHelper.Callback
@@ -92,11 +91,10 @@
             displayMetrics = displayMetrics,
             mediaHierarchyManager = mediaHierarchyManager,
             scrimController = scrimController,
-            featureFlags = featureFlags,
+            depthController = depthController,
             context = context,
             configurationController = configurationController,
-            falsingManager = falsingManager,
-            depthController = depthController
+            falsingManager = falsingManager
         )
         whenever(nsslController.view).thenReturn(stackscroller)
         whenever(nsslController.expandHelperCallback).thenReturn(expandHelperCallback)
@@ -211,7 +209,7 @@
         verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat())
         verify(notificationPanelController, never()).setTransitionToFullShadeAmount(anyFloat(),
                 anyBoolean(), anyLong())
-        verify(qS, never()).setTransitionToFullShadeAmount(anyFloat(), anyBoolean())
+        verify(qS, never()).setTransitionToFullShadeAmount(anyFloat(), anyFloat())
     }
 
     @Test
@@ -222,7 +220,7 @@
         verify(scrimController).setTransitionToFullShadeProgress(anyFloat())
         verify(notificationPanelController).setTransitionToFullShadeAmount(anyFloat(),
                 anyBoolean(), anyLong())
-        verify(qS).setTransitionToFullShadeAmount(anyFloat(), anyBoolean())
+        verify(qS).setTransitionToFullShadeAmount(anyFloat(), anyFloat())
         verify(depthController).transitionToFullShadeProgress = anyFloat()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 0c65830..ea21aa9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -56,6 +56,7 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.KeyguardNotificationSuppressor;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -423,7 +424,8 @@
                     mStatusBarStateController,
                     Handler.createAsync(Looper.myLooper()),
                     mDeviceProvisionedController,
-                    mKeyguardStateController);
+                    mKeyguardStateController,
+                    mock(DumpManager.class));
         }
 
         public BroadcastReceiver getBaseBroadcastReceiverForTest() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index 8e949e7..5944e9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -25,6 +25,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputActiveExtender;
 import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputHistoryExtender;
@@ -44,6 +45,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 import dagger.Lazy;
 
 @SmallTest
@@ -80,12 +83,13 @@
 
         mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext,
                 mLockscreenUserManager, mSmartReplyController, mEntryManager,
-                () -> mock(StatusBar.class),
+                () -> Optional.of(mock(StatusBar.class)),
                 mStateController,
                 Handler.createAsync(Looper.myLooper()),
                 mRemoteInputUriController,
                 mClickNotifier,
-                mock(ActionClickLogger.class));
+                mock(ActionClickLogger.class),
+                mock(DumpManager.class));
         mEntry = new NotificationEntryBuilder()
                 .setPkg(TEST_PACKAGE_NAME)
                 .setOpPkg(TEST_PACKAGE_NAME)
@@ -265,23 +269,25 @@
                 NotificationLockscreenUserManager lockscreenUserManager,
                 SmartReplyController smartReplyController,
                 NotificationEntryManager notificationEntryManager,
-                Lazy<StatusBar> statusBarLazy,
+                Lazy<Optional<StatusBar>> statusBarOptionalLazy,
                 StatusBarStateController statusBarStateController,
                 Handler mainHandler,
                 RemoteInputUriController remoteInputUriController,
                 NotificationClickNotifier clickNotifier,
-                ActionClickLogger actionClickLogger) {
+                ActionClickLogger actionClickLogger,
+                DumpManager dumpManager) {
             super(
                     context,
                     lockscreenUserManager,
                     smartReplyController,
                     notificationEntryManager,
-                    statusBarLazy,
+                    statusBarOptionalLazy,
                     statusBarStateController,
                     mainHandler,
                     remoteInputUriController,
                     clickNotifier,
-                    actionClickLogger);
+                    actionClickLogger,
+                    dumpManager);
         }
 
         public void setUpWithPresenterForTest(Callback callback,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 465370b..dbd5168 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar
 
-import android.app.WallpaperManager
 import android.os.IBinder
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
@@ -25,13 +24,14 @@
 import android.view.ViewRootImpl
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.WallpaperController
 import com.android.systemui.util.mockito.eq
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -47,10 +47,8 @@
 import org.mockito.Mockito.anyFloat
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doThrow
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
-import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 import java.util.function.Consumer
@@ -65,7 +63,7 @@
     @Mock private lateinit var biometricUnlockController: BiometricUnlockController
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var choreographer: Choreographer
-    @Mock private lateinit var wallpaperManager: WallpaperManager
+    @Mock private lateinit var wallpaperController: WallpaperController
     @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var root: View
@@ -101,7 +99,7 @@
 
         notificationShadeDepthController = NotificationShadeDepthController(
                 statusBarStateController, blurUtils, biometricUnlockController,
-                keyguardStateController, choreographer, wallpaperManager,
+                keyguardStateController, choreographer, wallpaperController,
                 notificationShadeWindowController, dozeParameters, dumpManager)
         notificationShadeDepthController.shadeAnimation = shadeAnimation
         notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring
@@ -209,8 +207,8 @@
         notificationShadeDepthController.qsPanelExpansion = 0.25f
         notificationShadeDepthController.onPanelExpansionChanged(1f, tracking = false)
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
-        verify(wallpaperManager).setWallpaperZoomOut(any(),
-                eq(Interpolators.getNotificationScrimAlpha(0.25f, false /* notifications */)))
+        verify(wallpaperController).setNotificationShadeZoom(
+                eq(ShadeInterpolation.getNotificationScrimAlpha(0.25f)))
     }
 
     @Test
@@ -242,14 +240,14 @@
         notificationShadeDepthController.transitionToFullShadeProgress = 1f
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
         verify(blurUtils).applyBlur(any(), eq(0), eq(false))
-        verify(wallpaperManager).setWallpaperZoomOut(any(), eq(1f))
+        verify(wallpaperController).setNotificationShadeZoom(eq(1f))
     }
 
     @Test
     fun updateBlurCallback_setsBlurAndZoom() {
         notificationShadeDepthController.addListener(listener)
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
-        verify(wallpaperManager).setWallpaperZoomOut(any(), anyFloat())
+        verify(wallpaperController).setNotificationShadeZoom(anyFloat())
         verify(listener).onWallpaperZoomOutChanged(anyFloat())
         verify(blurUtils).applyBlur(any(), anyInt(), eq(false))
     }
@@ -279,21 +277,6 @@
     }
 
     @Test
-    fun updateBlurCallback_invalidWindow() {
-        `when`(root.isAttachedToWindow).thenReturn(false)
-        notificationShadeDepthController.updateBlurCallback.doFrame(0)
-        verify(wallpaperManager, times(0)).setWallpaperZoomOut(any(), anyFloat())
-    }
-
-    @Test
-    fun updateBlurCallback_exception() {
-        doThrow(IllegalArgumentException("test exception")).`when`(wallpaperManager)
-                .setWallpaperZoomOut(any(), anyFloat())
-        notificationShadeDepthController.updateBlurCallback.doFrame(0)
-        verify(wallpaperManager).setWallpaperZoomOut(any(), anyFloat())
-    }
-
-    @Test
     fun ignoreShadeBlurUntilHidden_schedulesFrame() {
         notificationShadeDepthController.blursDisabledForAppLaunch = true
         verify(choreographer).postFrameCallback(
@@ -322,7 +305,7 @@
 
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
         verify(notificationShadeWindowController).setBackgroundBlurRadius(eq(0))
-        verify(wallpaperManager).setWallpaperZoomOut(any(), eq(1f))
+        verify(wallpaperController).setNotificationShadeZoom(eq(1f))
         verify(blurUtils).applyBlur(eq(viewRootImpl), eq(0), eq(false))
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java
index ac699f7..045e6f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java
@@ -52,7 +52,7 @@
     private ArrayList<Notification.Action> mSmartActions = new ArrayList<>();
     private ArrayList<CharSequence> mSmartReplies = new ArrayList<>();
     private boolean mCanBubble = false;
-    private boolean mIsVisuallyInterruptive = false;
+    private boolean mIsTextChanged = false;
     private boolean mIsConversation = false;
     private ShortcutInfo mShortcutInfo = null;
     private int mRankingAdjustment = 0;
@@ -81,7 +81,7 @@
         mSmartActions = copyList(ranking.getSmartActions());
         mSmartReplies = copyList(ranking.getSmartReplies());
         mCanBubble = ranking.canBubble();
-        mIsVisuallyInterruptive = ranking.visuallyInterruptive();
+        mIsTextChanged = ranking.isTextChanged();
         mIsConversation = ranking.isConversation();
         mShortcutInfo = ranking.getConversationShortcutInfo();
         mRankingAdjustment = ranking.getRankingAdjustment();
@@ -110,7 +110,7 @@
                 mSmartActions,
                 mSmartReplies,
                 mCanBubble,
-                mIsVisuallyInterruptive,
+                mIsTextChanged,
                 mIsConversation,
                 mShortcutInfo,
                 mRankingAdjustment,
@@ -189,8 +189,8 @@
         return this;
     }
 
-    public RankingBuilder setVisuallyInterruptive(boolean interruptive) {
-        mIsVisuallyInterruptive = interruptive;
+    public RankingBuilder setTextChanged(boolean textChanged) {
+        mIsTextChanged = textChanged;
         return this;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
index 7cbc4e4..837d71f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
@@ -38,6 +38,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -51,6 +52,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 @SmallTest
@@ -90,12 +93,13 @@
 
         mRemoteInputManager = new NotificationRemoteInputManager(mContext,
                 mock(NotificationLockscreenUserManager.class), mSmartReplyController,
-                mNotificationEntryManager, () -> mock(StatusBar.class),
+                mNotificationEntryManager, () -> Optional.of(mock(StatusBar.class)),
                 mStatusBarStateController,
                 Handler.createAsync(Looper.myLooper()),
                 mRemoteInputUriController,
                 mClickNotifier,
-                mock(ActionClickLogger.class));
+                mock(ActionClickLogger.class),
+                mock(DumpManager.class));
         mRemoteInputManager.setUpWithCallback(mCallback, mDelegate);
         mNotification = new Notification.Builder(mContext, "")
                 .setSmallIcon(R.drawable.ic_person)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index fca6bc5..c974882 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -21,10 +21,12 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -37,7 +39,7 @@
     @Before
     fun setUp() {
         uiEventLogger = UiEventLoggerFake()
-        controller = StatusBarStateControllerImpl(uiEventLogger)
+        controller = StatusBarStateControllerImpl(uiEventLogger, mock(DumpManager::class.java))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
index 85ec3fa..f2671b76 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
@@ -22,7 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/AccessPointControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/AccessPointControllerImplTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
index 4068f93..7896a26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/AccessPointControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy
+package com.android.systemui.statusbar.connectivity
 
 import android.os.UserManager
 import android.test.suitebuilder.annotation.SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CallbackHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/CallbackHandlerTest.java
similarity index 90%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CallbackHandlerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/CallbackHandlerTest.java
index 2418243..11a53c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CallbackHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/CallbackHandlerTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
@@ -28,11 +28,11 @@
 
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.EmergencyListener;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
 import com.android.systemui.tests.R;
 
 import org.junit.Before;
@@ -124,7 +124,7 @@
         boolean roaming = true;
         MobileDataIndicators indicators = new MobileDataIndicators(
                 status, qs, type, qsType, in, out, typeDescription,
-                typeDescriptionHtml, description, wide, subId, roaming, true);
+                typeDescriptionHtml, description, subId, roaming, true);
         mHandler.setMobileDataIndicators(indicators);
         waitForCallbacks();
 
@@ -141,8 +141,7 @@
         assertEquals(out, expected.activityOut);
         assertEquals(typeDescription, expected.typeContentDescription);
         assertEquals(typeDescriptionHtml, expected.typeContentDescriptionHtml);
-        assertEquals(description, expected.description);
-        assertEquals(wide, expected.isWide);
+        assertEquals(description, expected.qsDescription);
         assertEquals(subId, expected.subId);
         assertTrue(expected.roaming);
         assertTrue(expected.showTriangle);
@@ -183,7 +182,8 @@
 
     @Test
     public void testSignalCallback_setIsAirplaneMode() {
-        IconState state = new IconState(true, R.drawable.stat_sys_airplane_mode, "Test Description");
+        IconState state =
+                new IconState(true, R.drawable.stat_sys_airplane_mode, "Test Description");
         mHandler.setIsAirplaneMode(state);
         waitForCallbacks();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index 6c4ec22..67cab74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
 import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
@@ -71,11 +71,12 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.MobileDataIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
-import com.android.systemui.statusbar.policy.NetworkController.MobileDataIndicators;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -268,7 +269,7 @@
         setSubscriptions(mSubId);
         mMobileSignalController = mNetworkController.mMobileSignalControllers.get(mSubId);
         ArgumentCaptor<ConnectivityManager.NetworkCallback> callbackArg =
-            ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
         verify(mMockCm, atLeastOnce())
             .registerDefaultNetworkCallback(callbackArg.capture(), isA(Handler.class));
         int captureSize = callbackArg.getAllValues().size();
@@ -404,7 +405,7 @@
     }
 
     private static void setConnectivityCommon(NetworkCapabilities.Builder builder,
-        int networkType, boolean validated, boolean isConnected){
+            int networkType, boolean validated, boolean isConnected) {
         // TODO: Separate out into several NetworkCapabilities.
         if (isConnected) {
             builder.addTransportType(networkType);
@@ -538,7 +539,7 @@
     }
 
     protected void verifyLastMobileDataIndicators(boolean visible, int icon, int typeIcon,
-        boolean roaming, boolean inet) {
+            boolean roaming, boolean inet) {
         ArgumentCaptor<MobileDataIndicators> indicatorsArg =
                 ArgumentCaptor.forClass(MobileDataIndicators.class);
 
@@ -646,7 +647,7 @@
     }
 
     protected void assertNetworkNameEquals(String expected) {
-       assertEquals("Network name", expected, mMobileSignalController.getState().networkName);
+        assertEquals("Network name", expected, mMobileSignalController.getState().networkName);
     }
 
     protected void assertDataNetworkNameEquals(String expected) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index 3433a14..00dedd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -1,4 +1,20 @@
-package com.android.systemui.statusbar.policy;
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.connectivity;
 
 import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
 import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
@@ -22,6 +38,7 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
 import org.junit.Test;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerEthernetTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerEthernetTest.java
similarity index 71%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerEthernetTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerEthernetTest.java
index 6aab9c7..675d755 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerEthernetTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerEthernetTest.java
@@ -1,4 +1,20 @@
-package com.android.systemui.statusbar.policy;
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.connectivity;
 
 import static junit.framework.Assert.assertEquals;
 
@@ -7,7 +23,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 
-import com.android.systemui.statusbar.policy.NetworkController.IconState;
+import com.android.systemui.statusbar.connectivity.NetworkController.IconState;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
similarity index 78%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerSignalTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
index 4ff1301..73eddd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerSignalTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.policy;
+package com.android.systemui.statusbar.connectivity;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -42,6 +42,7 @@
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
 import org.junit.Test;
@@ -280,7 +281,7 @@
     // TODO: Put this somewhere else, maybe in its own file.
     @Test
     public void testHasCorrectMobileControllers() {
-        int[] testSubscriptions = new int[] { 1, 5, 3 };
+        int[] testSubscriptions = new int[]{1, 5, 3};
         int notTestSubscription = 0;
         MobileSignalController mobileSignalController = Mockito.mock(MobileSignalController.class);
 
@@ -312,8 +313,8 @@
         // We will not add one subscription to make sure it's controller gets removed.
         int indexToSkipSubscription = 1;
 
-        int[] testSubscriptions = new int[] { 1, 5, 3 };
-        MobileSignalController[] mobileSignalControllers = new MobileSignalController[] {
+        int[] testSubscriptions = new int[]{1, 5, 3};
+        MobileSignalController[] mobileSignalControllers = new MobileSignalController[]{
                 Mockito.mock(MobileSignalController.class),
                 Mockito.mock(MobileSignalController.class),
                 Mockito.mock(MobileSignalController.class),
@@ -401,24 +402,24 @@
     @Test
     public void testOnReceive_stringsUpdatedAction_bothFalse() {
         Intent intent = createStringsUpdatedIntent(false /* showSpn */,
-              "Irrelevant" /* spn */,
-              false /* showPlmn */,
-              "Irrelevant" /* plmn */);
+                "Irrelevant" /* spn */,
+                false /* showPlmn */,
+                "Irrelevant" /* plmn */);
 
         mNetworkController.onReceive(mContext, intent);
 
         String defaultNetworkName = mMobileSignalController
                 .getTextIfExists(
-                com.android.internal.R.string.lockscreen_carrier_default).toString();
+                        com.android.internal.R.string.lockscreen_carrier_default).toString();
         assertNetworkNameEquals(defaultNetworkName);
     }
 
     @Test
     public void testOnReceive_stringsUpdatedAction_bothTrueAndNull() {
         Intent intent = createStringsUpdatedIntent(true /* showSpn */,
-            null /* spn */,
-            true /* showPlmn */,
-            null /* plmn */);
+                null /* spn */,
+                true /* showPlmn */,
+                null /* plmn */);
 
         mNetworkController.onReceive(mContext, intent);
 
@@ -433,15 +434,15 @@
         String plmn = "Test2";
 
         Intent intent = createStringsUpdatedIntent(true /* showSpn */,
-            spn /* spn */,
-            true /* showPlmn */,
-            plmn /* plmn */);
+                spn /* spn */,
+                true /* showPlmn */,
+                plmn /* plmn */);
 
         mNetworkController.onReceive(mContext, intent);
 
         assertNetworkNameEquals(plmn
                 + mMobileSignalController.getTextIfExists(
-                        R.string.status_bar_network_name_separator).toString()
+                R.string.status_bar_network_name_separator).toString()
                 + spn);
     }
 
@@ -477,145 +478,149 @@
 
     @Test
     public void testOnUpdateDataActivity_dataOut() {
-      setupDefaultSignal();
+        setupDefaultSignal();
 
-      updateDataActivity(TelephonyManager.DATA_ACTIVITY_OUT);
+        updateDataActivity(TelephonyManager.DATA_ACTIVITY_OUT);
 
-      verifyLastQsMobileDataIndicators(true /* visible */,
-              DEFAULT_LEVEL /* icon */,
-              DEFAULT_QS_ICON /* typeIcon */,
-              false /* dataIn */,
-              true /* dataOut */);
+        verifyLastQsMobileDataIndicators(true /* visible */,
+                DEFAULT_LEVEL /* icon */,
+                DEFAULT_QS_ICON /* typeIcon */,
+                false /* dataIn */,
+                true /* dataOut */);
     }
 
     @Test
     public void testOnUpdateDataActivity_dataInOut() {
-      setupDefaultSignal();
+        setupDefaultSignal();
 
-      updateDataActivity(TelephonyManager.DATA_ACTIVITY_INOUT);
+        updateDataActivity(TelephonyManager.DATA_ACTIVITY_INOUT);
 
-      verifyLastQsMobileDataIndicators(true /* visible */,
-              DEFAULT_LEVEL /* icon */,
-              DEFAULT_QS_ICON /* typeIcon */,
-              true /* dataIn */,
-              true /* dataOut */);
+        verifyLastQsMobileDataIndicators(true /* visible */,
+                DEFAULT_LEVEL /* icon */,
+                DEFAULT_QS_ICON /* typeIcon */,
+                true /* dataIn */,
+                true /* dataOut */);
 
     }
 
     @Test
     public void testOnUpdateDataActivity_dataActivityNone() {
-      setupDefaultSignal();
+        setupDefaultSignal();
 
-      updateDataActivity(TelephonyManager.DATA_ACTIVITY_NONE);
+        updateDataActivity(TelephonyManager.DATA_ACTIVITY_NONE);
 
-      verifyLastQsMobileDataIndicators(true /* visible */,
-              DEFAULT_LEVEL /* icon */,
-              DEFAULT_QS_ICON /* typeIcon */,
-              false /* dataIn */,
-              false /* dataOut */);
+        verifyLastQsMobileDataIndicators(true /* visible */,
+                DEFAULT_LEVEL /* icon */,
+                DEFAULT_QS_ICON /* typeIcon */,
+                false /* dataIn */,
+                false /* dataOut */);
 
     }
 
     @Test
     public void testCarrierNetworkChange_carrierNetworkChange() {
-      int strength = SignalStrength.SIGNAL_STRENGTH_GREAT;
+        int strength = SignalStrength.SIGNAL_STRENGTH_GREAT;
 
-      setupDefaultSignal();
-      setLevel(strength);
+        setupDefaultSignal();
+        setLevel(strength);
 
-      // Verify baseline
-      verifyLastMobileDataIndicators(true /* visible */,
-              strength /* strengthIcon */,
-              DEFAULT_ICON /* typeIcon */);
+        // Verify baseline
+        verifyLastMobileDataIndicators(true /* visible */,
+                strength /* strengthIcon */,
+                DEFAULT_ICON /* typeIcon */);
 
-      // API call is made
-      setCarrierNetworkChange(true /* enabled */);
+        // API call is made
+        setCarrierNetworkChange(true /* enabled */);
 
-      // Carrier network change is true, show special indicator
-      verifyLastMobileDataIndicators(true /* visible */,
-              SignalDrawable.getCarrierChangeState(CellSignalStrength.getNumSignalStrengthLevels()),
-              0 /* typeIcon */);
+        // Carrier network change is true, show special indicator
+        verifyLastMobileDataIndicators(true /* visible */,
+                SignalDrawable.getCarrierChangeState(
+                        CellSignalStrength.getNumSignalStrengthLevels()),
+                0 /* typeIcon */);
 
-      // Revert back
-      setCarrierNetworkChange(false /* enabled */);
+        // Revert back
+        setCarrierNetworkChange(false /* enabled */);
 
-      // Verify back in previous state
-      verifyLastMobileDataIndicators(true /* visible */,
-              strength /* strengthIcon */,
-              DEFAULT_ICON /* typeIcon */);
+        // Verify back in previous state
+        verifyLastMobileDataIndicators(true /* visible */,
+                strength /* strengthIcon */,
+                DEFAULT_ICON /* typeIcon */);
     }
 
     @Test
     public void testCarrierNetworkChange_roamingBeforeNetworkChange() {
-      int strength = SignalStrength.SIGNAL_STRENGTH_GREAT;
+        int strength = SignalStrength.SIGNAL_STRENGTH_GREAT;
 
-      setupDefaultSignal();
-      setLevel(strength);
-      setGsmRoaming(true);
+        setupDefaultSignal();
+        setLevel(strength);
+        setGsmRoaming(true);
 
-      // Verify baseline
-      verifyLastMobileDataIndicators(true /* visible */,
-              strength /* strengthIcon */,
-              DEFAULT_ICON /* typeIcon */,
-              true /* roaming */);
+        // Verify baseline
+        verifyLastMobileDataIndicators(true /* visible */,
+                strength /* strengthIcon */,
+                DEFAULT_ICON /* typeIcon */,
+                true /* roaming */);
 
-      // API call is made
-      setCarrierNetworkChange(true /* enabled */);
+        // API call is made
+        setCarrierNetworkChange(true /* enabled */);
 
-      // Carrier network change is true, show special indicator, no roaming.
-      verifyLastMobileDataIndicators(true /* visible */,
-              SignalDrawable.getCarrierChangeState(CellSignalStrength.getNumSignalStrengthLevels()),
-              0 /* typeIcon */,
-              false /* roaming */);
+        // Carrier network change is true, show special indicator, no roaming.
+        verifyLastMobileDataIndicators(true /* visible */,
+                SignalDrawable.getCarrierChangeState(
+                        CellSignalStrength.getNumSignalStrengthLevels()),
+                0 /* typeIcon */,
+                false /* roaming */);
 
-      // Revert back
-      setCarrierNetworkChange(false /* enabled */);
+        // Revert back
+        setCarrierNetworkChange(false /* enabled */);
 
-      // Verify back in previous state
-      verifyLastMobileDataIndicators(true /* visible */,
-              strength /* strengthIcon */,
-              DEFAULT_ICON /* typeIcon */,
-              true /* roaming */);
+        // Verify back in previous state
+        verifyLastMobileDataIndicators(true /* visible */,
+                strength /* strengthIcon */,
+                DEFAULT_ICON /* typeIcon */,
+                true /* roaming */);
     }
 
     @Test
     public void testCarrierNetworkChange_roamingAfterNetworkChange() {
-      int strength = SignalStrength.SIGNAL_STRENGTH_GREAT;
+        int strength = SignalStrength.SIGNAL_STRENGTH_GREAT;
 
-      setupDefaultSignal();
-      setLevel(strength);
+        setupDefaultSignal();
+        setLevel(strength);
 
-      // Verify baseline
-      verifyLastMobileDataIndicators(true /* visible */,
-              strength /* strengthIcon */,
-              DEFAULT_ICON /* typeIcon */,
-              false /* roaming */);
+        // Verify baseline
+        verifyLastMobileDataIndicators(true /* visible */,
+                strength /* strengthIcon */,
+                DEFAULT_ICON /* typeIcon */,
+                false /* roaming */);
 
-      // API call is made
-      setCarrierNetworkChange(true /* enabled */);
+        // API call is made
+        setCarrierNetworkChange(true /* enabled */);
 
-      // Carrier network change is true, show special indicator, no roaming.
-      verifyLastMobileDataIndicators(true /* visible */,
-              SignalDrawable.getCarrierChangeState(CellSignalStrength.getNumSignalStrengthLevels()),
-              0 /* typeIcon */,
-              false /* roaming */);
+        // Carrier network change is true, show special indicator, no roaming.
+        verifyLastMobileDataIndicators(true /* visible */,
+                SignalDrawable.getCarrierChangeState(
+                        CellSignalStrength.getNumSignalStrengthLevels()),
+                0 /* typeIcon */,
+                false /* roaming */);
 
-      setGsmRoaming(true);
+        setGsmRoaming(true);
 
-      // Roaming should not show.
-      verifyLastMobileDataIndicators(true /* visible */,
-              SignalDrawable.getCarrierChangeState(CellSignalStrength.getNumSignalStrengthLevels()),
-              0 /* typeIcon */,
-              false /* roaming */);
+        // Roaming should not show.
+        verifyLastMobileDataIndicators(true /* visible */,
+                SignalDrawable.getCarrierChangeState(
+                        CellSignalStrength.getNumSignalStrengthLevels()),
+                0 /* typeIcon */,
+                false /* roaming */);
 
-      // Revert back
-      setCarrierNetworkChange(false /* enabled */);
+        // Revert back
+        setCarrierNetworkChange(false /* enabled */);
 
-      // Verify back in previous state
-      verifyLastMobileDataIndicators(true /* visible */,
-              strength /* strengthIcon */,
-              DEFAULT_ICON /* typeIcon */,
-              true /* roaming */);
+        // Verify back in previous state
+        verifyLastMobileDataIndicators(true /* visible */,
+                strength /* strengthIcon */,
+                DEFAULT_ICON /* typeIcon */,
+                true /* roaming */);
     }
 
     private void verifyEmergencyOnly(boolean isEmergencyOnly) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
index 4a5770d..ffeaf20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
@@ -1,10 +1,25 @@
-package com.android.systemui.statusbar.policy;
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.connectivity;
 
 import static junit.framework.Assert.assertEquals;
 
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Matchers.anyLong;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -21,7 +36,7 @@
 import android.testing.TestableLooper.RunWithLooper;
 
 import com.android.settingslib.mobile.TelephonyIcons;
-import com.android.systemui.statusbar.policy.NetworkController.WifiIndicators;
+import com.android.systemui.statusbar.connectivity.NetworkController.WifiIndicators;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -165,7 +180,7 @@
     }
 
     @Test
-    public void testWifiIconDisconnectedViaCallback(){
+    public void testWifiIconDisconnectedViaCallback() {
         // Setup normal connection
         String testSsid = "Test SSID";
         int testLevel = 2;
@@ -183,7 +198,7 @@
     }
 
     @Test
-    public void testVpnWithUnderlyingWifi(){
+    public void testVpnWithUnderlyingWifi() {
         String testSsid = "Test SSID";
         int testLevel = 2;
         setWifiEnabled(true);
@@ -299,7 +314,7 @@
 
     protected void setWifiLevel(int level) {
         float amountPerLevel = (MAX_RSSI - MIN_RSSI) / (WifiIcons.WIFI_LEVEL_COUNT - 1);
-        int rssi = (int)(MIN_RSSI + level * amountPerLevel);
+        int rssi = (int) (MIN_RSSI + level * amountPerLevel);
         // Put RSSI in the middle of the range.
         rssi += amountPerLevel / 2;
         when(mWifiInfo.getRssi()).thenReturn(rssi);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 9e103d6..ff91978 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -41,7 +41,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -53,7 +53,6 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.time.FakeSystemClock
-import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.mockito.ArgumentCaptor
@@ -121,6 +120,8 @@
     private lateinit var statusBarStateListener: StateListener
     private lateinit var deviceProvisionedListener: DeviceProvisionedListener
 
+    private lateinit var smartspaceView: SmartspaceView
+
     private val clock = FakeSystemClock()
     private val executor = FakeExecutor(clock)
     private val execution = FakeExecution()
@@ -148,7 +149,7 @@
         `when`(secureSettings.getUriFor(PRIVATE_LOCKSCREEN_SETTING))
                 .thenReturn(fakePrivateLockscreenSettingUri)
         `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession)
-        `when`(plugin.getView(any())).thenReturn(fakeSmartspaceView)
+        `when`(plugin.getView(any())).thenReturn(createSmartspaceView(), createSmartspaceView())
         `when`(userTracker.userProfiles).thenReturn(userList)
         `when`(statusBarStateController.dozeAmount).thenReturn(0.5f)
         `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true)
@@ -198,8 +199,9 @@
         `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(false)
         `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(false)
 
-        // WHEN a connection attempt is made
-        controller.buildAndConnectView(fakeParent)
+        // WHEN a connection attempt is made and view is attached
+        val view = controller.buildAndConnectView(fakeParent)
+        controller.stateChangeListener.onViewAttachedToWindow(view)
 
         // THEN no session is created
         verify(smartspaceManager, never()).createSmartspaceSession(any())
@@ -244,6 +246,7 @@
         clearInvocations(plugin)
 
         // WHEN the session is closed
+        controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View)
         controller.disconnect()
 
         // THEN the listener receives an empty list of targets
@@ -283,7 +286,7 @@
         configChangeListener.onThemeChanged()
 
         // We update the new text color to match the wallpaper color
-        verify(fakeSmartspaceView).setPrimaryTextColor(anyInt())
+        verify(smartspaceView).setPrimaryTextColor(anyInt())
     }
 
     @Test
@@ -295,7 +298,7 @@
         statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
 
         // We pass that along to the view
-        verify(fakeSmartspaceView).setDozeAmount(0.7f)
+        verify(smartspaceView).setDozeAmount(0.7f)
     }
 
     @Test
@@ -415,6 +418,7 @@
         connectSession()
 
         // WHEN we are told to cleanup
+        controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View)
         controller.disconnect()
 
         // THEN we disconnect from the session and unregister any listeners
@@ -427,35 +431,21 @@
     }
 
     @Test
-    fun testBuildViewIsIdempotent() {
-        // GIVEN a connected session
-        connectSession()
-        clearInvocations(plugin)
-
-        // WHEN we disconnect and then reconnect
-        controller.disconnect()
-        controller.buildAndConnectView(fakeParent)
-
-        // THEN the view is not rebuilt
-        verify(plugin, never()).getView(any())
-        assertEquals(fakeSmartspaceView, controller.view)
-    }
-
-    @Test
-    fun testDoubleConnectIsIgnored() {
+    fun testMultipleViewsUseSameSession() {
         // GIVEN a connected session
         connectSession()
         clearInvocations(smartspaceManager)
         clearInvocations(plugin)
 
-        // WHEN we're asked to connect a second time and add to a parent
+        // WHEN we're asked to connect a second time and add to a parent. If the same view
+        // was created the ViewGroup will throw an exception
         val view = controller.buildAndConnectView(fakeParent)
         fakeParent.addView(view)
+        val smartspaceView2 = view as SmartspaceView
 
-        // THEN the existing view and session are reused
+        // THEN the existing session is reused and views are registered
         verify(smartspaceManager, never()).createSmartspaceSession(any())
-        verify(plugin, never()).getView(any())
-        assertEquals(fakeSmartspaceView, controller.view)
+        verify(smartspaceView2).registerDataProvider(plugin)
     }
 
     @Test
@@ -473,8 +463,12 @@
     }
 
     private fun connectSession() {
-        controller.buildAndConnectView(fakeParent)
+        val view = controller.buildAndConnectView(fakeParent)
+        smartspaceView = view as SmartspaceView
 
+        controller.stateChangeListener.onViewAttachedToWindow(view)
+
+        verify(smartspaceView).registerDataProvider(plugin)
         verify(smartspaceSession)
                 .addOnTargetsAvailableListener(any(), capture(sessionListenerCaptor))
         sessionListener = sessionListenerCaptor.value
@@ -498,11 +492,11 @@
         verify(smartspaceSession).requestSmartspaceUpdate()
         clearInvocations(smartspaceSession)
 
-        verify(fakeSmartspaceView).setPrimaryTextColor(anyInt())
-        verify(fakeSmartspaceView).setDozeAmount(0.5f)
-        clearInvocations(fakeSmartspaceView)
+        verify(smartspaceView).setPrimaryTextColor(anyInt())
+        verify(smartspaceView).setDozeAmount(0.5f)
+        clearInvocations(view)
 
-        fakeParent.addView(fakeSmartspaceView)
+        fakeParent.addView(view)
     }
 
     private fun setActiveUser(userHandle: UserHandle) {
@@ -538,31 +532,33 @@
         ).thenReturn(if (value) 1 else 0)
     }
 
-    private val fakeSmartspaceView = spy(object : View(context), SmartspaceView {
-        override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
-        }
+    private fun createSmartspaceView(): SmartspaceView {
+        return spy(object : View(context), SmartspaceView {
+            override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+            }
 
-        override fun setPrimaryTextColor(color: Int) {
-        }
+            override fun setPrimaryTextColor(color: Int) {
+            }
 
-        override fun setDozeAmount(amount: Float) {
-        }
+            override fun setDozeAmount(amount: Float) {
+            }
 
-        override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
-        }
+            override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+            }
 
-        override fun setFalsingManager(falsingManager: FalsingManager?) {
-        }
+            override fun setFalsingManager(falsingManager: FalsingManager?) {
+            }
 
-        override fun setDnd(image: Drawable?, description: String?) {
-        }
+            override fun setDnd(image: Drawable?, description: String?) {
+            }
 
-        override fun setNextAlarm(image: Drawable?, description: String?) {
-        }
+            override fun setNextAlarm(image: Drawable?, description: String?) {
+            }
 
-        override fun setMediaTarget(target: SmartspaceTarget?) {
-        }
-    })
+            override fun setMediaTarget(target: SmartspaceTarget?) {
+            }
+        })
+    }
 }
 
 private const val PRIVATE_LOCKSCREEN_SETTING =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index 1be14b6..902d115 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -32,6 +32,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
@@ -64,8 +65,10 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLifetimeExtender;
+import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -79,6 +82,7 @@
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -92,6 +96,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -118,6 +123,7 @@
     @Mock private KeyguardEnvironment mEnvironment;
     @Mock private ExpandableNotificationRow mRow;
     @Mock private NotificationEntryListener mEntryListener;
+    @Mock private NotifCollectionListener mNotifCollectionListener;
     @Mock private NotificationRemoveInterceptor mRemoveInterceptor;
     @Mock private HeadsUpManager mHeadsUpManager;
     @Mock private RankingMap mRankingMap;
@@ -130,6 +136,7 @@
     @Mock private LeakDetector mLeakDetector;
     @Mock private NotificationMediaManager mNotificationMediaManager;
     @Mock private NotificationRowBinder mNotificationRowBinder;
+    @Mock private NotificationListener mNotificationListener;
 
     private int mId;
     private NotificationEntry mEntry;
@@ -195,9 +202,11 @@
                 () -> mRemoteInputManager,
                 mLeakDetector,
                 mock(ForegroundServiceDismissalFeatureController.class),
-                mock(IStatusBarService.class)
+                mock(IStatusBarService.class),
+                mock(DumpManager.class)
         );
-        mEntryManager.setRanker(
+        mEntryManager.initialize(
+                mNotificationListener,
                 new NotificationRankingManager(
                         () -> mNotificationMediaManager,
                         mGroupManager,
@@ -210,6 +219,7 @@
                         mEnvironment));
         mEntryManager.setUpWithPresenter(mPresenter);
         mEntryManager.addNotificationEntryListener(mEntryListener);
+        mEntryManager.addCollectionListener(mNotifCollectionListener);
         mEntryManager.addNotificationRemoveInterceptor(mRemoveInterceptor);
 
         setUserSentiment(mSbn.getKey(), Ranking.USER_SENTIMENT_NEUTRAL);
@@ -313,13 +323,20 @@
                 eq(mEntry), any(), eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON));
     }
 
+    /** Regression test for b/201097913. */
     @Test
-    public void testRemoveNotification_whilePending() {
+    public void testRemoveNotification_whilePending_onlyCollectionListenerNotified() {
+        // Add and then remove a pending entry (entry that hasn't been inflated).
         mEntryManager.addNotification(mSbn, mRankingMap);
         mEntryManager.removeNotification(mSbn.getKey(), mRankingMap, UNDEFINED_DISMISS_REASON);
 
+        // Verify that only the listener for the NEW pipeline is notified.
+        // Old pipeline:
         verify(mEntryListener, never()).onEntryRemoved(
-                eq(mEntry), any(), eq(false /* removedByUser */), eq(UNDEFINED_DISMISS_REASON));
+                argThat(matchEntryOnSbn()), any(), anyBoolean(), anyInt());
+        // New pipeline:
+        verify(mNotifCollectionListener).onEntryRemoved(
+                argThat(matchEntryOnSbn()), anyInt());
     }
 
     @Test
@@ -634,6 +651,11 @@
                     PendingIntent.FLAG_IMMUTABLE)).build();
     }
 
+    // TODO(b/201321631): Update more tests to use this function instead of eq(mEntry).
+    private ArgumentMatcher<NotificationEntry> matchEntryOnSbn() {
+        return e -> e.getSbn().equals(mSbn);
+    }
+
     private static class FakeNotificationLifetimeExtender implements NotificationLifetimeExtender {
         private NotificationSafeToRemoveCallback mCallback;
         private boolean mExtendLifetimes = true;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
index b1eef4b..b02a336 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
@@ -42,6 +42,7 @@
 
 import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.MediaFeatureFlag;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -119,7 +120,8 @@
                 new NotificationGroupManagerLegacy(
                         mock(StatusBarStateController.class),
                         () -> mock(PeopleNotificationIdentifier.class),
-                        Optional.of(mock(Bubbles.class))));
+                        Optional.of(mock(Bubbles.class)),
+                        mock(DumpManager.class)));
         mDependency.injectMockDependency(ShadeController.class);
         mDependency.injectMockDependency(NotificationLockscreenUserManager.class);
         mDependency.injectTestDependency(KeyguardEnvironment.class, mEnvironment);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/VisualStabilityManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/VisualStabilityManagerTest.java
index baeedcf..a737ce5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/VisualStabilityManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/VisualStabilityManagerTest.java
@@ -32,6 +32,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -70,7 +71,8 @@
                 mock(NotificationEntryManager.class),
                 new Handler(mTestableLooper.getLooper()),
                 statusBarStateController,
-                wakefulnessLifecycle);
+                wakefulnessLifecycle,
+                mock(DumpManager.class));
 
         mVisualStabilityManager.setVisibilityLocationProvider(mLocationProvider);
         mEntry = new NotificationEntryBuilder().build();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
index 62667bc..da956ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
@@ -30,3 +30,7 @@
     modifier(builder)
     builder.apply(entry)
 }
+
+fun getAttachState(entry: ListEntry): ListAttachState {
+    return entry.attachState
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 9a5482c..39d794d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -67,7 +67,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.LogBufferEulogizer;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent;
 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 2ce22a6..3378003 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -802,13 +802,13 @@
                 .onBeforeTransformGroups(anyList());
         inOrder.verify(promoter, atLeastOnce())
                 .shouldPromoteToTopLevel(any(NotificationEntry.class));
+        inOrder.verify(mOnBeforeFinalizeFilterListener).onBeforeFinalizeFilter(anyList());
+        inOrder.verify(preRenderFilter, atLeastOnce())
+                .shouldFilterOut(any(NotificationEntry.class), anyLong());
         inOrder.verify(mOnBeforeSortListener).onBeforeSort(anyList());
         inOrder.verify(section, atLeastOnce()).isInSection(any(ListEntry.class));
         inOrder.verify(comparator, atLeastOnce())
                 .compare(any(ListEntry.class), any(ListEntry.class));
-        inOrder.verify(mOnBeforeFinalizeFilterListener).onBeforeFinalizeFilter(anyList());
-        inOrder.verify(preRenderFilter, atLeastOnce())
-                .shouldFilterOut(any(NotificationEntry.class), anyLong());
         inOrder.verify(mOnBeforeRenderListListener).onBeforeRenderList(anyList());
         inOrder.verify(mOnRenderListListener).onRenderList(anyList());
     }
@@ -1045,12 +1045,32 @@
         // because group changes aren't allowed by the stability manager
         verifyBuiltList(
                 notif(0),
+                notif(2),
                 group(
                         summary(3),
                         child(4),
                         child(5)
-                ),
-                notif(2)
+                )
+        );
+    }
+
+    @Test
+    public void testBrokenGroupNotificationOrdering() {
+        // GIVEN two group children with different sections & without a summary yet
+
+        addGroupChild(0, PACKAGE_2, GROUP_1);
+        addNotif(1, PACKAGE_1);
+        addGroupChild(2, PACKAGE_2, GROUP_1);
+        addGroupChild(3, PACKAGE_2, GROUP_1);
+
+        dispatchBuild();
+
+        // THEN all notifications are not grouped and posted in order by index
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3)
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java
index 7e771ce..a3569e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java
@@ -31,7 +31,6 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -72,7 +71,6 @@
     @Mock private HeadsUpViewBinder mHeadsUpViewBinder;
     @Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
-    @Mock private RemoteInputController mRemoteInputController;
     @Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;
     @Mock private NodeController mHeaderController;
 
@@ -81,7 +79,6 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController);
 
         mCoordinator = new HeadsUpCoordinator(
                 mHeadsUpManager,
@@ -215,7 +212,7 @@
 
         // WHEN mEntry is removed from the notification collection
         mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0);
-        when(mRemoteInputController.isSpinning(any())).thenReturn(false);
+        when(mRemoteInputManager.isSpinning(any())).thenReturn(false);
 
         // THEN heads up manager should remove the entry
         verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
new file mode 100644
index 0000000..2e676bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.render
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.getAttachState
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.util.mockito.any
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class NodeSpecBuilderTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var viewBarn: NotifViewBarn
+
+    private var rootController: NodeController = buildFakeController("rootController")
+    private var headerController0: NodeController = buildFakeController("header0")
+    private var headerController1: NodeController = buildFakeController("header1")
+    private var headerController2: NodeController = buildFakeController("header2")
+
+    private val section0 = buildSection(0, headerController0)
+    private val section0NoHeader = buildSection(0, null)
+    private val section1 = buildSection(1, headerController1)
+    private val section1NoHeader = buildSection(1, null)
+    private val section2 = buildSection(2, headerController2)
+
+    private val fakeViewBarn = FakeViewBarn()
+
+    private lateinit var specBuilder: NodeSpecBuilder
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(viewBarn.requireView(any())).thenAnswer {
+            fakeViewBarn.getViewByEntry(it.getArgument(0))
+        }
+
+        specBuilder = NodeSpecBuilder(viewBarn)
+    }
+
+    @Test
+    fun testSimpleMapping() {
+        checkOutput(
+                // GIVEN a simple flat list of notifications all in the same headerless section
+                listOf(
+                        notif(0, section0NoHeader),
+                        notif(1, section0NoHeader),
+                        notif(2, section0NoHeader),
+                        notif(3, section0NoHeader)
+                ),
+
+                // THEN we output a similarly simple flag list of nodes
+                tree(
+                        notifNode(0),
+                        notifNode(1),
+                        notifNode(2),
+                        notifNode(3)
+                )
+        )
+    }
+
+    @Test
+    fun testHeaderInjection() {
+        checkOutput(
+                // GIVEN a flat list of notifications, spread across three sections
+                listOf(
+                        notif(0, section0),
+                        notif(1, section0),
+                        notif(2, section1),
+                        notif(3, section2)
+                ),
+
+                // THEN each section has its header injected
+                tree(
+                        node(headerController0),
+                        notifNode(0),
+                        notifNode(1),
+                        node(headerController1),
+                        notifNode(2),
+                        node(headerController2),
+                        notifNode(3)
+                )
+        )
+    }
+
+    @Test
+    fun testGroups() {
+        checkOutput(
+                // GIVEN a mixed list of top-level notifications and groups
+                listOf(
+                    notif(0, section0),
+                    group(1, section1,
+                            notif(2),
+                            notif(3),
+                            notif(4)
+                    ),
+                    notif(5, section2),
+                    group(6, section2,
+                            notif(7),
+                            notif(8),
+                            notif(9)
+                    )
+                ),
+
+                // THEN we properly construct all the nodes
+                tree(
+                        node(headerController0),
+                        notifNode(0),
+                        node(headerController1),
+                        notifNode(1,
+                                notifNode(2),
+                                notifNode(3),
+                                notifNode(4)
+                        ),
+                        node(headerController2),
+                        notifNode(5),
+                        notifNode(6,
+                                notifNode(7),
+                                notifNode(8),
+                                notifNode(9)
+                        )
+                )
+        )
+    }
+
+    @Test
+    fun testSecondSectionWithNoHeader() {
+        checkOutput(
+                // GIVEN a middle section with no associated header view
+                listOf(
+                        notif(0, section0),
+                        notif(1, section1NoHeader),
+                        group(2, section1NoHeader,
+                                notif(3),
+                                notif(4)
+                        ),
+                        notif(5, section2)
+                ),
+
+                // THEN the header view is left out of the tree (but the notifs are still present)
+                tree(
+                        node(headerController0),
+                        notifNode(0),
+                        notifNode(1),
+                        notifNode(2,
+                                notifNode(3),
+                                notifNode(4)
+                        ),
+                        node(headerController2),
+                        notifNode(5)
+                )
+        )
+    }
+
+    @Test(expected = RuntimeException::class)
+    fun testRepeatedSectionsThrow() {
+        checkOutput(
+                // GIVEN a malformed list where sections are not contiguous
+                listOf(
+                        notif(0, section0),
+                        notif(1, section1),
+                        notif(2, section0)
+                ),
+
+                // THEN an exception is thrown
+                tree()
+        )
+    }
+
+    private fun checkOutput(list: List<ListEntry>, desiredTree: NodeSpecImpl) {
+        checkTree(desiredTree, specBuilder.buildNodeSpec(rootController, list))
+    }
+
+    private fun checkTree(desiredTree: NodeSpec, actualTree: NodeSpec) {
+        try {
+            checkNode(desiredTree, actualTree)
+        } catch (e: AssertionError) {
+            throw AssertionError("Trees don't match: ${e.message}\nActual tree:\n" +
+                    treeSpecToStr(actualTree))
+        }
+    }
+
+    private fun checkNode(desiredTree: NodeSpec, actualTree: NodeSpec) {
+        if (actualTree.controller != desiredTree.controller) {
+            throw AssertionError("Node {${actualTree.controller.nodeLabel}} should " +
+                    "be ${desiredTree.controller.nodeLabel}")
+        }
+        for (i in 0 until desiredTree.children.size) {
+            if (i >= actualTree.children.size) {
+                throw AssertionError("Node {${actualTree.controller.nodeLabel}}" +
+                        " is missing child ${desiredTree.children[i].controller.nodeLabel}")
+            }
+            checkNode(desiredTree.children[i], actualTree.children[i])
+        }
+    }
+
+    private fun notif(id: Int, section: NotifSection? = null): NotificationEntry {
+        val entry = NotificationEntryBuilder()
+                .setId(id)
+                .build()
+        if (section != null) {
+            getAttachState(entry).section = section
+        }
+        fakeViewBarn.buildNotifView(id, entry)
+        return entry
+    }
+
+    private fun group(
+        id: Int,
+        section: NotifSection,
+        vararg children: NotificationEntry
+    ): GroupEntry {
+        val group = GroupEntryBuilder()
+                .setKey("group_$id")
+                .setSummary(
+                        NotificationEntryBuilder()
+                                .setId(id)
+                                .build())
+                .setChildren(children.asList())
+                .build()
+        getAttachState(group).section = section
+        fakeViewBarn.buildNotifView(id, group.summary!!)
+
+        for (child in children) {
+            getAttachState(child).section = section
+        }
+        return group
+    }
+
+    private fun tree(vararg children: NodeSpecImpl): NodeSpecImpl {
+        return node(rootController, *children)
+    }
+
+    private fun node(view: NodeController, vararg children: NodeSpecImpl): NodeSpecImpl {
+        val node = NodeSpecImpl(null, view)
+        node.children.addAll(children)
+        return node
+    }
+
+    private fun notifNode(id: Int, vararg children: NodeSpecImpl): NodeSpecImpl {
+        return node(fakeViewBarn.getViewById(id), *children)
+    }
+}
+
+private class FakeViewBarn {
+    private val entries = mutableMapOf<Int, NotificationEntry>()
+    private val views = mutableMapOf<NotificationEntry, NodeController>()
+
+    fun buildNotifView(id: Int, entry: NotificationEntry) {
+        if (entries.contains(id)) {
+            throw RuntimeException("ID $id is already in use")
+        }
+        entries[id] = entry
+        views[entry] = buildFakeController("Entry $id")
+    }
+
+    fun getViewById(id: Int): NodeController {
+        return views[entries[id] ?: throw RuntimeException("No view with ID $id")]!!
+    }
+
+    fun getViewByEntry(entry: NotificationEntry): NodeController {
+        return views[entry] ?: throw RuntimeException("No view defined for key ${entry.key}")
+    }
+}
+
+private fun buildFakeController(name: String): NodeController {
+    val controller = Mockito.mock(NodeController::class.java)
+    `when`(controller.nodeLabel).thenReturn(name)
+    return controller
+}
+
+private fun buildSection(index: Int, nodeController: NodeController?): NotifSection {
+    return NotifSection(object : NotifSectioner("Section $index") {
+
+        override fun isInSection(entry: ListEntry?): Boolean {
+            throw NotImplementedError("This should never be called")
+        }
+
+        override fun getHeaderNodeController(): NodeController? {
+            return nodeController
+        }
+    }, index)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
new file mode 100644
index 0000000..24a0ad3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import static android.view.DragEvent.ACTION_DRAG_STARTED;
+
+import android.content.Context;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.DragEvent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.statusbar.phone.ShadeController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
+
+    private ExpandableNotificationRow mRow;
+    private ExpandableNotificationRow mGroupRow;
+    private ExpandableNotificationRowDragController mController;
+    private NotificationTestHelper mNotificationTestHelper;
+
+    private NotificationGutsManager mGutsManager = mock(NotificationGutsManager.class);
+    private HeadsUpManager mHeadsUpManager = mock(HeadsUpManager.class);
+    private NotificationMenuRow mMenuRow = mock(NotificationMenuRow.class);
+    private NotificationMenuRowPlugin.MenuItem mMenuItem =
+            mock(NotificationMenuRowPlugin.MenuItem.class);
+
+    @Before
+    public void setUp() throws Exception {
+        allowTestableLooperAsMainThread();
+
+        mDependency.injectMockDependency(ShadeController.class);
+
+        mNotificationTestHelper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        mRow = mNotificationTestHelper.createRow();
+        mGroupRow = mNotificationTestHelper.createGroup(4);
+        when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
+
+        mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager);
+    }
+
+    @Test
+    public void testDoStartDragHeadsUpNotif_startDragAndDrop() throws Exception {
+        ExpandableNotificationRowDragController controller = createSpyController();
+        mRow.setDragController(controller);
+        mRow.setHeadsUp(true);
+        mRow.setPinned(true);
+
+        mRow.doLongClickCallback(0, 0);
+        mRow.doDragCallback(0, 0);
+        verify(controller).startDragAndDrop(mRow);
+
+        // Simulate the drag start
+        mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
+                null, null, false));
+        verify(mHeadsUpManager, times(1)).releaseAllImmediately();
+    }
+
+    @Test
+    public void testDoStartDragNotif() throws Exception {
+        ExpandableNotificationRowDragController controller = createSpyController();
+        mRow.setDragController(controller);
+
+        mDependency.get(ShadeController.class).instantExpandNotificationsPanel();
+        mRow.doDragCallback(0, 0);
+        verify(controller).startDragAndDrop(mRow);
+
+        // Simulate the drag start
+        mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
+                null, null, false));
+        verify(mDependency.get(ShadeController.class)).animateCollapsePanels(0, true);
+    }
+
+    private ExpandableNotificationRowDragController createSpyController() {
+        return spy(mController);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
index cea49b7..d3738f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
@@ -47,11 +47,13 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationPresenter;
@@ -119,6 +121,7 @@
         throw new RuntimeException("Timed out waiting to inflate");
     };
 
+    @Mock private NotificationListener mNotificationListener;
     @Mock private NotificationPresenter mPresenter;
     @Mock private NotificationEntryManager.KeyguardEnvironment mEnvironment;
     @Mock private NotificationListContainer mListContainer;
@@ -186,9 +189,11 @@
                 () -> mRemoteInputManager,
                 mLeakDetector,
                 mock(ForegroundServiceDismissalFeatureController.class),
-                mock(IStatusBarService.class)
+                mock(IStatusBarService.class),
+                mock(DumpManager.class)
         );
-        mEntryManager.setRanker(
+        mEntryManager.initialize(
+                mNotificationListener,
                 new NotificationRankingManager(
                         () -> mock(NotificationMediaManager.class),
                         mGroupMembershipManager,
@@ -265,7 +270,8 @@
                                 new FalsingManagerFake(),
                                 new FalsingCollectorFake(),
                                 mPeopleNotificationIdentifier,
-                                Optional.of(mock(BubblesManager.class))
+                                Optional.of(mock(BubblesManager.class)),
+                                mock(ExpandableNotificationRowDragController.class)
                         ));
 
         when(mNotificationRowComponentBuilder.activatableNotificationView(any()))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index 9f537f5..7d8e0d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -68,6 +68,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.settings.UserContextProvider;
@@ -91,7 +92,6 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
@@ -99,8 +99,6 @@
 
 import java.util.Optional;
 
-import javax.inject.Provider;
-
 /**
  * Tests for {@link NotificationGutsManager}.
  */
@@ -157,11 +155,12 @@
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
 
         mGutsManager = new NotificationGutsManager(mContext,
-                () -> mStatusBar, mHandler, mHandler, mAccessibilityManager, mHighPriorityProvider,
-                mINotificationManager, mNotificationEntryManager, mPeopleSpaceWidgetManager,
-                mLauncherApps, mShortcutManager, mChannelEditorDialogController, mContextTracker,
-                mAssistantFeedbackController, Optional.of(mBubblesManager),
-                new UiEventLoggerFake(), mOnUserInteractionCallback, mShadeController);
+                () -> Optional.of(mStatusBar), mHandler, mHandler, mAccessibilityManager,
+                mHighPriorityProvider, mINotificationManager, mNotificationEntryManager,
+                mPeopleSpaceWidgetManager, mLauncherApps, mShortcutManager,
+                mChannelEditorDialogController, mContextTracker, mAssistantFeedbackController,
+                Optional.of(mBubblesManager), new UiEventLoggerFake(), mOnUserInteractionCallback,
+                mShadeController, mock(DumpManager.class));
         mGutsManager.setUpWithPresenter(mPresenter, mNotificationListContainer,
                 mCheckSaveListener, mOnSettingsClickListener);
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 0bb66fc..42aecfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -46,6 +46,7 @@
 import com.android.systemui.TestableDependency;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -125,7 +126,8 @@
         mGroupMembershipManager = new NotificationGroupManagerLegacy(
                 mStatusBarStateController,
                 () -> mock(PeopleNotificationIdentifier.class),
-                Optional.of((mock(Bubbles.class))));
+                Optional.of((mock(Bubbles.class))),
+                mock(DumpManager.class));
         mGroupExpansionManager = mGroupMembershipManager;
         mHeadsUpManager = new HeadsUpManagerPhone(mContext, mStatusBarStateController,
                 mock(KeyguardBypassController.class), mock(NotificationGroupManagerLegacy.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index 0772c03..73560be1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
@@ -32,6 +32,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -61,6 +62,8 @@
     private ExpandableNotificationRow mSecond;
     @Mock
     private KeyguardBypassController mBypassController;
+    @Mock
+    private FeatureFlags mFeatureFlags;
     private float mSmallRadiusRatio;
 
     @Before
@@ -71,7 +74,8 @@
                 / resources.getDimension(R.dimen.notification_corner_radius);
         mRoundnessManager = new NotificationRoundnessManager(
                 mBypassController,
-                new NotificationSectionsFeatureManager(new DeviceConfigProxy(), mContext));
+                new NotificationSectionsFeatureManager(new DeviceConfigProxy(), mContext),
+                mFeatureFlags);
         allowTestableLooperAsMainThread();
         NotificationTestHelper testHelper = new NotificationTestHelper(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
similarity index 87%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollerControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 42f3889..baed694 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -45,11 +46,11 @@
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
@@ -93,7 +94,7 @@
  */
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-public class NotificationStackScrollerControllerTest extends SysuiTestCase {
+public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
 
     @Mock private NotificationGutsManager mNotificationGutsManager;
     @Mock private HeadsUpManagerPhone mHeadsUpManager;
@@ -130,7 +131,6 @@
     @Mock private ForegroundServiceDungeonView mForegroundServiceDungeonView;
     @Mock private LayoutInflater mLayoutInflater;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
-    @Mock private RemoteInputController mRemoteInputController;
     @Mock private VisualStabilityManager mVisualStabilityManager;
     @Mock private ShadeController mShadeController;
 
@@ -147,7 +147,6 @@
         when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false);
         when(mFgServicesSectionController.createView(mLayoutInflater))
                 .thenReturn(mForegroundServiceDungeonView);
-        when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController);
 
         mController = new NotificationStackScrollLayoutController(
                 true,
@@ -235,16 +234,15 @@
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                true /* visible */,
-
-                true /* notifVisibleInShade */);
+                /* visible= */ true,
+                /* notifVisibleInShade= */ true);
 
         setupShowEmptyShadeViewState(stateListener, false);
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                false /* visible */,
-                true /* notifVisibleInShade */);
+                /* visible= */ false,
+                /* notifVisibleInShade= */ true);
     }
 
     @Test
@@ -260,15 +258,42 @@
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                true /* visible */,
-                false /* notifVisibleInShade */);
+                /* visible= */ true,
+                /* notifVisibleInShade= */ false);
 
         setupShowEmptyShadeViewState(stateListener, false);
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                false /* visible */,
-                false /* notifVisibleInShade */);
+                /* visible= */ false,
+                /* notifVisibleInShade= */ false);
+    }
+
+    @Test
+    public void testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView() {
+        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
+        mController.attach(mNotificationStackScrollLayout);
+        verify(mSysuiStatusBarStateController).addCallback(
+                mStateListenerArgumentCaptor.capture(), anyInt());
+        StatusBarStateController.StateListener stateListener =
+                mStateListenerArgumentCaptor.getValue();
+        when(mNotificationStackScrollLayout.isUsingSplitNotificationShade()).thenReturn(true);
+        stateListener.onStateChanged(SHADE);
+        mController.getView().removeAllViews();
+
+        mController.setQsExpanded(false);
+        reset(mNotificationStackScrollLayout);
+        mController.updateShowEmptyShadeView();
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
+                /* visible= */ true,
+                /* notifVisibleInShade= */ false);
+
+        mController.setQsExpanded(true);
+        reset(mNotificationStackScrollLayout);
+        mController.updateShowEmptyShadeView();
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
+                /* visible= */ true,
+                /* notifVisibleInShade= */ false);
     }
 
     @Test
@@ -379,6 +404,19 @@
                 any(ForegroundServiceDungeonView.class));
     }
 
+    @Test
+    public void testUpdateFooter_remoteInput() {
+        ArgumentCaptor<RemoteInputController.Callback> callbackCaptor =
+                ArgumentCaptor.forClass(RemoteInputController.Callback.class);
+        doNothing().when(mRemoteInputManager).addControllerCallback(callbackCaptor.capture());
+        when(mRemoteInputManager.isRemoteInputActive()).thenReturn(false);
+        mController.attach(mNotificationStackScrollLayout);
+        verify(mNotificationStackScrollLayout).setIsRemoteInputActive(false);
+        RemoteInputController.Callback callback = callbackCaptor.getValue();
+        callback.onRemoteInputActive(true);
+        verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true);
+    }
+
     private LogMaker logMatcher(int category, int type) {
         return argThat(new LogMatcher(category, type));
     }
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 4151ab2..9f42fa4d 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
@@ -52,18 +52,14 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.EmptyShadeView;
-import com.android.systemui.statusbar.FeatureFlags;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.NotificationShelfController;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.FooterView;
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.KeyguardBypassEnabledProvider;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -98,17 +94,12 @@
     @Mock private NotificationGroupManagerLegacy mGroupExpansionManager;
     @Mock private ExpandHelper mExpandHelper;
     @Mock private EmptyShadeView mEmptyShadeView;
-    @Mock private NotificationRemoteInputManager mRemoteInputManager;
-    @Mock private RemoteInputController mRemoteInputController;
     @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
-    @Mock private KeyguardBypassEnabledProvider mKeyguardBypassEnabledProvider;
     @Mock private KeyguardBypassController mBypassController;
     @Mock private NotificationSectionsManager mNotificationSectionsManager;
     @Mock private NotificationSection mNotificationSection;
-    @Mock private SysuiStatusBarStateController mStatusBarStateController;
     @Mock private NotificationSwipeHelper mNotificationSwipeHelper;
     @Mock private NotificationStackScrollLayoutController mStackScrollLayoutController;
-    @Mock private FeatureFlags mFeatureFlags;
     @Mock private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
     @Before
@@ -131,7 +122,6 @@
                 new NotificationSection[]{
                         mNotificationSection
                 });
-        when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController);
 
         // Interact with real instance of AmbientState.
         mAmbientState = new AmbientState(mContext, mNotificationSectionsManager, mBypassController);
@@ -148,10 +138,8 @@
                 mGroupMembershipManger,
                 mGroupExpansionManager,
                 mAmbientState,
-                mFeatureFlags,
                 mUnlockedScreenOffAnimationController);
-        mStackScrollerInternal.initView(getContext(), mKeyguardBypassEnabledProvider,
-                mNotificationSwipeHelper);
+        mStackScrollerInternal.initView(getContext(), mNotificationSwipeHelper);
         mStackScroller = spy(mStackScrollerInternal);
         mStackScroller.setShelfController(notificationShelfController);
         mStackScroller.setStatusBar(mBar);
@@ -159,7 +147,6 @@
         when(mStackScrollLayoutController.getNoticationRoundessManager())
                 .thenReturn(mNotificationRoundnessManager);
         mStackScroller.setController(mStackScrollLayoutController);
-        mStackScroller.setRemoteInputManager(mRemoteInputManager);
 
         // Stub out functionality that isn't necessary to test.
         doNothing().when(mBar)
@@ -233,21 +220,24 @@
     @Test
     @UiThreadTest
     public void testSetExpandedHeight_withSplitShade_doesntInterpolateStackHeight() {
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(true);
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
         final int[] expectedStackHeight = {0};
 
         mStackScroller.addOnExpandedHeightChangedListener((expandedHeight, appear) -> {
             assertWithMessage("Given shade enabled: %s",
-                    mFeatureFlags.isTwoColumnNotificationShadeEnabled())
+                    true)
                     .that(mStackScroller.getHeight())
                     .isEqualTo(expectedStackHeight[0]);
         });
 
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(false);
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_use_split_notification_shade, /* value= */ false);
         expectedStackHeight[0] = 0;
         mStackScroller.setExpandedHeight(100f);
 
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(true);
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
         expectedStackHeight[0] = 100;
         mStackScroller.setExpandedHeight(100f);
     }
@@ -307,7 +297,7 @@
         when(row.canViewBeDismissed()).thenReturn(true);
         when(mStackScroller.getChildCount()).thenReturn(1);
         when(mStackScroller.getChildAt(anyInt())).thenReturn(row);
-        when(mRemoteInputController.isRemoteInputActive()).thenReturn(true);
+        mStackScroller.setIsRemoteInputActive(true);
         when(mStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL))
                 .thenReturn(true);
         when(mStackScrollLayoutController.hasActiveNotifications()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
new file mode 100644
index 0000000..5b60c9e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -0,0 +1,58 @@
+package com.android.systemui.statusbar.notification.stack
+
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.BypassController
+import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+class StackScrollAlgorithmTest : SysuiTestCase() {
+
+    private val hostView = FrameLayout(context)
+    private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
+    private val expandableViewState = ExpandableViewState()
+    private val notificationRow = mock(ExpandableNotificationRow::class.java)
+    private val ambientState = AmbientState(
+            context,
+            SectionProvider { _, _ -> false },
+            BypassController { false })
+
+    @Before
+    fun setUp() {
+        whenever(notificationRow.viewState).thenReturn(expandableViewState)
+        hostView.addView(notificationRow)
+    }
+
+    @Test
+    fun testUpTranslationSetToDefaultValue() {
+        whenever(notificationRow.isPinned).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+
+        stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+        assertThat(expandableViewState.yTranslation).isEqualTo(stackScrollAlgorithm.mHeadsUpInset)
+    }
+
+    @Test
+    fun testHeadsUpTranslationChangesBasedOnStackMargin() {
+        whenever(notificationRow.isPinned).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        val minHeadsUpTranslation = context.resources
+                .getDimensionPixelSize(R.dimen.notification_side_paddings)
+
+        // split shade case with top margin introduced by shade's status bar
+        ambientState.stackTopMargin = 100
+        stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+        // top margin presence should decrease heads up translation up to minHeadsUpTranslation
+        assertThat(expandableViewState.yTranslation).isEqualTo(minHeadsUpTranslation)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLoggerTest.kt
new file mode 100644
index 0000000..f3136c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentLoggerTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.statusbar.DisableFlagsLogger
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.mock
+import java.io.PrintWriter
+import java.io.StringWriter
+
+@SmallTest
+class CollapsedStatusBarFragmentLoggerTest : SysuiTestCase() {
+
+    private val buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+            .create("buffer", 10)
+    private val disableFlagsLogger = DisableFlagsLogger(
+            listOf(DisableFlagsLogger.DisableFlag(0b001, 'A', 'a')),
+            listOf(DisableFlagsLogger.DisableFlag(0b001, 'B', 'b'))
+    )
+    private val logger = CollapsedStatusBarFragmentLogger(buffer, disableFlagsLogger)
+
+    @Test
+    fun logToBuffer_bufferHasStates() {
+        val state = DisableFlagsLogger.DisableState(0, 1)
+
+        logger.logDisableFlagChange(state, state)
+
+        val stringWriter = StringWriter()
+        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+        val actualString = stringWriter.toString()
+        val expectedLogString = disableFlagsLogger.getDisableFlagsString(state, state)
+
+        assertThat(actualString).contains(expectedLogString)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java
index bc53074..f23f148 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java
@@ -17,6 +17,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.mock;
@@ -37,13 +38,17 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiBaseFragmentTest;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.LogcatEchoTracker;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.DisableFlagsLogger;
+import com.android.systemui.statusbar.OperatorNameViewController;
+import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -51,6 +56,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import java.util.Optional;
+
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -69,6 +76,8 @@
 
     private final StatusBar mStatusBar = mock(StatusBar.class);
     private final CommandQueue mCommandQueue = mock(CommandQueue.class);
+    private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
+    private OperatorNameViewController mOperatorNameViewController;
 
     public CollapsedStatusBarFragmentTest() {
         super(CollapsedStatusBarFragment.class);
@@ -76,6 +85,8 @@
 
     @Before
     public void setup() {
+        mStatusBarStateController = mDependency
+                .injectMockDependency(StatusBarStateController.class);
         injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
         when(mStatusBar.getPanelController()).thenReturn(
                 mock(NotificationPanelViewController.class));
@@ -237,6 +248,11 @@
         mNetworkController = mock(NetworkController.class);
         mStatusBarStateController = mock(StatusBarStateController.class);
         mKeyguardStateController = mock(KeyguardStateController.class);
+        mOperatorNameViewController = mock(OperatorNameViewController.class);
+        mOperatorNameViewControllerFactory = mock(OperatorNameViewController.Factory.class);
+        when(mOperatorNameViewControllerFactory.create(any()))
+                .thenReturn(mOperatorNameViewController);
+
         setUpNotificationIconAreaController();
         return new CollapsedStatusBarFragment(
                 mOngoingCallController,
@@ -244,12 +260,17 @@
                 mLocationPublisher,
                 mMockNotificationAreaController,
                 mock(FeatureFlags.class),
+                () -> Optional.of(mStatusBar),
                 mStatusBarIconController,
                 mKeyguardStateController,
                 mNetworkController,
                 mStatusBarStateController,
-                mStatusBar,
-                mCommandQueue);
+                mCommandQueue,
+                new CollapsedStatusBarFragmentLogger(
+                        new LogBuffer("TEST", 1, 1, mock(LogcatEchoTracker.class)),
+                        new DisableFlagsLogger()
+                        ),
+                mOperatorNameViewControllerFactory);
     }
 
     private void setUpNotificationIconAreaController() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
index 5bf1bb3..7a0b366 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
@@ -38,7 +38,7 @@
 import com.android.systemui.doze.AlwaysOnDisplayPolicy;
 import com.android.systemui.doze.DozeScreenState;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.tuner.TunerService;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 8b5ba38..38d7ce7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -164,7 +164,7 @@
     @Test
     public void testPulseWhileDozing_notifyAuthInterrupt() {
         HashSet<Integer> reasonsWantingAuth = new HashSet<>(
-                Collections.singletonList(DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN));
+                Collections.singletonList(DozeLog.PULSE_REASON_SENSOR_WAKE_REACH));
         HashSet<Integer> reasonsSkippingAuth = new HashSet<>(
                 Arrays.asList(DozeLog.PULSE_REASON_INTENT,
                         DozeLog.PULSE_REASON_NOTIFICATION,
@@ -173,7 +173,7 @@
                         DozeLog.REASON_SENSOR_DOUBLE_TAP,
                         DozeLog.PULSE_REASON_SENSOR_LONG_PRESS,
                         DozeLog.PULSE_REASON_DOCKING,
-                        DozeLog.REASON_SENSOR_WAKE_UP,
+                        DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE,
                         DozeLog.REASON_SENSOR_QUICK_PICKUP,
                         DozeLog.REASON_SENSOR_TAP));
         HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
index bca1227..bafbccd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
@@ -198,7 +198,6 @@
         mHeadsUpAppearanceController.destroy();
         verify(mHeadsUpManager).removeListener(any());
         verify(mDarkIconDispatcher).removeDarkReceiver((DarkIconDispatcher.DarkReceiver) any());
-        verify(mPanelView).setVerticalTranslationListener(isNull());
         verify(mPanelView).removeTrackingHeadsUpListener(any());
         verify(mPanelView).setHeadsUpAppearanceController(isNull());
         verify(mStackScrollerController).removeOnExpandedHeightChangedListener(any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index 1043faa..d098e1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -45,30 +45,27 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class KeyguardClockPositionAlgorithmTest extends SysuiTestCase {
-
     private static final int SCREEN_HEIGHT = 2000;
     private static final int EMPTY_HEIGHT = 0;
     private static final float ZERO_DRAG = 0.f;
     private static final float OPAQUE = 1.f;
     private static final float TRANSPARENT = 0.f;
-    private static final boolean HAS_CUSTOM_CLOCK = false;
-    private static final boolean HAS_VISIBLE_NOTIFS = false;
 
     @Mock
     private Resources mResources;
 
     private KeyguardClockPositionAlgorithm mClockPositionAlgorithm;
     private KeyguardClockPositionAlgorithm.Result mClockPosition;
+
     private MockitoSession mStaticMockSession;
     private int mNotificationStackHeight;
+
     private float mPanelExpansion;
     private int mKeyguardStatusBarHeaderHeight;
     private int mKeyguardStatusHeight;
     private float mDark;
-    private boolean mHasCustomClock;
-    private boolean mHasVisibleNotifs;
     private float mQsExpansion;
-    private int mCutoutTopInset = 0; // in pixels
+    private int mCutoutTopInset = 0;
     private boolean mIsSplitShade = false;
     private float mUdfpsTop = -1;
     private float mClockBottom = SCREEN_HEIGHT / 2;
@@ -86,9 +83,6 @@
         mClockPositionAlgorithm.loadDimens(mResources);
 
         mClockPosition = new KeyguardClockPositionAlgorithm.Result();
-
-        mHasCustomClock = HAS_CUSTOM_CLOCK;
-        mHasVisibleNotifs = HAS_VISIBLE_NOTIFS;
     }
 
     @After
@@ -98,9 +92,8 @@
 
     @Test
     public void clockPositionTopOfScreenOnAOD() {
-        // GIVEN on AOD and both stack scroll and clock have 0 height
+        // GIVEN on AOD and clock has 0 height
         givenAOD();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         // WHEN the clock position algorithm is run
         positionClock();
@@ -113,9 +106,8 @@
 
     @Test
     public void clockPositionBelowCutout() {
-        // GIVEN on AOD and both stack scroll and clock have 0 height
+        // GIVEN on AOD and clock has 0 height
         givenAOD();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         mCutoutTopInset = 300;
         // WHEN the clock position algorithm is run
@@ -131,7 +123,6 @@
     public void clockPositionAdjustsForKeyguardStatusOnAOD() {
         // GIVEN on AOD with a clock of height 100
         givenAOD();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = 100;
         // WHEN the clock position algorithm is run
         positionClock();
@@ -146,7 +137,6 @@
     public void clockPositionLargeClockOnAOD() {
         // GIVEN on AOD with a full screen clock
         givenAOD();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = SCREEN_HEIGHT;
         // WHEN the clock position algorithm is run
         positionClock();
@@ -159,9 +149,8 @@
 
     @Test
     public void clockPositionTopOfScreenOnLockScreen() {
-        // GIVEN on lock screen with stack scroll and clock of 0 height
+        // GIVEN on lock screen with clock of 0 height
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         // WHEN the clock position algorithm is run
         positionClock();
@@ -172,24 +161,9 @@
     }
 
     @Test
-    public void clockPositionWithStackScrollExpandOnLockScreen() {
-        // GIVEN on lock screen with stack scroll of height 500
-        givenLockScreen();
-        mNotificationStackHeight = 500;
-        mKeyguardStatusHeight = EMPTY_HEIGHT;
-        // WHEN the clock position algorithm is run
-        positionClock();
-        // THEN the clock Y position stays to the top
-        assertThat(mClockPosition.clockY).isEqualTo(0);
-        // AND the clock is positioned on the left.
-        assertThat(mClockPosition.clockX).isEqualTo(0);
-    }
-
-    @Test
     public void clockPositionWithPartialDragOnLockScreen() {
         // GIVEN dragging up on lock screen
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         mPanelExpansion = 0.5f;
         // WHEN the clock position algorithm is run
@@ -205,7 +179,6 @@
     public void clockPositionWithFullDragOnLockScreen() {
         // GIVEN the lock screen is dragged up
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         mPanelExpansion = 0.f;
         // WHEN the clock position algorithm is run
@@ -218,7 +191,6 @@
     public void largeClockOnLockScreenIsTransparent() {
         // GIVEN on lock screen with a full screen clock
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = SCREEN_HEIGHT;
         // WHEN the clock position algorithm is run
         positionClock();
@@ -228,9 +200,8 @@
 
     @Test
     public void notifPositionTopOfScreenOnAOD() {
-        // GIVEN on AOD and both stack scroll and clock have 0 height
+        // GIVEN on AOD and clock has 0 height
         givenAOD();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         // WHEN the position algorithm is run
         positionClock();
@@ -242,7 +213,6 @@
     public void notifPositionIndependentOfKeyguardStatusHeightOnAOD() {
         // GIVEN on AOD and clock has a nonzero height
         givenAOD();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = 100;
         // WHEN the position algorithm is run
         positionClock();
@@ -254,7 +224,6 @@
     public void notifPositionWithLargeClockOnAOD() {
         // GIVEN on AOD and clock has a nonzero height
         givenAOD();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = SCREEN_HEIGHT;
         // WHEN the position algorithm is run
         positionClock();
@@ -264,9 +233,8 @@
 
     @Test
     public void notifPositionMiddleOfScreenOnLockScreen() {
-        // GIVEN on lock screen and both stack scroll and clock have 0 height
+        // GIVEN on lock screen and clock has 0 height
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         // WHEN the position algorithm is run
         positionClock();
@@ -275,47 +243,20 @@
     }
 
     @Test
-    public void notifPositionAdjustsForStackHeightOnLockScreen() {
-        // GIVEN on lock screen and stack scroller has a nonzero height
-        givenLockScreen();
-        mNotificationStackHeight = 500;
-        mKeyguardStatusHeight = EMPTY_HEIGHT;
-        // WHEN the position algorithm is run
-        positionClock();
-        // THEN the notif padding adjusts for keyguard status height
-        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(0);
-    }
-
-    @Test
     public void notifPositionAdjustsForClockHeightOnLockScreen() {
         // GIVEN on lock screen and stack scroller has a nonzero height
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = 200;
         // WHEN the position algorithm is run
         positionClock();
-        // THEN the notif padding adjusts for both clock and notif stack.
-        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(200);
-    }
-
-    @Test
-    public void notifPositionAdjustsForStackHeightAndClockHeightOnLockScreen() {
-        // GIVEN on lock screen and stack scroller has a nonzero height
-        givenLockScreen();
-        mNotificationStackHeight = 500;
-        mKeyguardStatusHeight = 200;
-        // WHEN the position algorithm is run
-        positionClock();
-        // THEN the notifs are placed below the statusview
         assertThat(mClockPosition.stackScrollerPadding).isEqualTo(200);
     }
 
     @Test
     public void notifPositionAlignedWithClockInSplitShadeMode() {
-        // GIVEN on lock screen and split shade mode
         givenLockScreen();
         mIsSplitShade = true;
-        mHasCustomClock = true;
+        mKeyguardStatusHeight = 200;
         // WHEN the position algorithm is run
         positionClock();
         // THEN the notif padding DOESN'T adjust for keyguard status height.
@@ -326,7 +267,6 @@
     public void notifPositionWithLargeClockOnLockScreen() {
         // GIVEN on lock screen and clock has a nonzero height
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = SCREEN_HEIGHT;
         // WHEN the position algorithm is run
         positionClock();
@@ -338,7 +278,6 @@
     public void notifPositionWithFullDragOnLockScreen() {
         // GIVEN the lock screen is dragged up
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = EMPTY_HEIGHT;
         mPanelExpansion = 0.f;
         // WHEN the clock position algorithm is run
@@ -351,19 +290,17 @@
     public void notifPositionWithLargeClockFullDragOnLockScreen() {
         // GIVEN the lock screen is dragged up and a full screen clock
         givenLockScreen();
-        mNotificationStackHeight = EMPTY_HEIGHT;
         mKeyguardStatusHeight = SCREEN_HEIGHT;
         mPanelExpansion = 0.f;
         // WHEN the clock position algorithm is run
         positionClock();
-        // THEN the notif padding is zero.
         assertThat(mClockPosition.stackScrollerPadding).isEqualTo(
                 (int) (mKeyguardStatusHeight * .667f));
     }
 
     @Test
     public void clockHiddenWhenQsIsExpanded() {
-        // GIVEN on the lock screen with a custom clock and visible notifications
+        // GIVEN on the lock screen with visible notifications
         givenLockScreen();
         mQsExpansion = 1;
         // WHEN the clock position algorithm is run
@@ -397,8 +334,8 @@
         givenLowestBurnInOffset();
         positionClock();
 
-        // THEN lowest case starts at mCutoutTopInset
-        assertThat(mClockPosition.clockY).isEqualTo(mCutoutTopInset);
+        // THEN lowest case starts at 0
+        assertThat(mClockPosition.clockY).isEqualTo(0);
     }
 
     @Test
@@ -516,7 +453,7 @@
     }
 
     private void givenMaxBurnInOffset(int offset) {
-        when(mResources.getDimensionPixelSize(R.dimen.burn_in_prevention_offset_y_large_clock))
+        when(mResources.getDimensionPixelSize(R.dimen.burn_in_prevention_offset_y_clock))
                 .thenReturn(offset);
         mClockPositionAlgorithm.loadDimens(mResources);
     }
@@ -539,15 +476,10 @@
     private void positionClock() {
         mClockPositionAlgorithm.setup(
                 mKeyguardStatusBarHeaderHeight,
-                SCREEN_HEIGHT,
-                mNotificationStackHeight,
                 mPanelExpansion,
-                SCREEN_HEIGHT,
                 mKeyguardStatusHeight,
                 0 /* userSwitchHeight */,
                 0 /* userSwitchPreferredY */,
-                mHasCustomClock,
-                mHasVisibleNotifs,
                 mDark,
                 ZERO_DRAG,
                 false /* bypassEnabled */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
new file mode 100644
index 0000000..faf968b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.keyguard.CarrierTextController;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.battery.BatteryMeterViewController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.UserInfoController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
+    @Mock
+    private CarrierTextController mCarrierTextController;
+    @Mock
+    private ConfigurationController mConfigurationController;
+    @Mock
+    private SystemStatusAnimationScheduler mAnimationScheduler;
+    @Mock
+    private BatteryController mBatteryController;
+    @Mock
+    private UserInfoController mUserInfoController;
+    @Mock
+    private StatusBarIconController mStatusBarIconController;
+    @Mock
+    private FeatureFlags mFeatureFlags;
+    @Mock
+    private BatteryMeterViewController mBatteryMeterViewController;
+    @Mock
+    private KeyguardStateController mKeyguardStateController;
+    @Mock
+    private KeyguardBypassController mKeyguardBypassController;
+    @Mock
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock
+    private BiometricUnlockController mBiometricUnlockController;
+    @Mock
+    private SysuiStatusBarStateController mStatusBarStateController;
+
+    private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider;
+    private KeyguardStatusBarView mKeyguardStatusBarView;
+    private KeyguardStatusBarViewController mController;
+
+    @Before
+    public void setup() throws Exception {
+        mNotificationPanelViewStateProvider = new TestNotificationPanelViewStateProvider();
+
+        MockitoAnnotations.initMocks(this);
+
+        allowTestableLooperAsMainThread();
+        TestableLooper.get(this).runWithLooper(() -> {
+            mKeyguardStatusBarView =
+                    (KeyguardStatusBarView) LayoutInflater.from(mContext)
+                            .inflate(R.layout.keyguard_status_bar, null);
+        });
+
+        mController = new KeyguardStatusBarViewController(
+                mKeyguardStatusBarView,
+                mCarrierTextController,
+                mConfigurationController,
+                mAnimationScheduler,
+                mBatteryController,
+                mUserInfoController,
+                mStatusBarIconController,
+                new StatusBarIconController.TintedIconManager.Factory(mFeatureFlags),
+                mBatteryMeterViewController,
+                mNotificationPanelViewStateProvider,
+                mKeyguardStateController,
+                mKeyguardBypassController,
+                mKeyguardUpdateMonitor,
+                mBiometricUnlockController,
+                mStatusBarStateController
+        );
+    }
+
+    @Test
+    public void onViewAttached_callbacksRegistered() {
+        mController.onViewAttached();
+
+        verify(mConfigurationController).addCallback(any());
+        verify(mAnimationScheduler).addCallback(any());
+        verify(mUserInfoController).addCallback(any());
+        verify(mStatusBarIconController).addIconGroup(any());
+    }
+
+    @Test
+    public void onViewDetached_callbacksUnregistered() {
+        // Set everything up first.
+        mController.onViewAttached();
+
+        mController.onViewDetached();
+
+        verify(mConfigurationController).removeCallback(any());
+        verify(mAnimationScheduler).removeCallback(any());
+        verify(mUserInfoController).removeCallback(any());
+        verify(mStatusBarIconController).removeIconGroup(any());
+    }
+
+    @Test
+    public void setBatteryListening_true_callbackAdded() {
+        mController.setBatteryListening(true);
+
+        verify(mBatteryController).addCallback(any());
+    }
+
+    @Test
+    public void setBatteryListening_false_callbackRemoved() {
+        // First set to true so that we know setting to false is a change in state.
+        mController.setBatteryListening(true);
+
+        mController.setBatteryListening(false);
+
+        verify(mBatteryController).removeCallback(any());
+    }
+
+    @Test
+    public void setBatteryListening_trueThenTrue_callbackAddedOnce() {
+        mController.setBatteryListening(true);
+        mController.setBatteryListening(true);
+
+        verify(mBatteryController).addCallback(any());
+    }
+
+    @Test
+    public void updateTopClipping_viewClippingUpdated() {
+        int viewTop = 20;
+        mKeyguardStatusBarView.setTop(viewTop);
+        int notificationPanelTop = 30;
+
+        mController.updateTopClipping(notificationPanelTop);
+
+        assertThat(mKeyguardStatusBarView.getClipBounds().top).isEqualTo(
+                notificationPanelTop - viewTop);
+    }
+
+    @Test
+    public void setNotTopClipping_viewClippingUpdatedToZero() {
+        // Start out with some amount of top clipping.
+        mController.updateTopClipping(50);
+        assertThat(mKeyguardStatusBarView.getClipBounds().top).isGreaterThan(0);
+
+        mController.setNoTopClipping();
+
+        assertThat(mKeyguardStatusBarView.getClipBounds().top).isEqualTo(0);
+    }
+
+    @Test
+    public void updateViewState_alphaAndVisibilityGiven_viewUpdated() {
+        // Verify the initial values so we know the method triggers changes.
+        assertThat(mKeyguardStatusBarView.getAlpha()).isEqualTo(1f);
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.VISIBLE);
+
+        float newAlpha = 0.5f;
+        int newVisibility = View.INVISIBLE;
+        mController.updateViewState(newAlpha, newVisibility);
+
+        assertThat(mKeyguardStatusBarView.getAlpha()).isEqualTo(newAlpha);
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(newVisibility);
+    }
+
+    @Test
+    public void updateViewState_notKeyguardState_nothingUpdated() {
+        mController.onViewAttached();
+        updateStateToNotKeyguard();
+
+        float oldAlpha = mKeyguardStatusBarView.getAlpha();
+
+        mController.updateViewState();
+
+        assertThat(mKeyguardStatusBarView.getAlpha()).isEqualTo(oldAlpha);
+    }
+
+    @Test
+    public void updateViewState_bypassEnabledAndShouldListenForFace_viewHidden() {
+        mController.onViewAttached();
+        updateStateToKeyguard();
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.VISIBLE);
+
+        when(mKeyguardUpdateMonitor.shouldListenForFace()).thenReturn(true);
+        when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
+        onFinishedGoingToSleep();
+
+        mController.updateViewState();
+
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.INVISIBLE);
+    }
+
+    @Test
+    public void updateViewState_bypassNotEnabled_viewShown() {
+        mController.onViewAttached();
+        updateStateToKeyguard();
+
+        when(mKeyguardUpdateMonitor.shouldListenForFace()).thenReturn(true);
+        when(mKeyguardBypassController.getBypassEnabled()).thenReturn(false);
+        onFinishedGoingToSleep();
+
+        mController.updateViewState();
+
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void updateViewState_shouldNotListenForFace_viewShown() {
+        mController.onViewAttached();
+        updateStateToKeyguard();
+
+        when(mKeyguardUpdateMonitor.shouldListenForFace()).thenReturn(false);
+        when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
+        onFinishedGoingToSleep();
+
+        mController.updateViewState();
+
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void updateViewState_panelExpandedHeightZero_viewHidden() {
+        mController.onViewAttached();
+        updateStateToKeyguard();
+
+        mNotificationPanelViewStateProvider.setPanelViewExpandedHeight(0);
+
+        mController.updateViewState();
+
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.INVISIBLE);
+    }
+
+    @Test
+    public void updateViewState_qsExpansionOne_viewHidden() {
+        mController.onViewAttached();
+        updateStateToKeyguard();
+
+        mNotificationPanelViewStateProvider.setQsExpansionFraction(1f);
+
+        mController.updateViewState();
+
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.INVISIBLE);
+    }
+
+    // TODO(b/195442899): Add more tests for #updateViewState once CLs are finalized.
+
+    @Test
+    public void updateForHeadsUp_headsUpShouldBeVisible_viewHidden() {
+        mController.onViewAttached();
+        updateStateToKeyguard();
+        mKeyguardStatusBarView.setVisibility(View.VISIBLE);
+
+        mNotificationPanelViewStateProvider.setShouldHeadsUpBeVisible(true);
+        mController.updateForHeadsUp(/* animate= */ false);
+
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.INVISIBLE);
+    }
+
+    @Test
+    public void updateForHeadsUp_headsUpShouldNotBeVisible_viewShown() {
+        mController.onViewAttached();
+        updateStateToKeyguard();
+
+        // Start with the opposite state.
+        mNotificationPanelViewStateProvider.setShouldHeadsUpBeVisible(true);
+        mController.updateForHeadsUp(/* animate= */ false);
+
+        mNotificationPanelViewStateProvider.setShouldHeadsUpBeVisible(false);
+        mController.updateForHeadsUp(/* animate= */ false);
+
+        assertThat(mKeyguardStatusBarView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    private void updateStateToNotKeyguard() {
+        updateStatusBarState(SHADE);
+    }
+
+    private void updateStateToKeyguard() {
+        updateStatusBarState(KEYGUARD);
+    }
+
+    private void updateStatusBarState(int state) {
+        ArgumentCaptor<StatusBarStateController.StateListener> statusBarStateListenerCaptor =
+                ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+        verify(mStatusBarStateController).addCallback(statusBarStateListenerCaptor.capture());
+        StatusBarStateController.StateListener callback = statusBarStateListenerCaptor.getValue();
+
+        callback.onStateChanged(state);
+    }
+
+    /**
+     * Calls {@link com.android.keyguard.KeyguardUpdateMonitorCallback#onFinishedGoingToSleep(int)}
+     * to ensure values are updated properly.
+     */
+    private void onFinishedGoingToSleep() {
+        ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateCallbackCaptor =
+                ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+        verify(mKeyguardUpdateMonitor).registerCallback(keyguardUpdateCallbackCaptor.capture());
+        KeyguardUpdateMonitorCallback callback = keyguardUpdateCallbackCaptor.getValue();
+
+        callback.onFinishedGoingToSleep(0);
+    }
+
+    private static class TestNotificationPanelViewStateProvider
+            implements NotificationPanelViewController.NotificationPanelViewStateProvider {
+
+        TestNotificationPanelViewStateProvider() {}
+
+        private float mPanelViewExpandedHeight = 100f;
+        private float mQsExpansionFraction = 0f;
+        private boolean mShouldHeadsUpBeVisible = false;
+
+        @Override
+        public float getPanelViewExpandedHeight() {
+            return mPanelViewExpandedHeight;
+        }
+
+        @Override
+        public float getQsExpansionFraction() {
+            return mQsExpansionFraction;
+        }
+
+        @Override
+        public boolean shouldHeadsUpBeVisible() {
+            return mShouldHeadsUpBeVisible;
+        }
+
+        public void setPanelViewExpandedHeight(float panelViewExpandedHeight) {
+            this.mPanelViewExpandedHeight = panelViewExpandedHeight;
+        }
+
+        public void setQsExpansionFraction(float qsExpansionFraction) {
+            this.mQsExpansionFraction = qsExpansionFraction;
+        }
+
+        public void setShouldHeadsUpBeVisible(boolean shouldHeadsUpBeVisible) {
+            this.mShouldHeadsUpBeVisible = shouldHeadsUpBeVisible;
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
new file mode 100644
index 0000000..3108ed9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.LayoutInflater;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class KeyguardStatusBarViewTest extends SysuiTestCase {
+
+    private KeyguardStatusBarView mKeyguardStatusBarView;
+
+    @Before
+    public void setup() throws Exception {
+        allowTestableLooperAsMainThread();
+        TestableLooper.get(this).runWithLooper(() -> {
+            mKeyguardStatusBarView =
+                    (KeyguardStatusBarView) LayoutInflater.from(mContext)
+                            .inflate(R.layout.keyguard_status_bar, null);
+        });
+    }
+
+    @Test
+    public void setTopClipping_clippingUpdated() {
+        int topClipping = 40;
+
+        mKeyguardStatusBarView.setTopClipping(topClipping);
+
+        assertThat(mKeyguardStatusBarView.getClipBounds().top).isEqualTo(topClipping);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index ccdc69a..7e33c01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -34,6 +34,7 @@
 
 import com.android.internal.view.AppearanceRegion;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.statusbar.policy.BatteryController;
 
@@ -56,8 +57,12 @@
         mLightBarTransitionsController = mock(LightBarTransitionsController.class);
         when(mStatusBarIconController.getTransitionsController()).thenReturn(
                 mLightBarTransitionsController);
-        mLightBarController = new LightBarController(mContext, mStatusBarIconController,
-                mock(BatteryController.class), mock(NavigationModeController.class));
+        mLightBarController = new LightBarController(
+                mContext,
+                mStatusBarIconController,
+                mock(BatteryController.class),
+                mock(NavigationModeController.class),
+                mock(DumpManager.class));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
index cdfab1e..74f08ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
@@ -102,7 +102,8 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                false /* isFullscreen */);
+                null /* requestedVisibilities */,
+                null /* packageName */);
         assertTrue(mLightsOutNotifController.areLightsOut());
     }
 
@@ -114,7 +115,8 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                false /* isFullscreen */);
+                null /* requestedVisibilities */,
+                null /* packageName */);
         assertFalse(mLightsOutNotifController.areLightsOut());
     }
 
@@ -144,7 +146,8 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                false /* isFullscreen */);
+                null /* requestedVisibilities */,
+                null /* packageName */);
 
         // THEN we should show dot
         assertTrue(mLightsOutNotifController.shouldShowDot());
@@ -163,7 +166,8 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                false /* isFullscreen */);
+                null /* requestedVisibilities */,
+                null /* packageName */);
 
         // THEN we shouldn't show the dot
         assertFalse(mLightsOutNotifController.shouldShowDot());
@@ -182,7 +186,8 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                false /* isFullscreen */);
+                null /* requestedVisibilities */,
+                null /* packageName */);
 
         // THEN we shouldn't show the dot
         assertFalse(mLightsOutNotifController.shouldShowDot());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
index 88852f1..80d9c08 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
@@ -37,6 +37,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -93,7 +94,8 @@
         mGroupManager = new NotificationGroupManagerLegacy(
                 mock(StatusBarStateController.class),
                 () -> mPeopleNotificationIdentifier,
-                Optional.of(mock(Bubbles.class)));
+                Optional.of(mock(Bubbles.class)),
+                mock(DumpManager.class));
         mDependency.injectTestDependency(NotificationGroupManagerLegacy.class, mGroupManager);
         mGroupManager.setHeadsUpManager(mHeadsUpManager);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerLegacyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerLegacyTest.java
index 0110d7b..1be27da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerLegacyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerLegacyTest.java
@@ -33,6 +33,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
@@ -76,7 +77,8 @@
         mGroupManager = new NotificationGroupManagerLegacy(
                 mock(StatusBarStateController.class),
                 () -> mPeopleNotificationIdentifier,
-                Optional.of(mock(Bubbles.class)));
+                Optional.of(mock(Bubbles.class)),
+                mock(DumpManager.class));
         mGroupManager.setHeadsUpManager(mHeadsUpManager);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
index f247788..2369d8e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
@@ -18,6 +18,8 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
+import static com.android.keyguard.KeyguardClockSwitch.LARGE;
+import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
@@ -35,6 +37,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -44,7 +47,6 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.hardware.biometrics.BiometricSourceType;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.PowerManager;
@@ -87,6 +89,7 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.doze.DozeLog;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.media.KeyguardMediaController;
@@ -97,7 +100,6 @@
 import com.android.systemui.qs.QSDetailDisplayer;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardAffordanceView;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -106,7 +108,6 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -155,8 +156,6 @@
     private KeyguardBottomAreaView mQsFrame;
     private KeyguardStatusView mKeyguardStatusView;
     @Mock
-    private ViewGroup mBigClockContainer;
-    @Mock
     private NotificationIconAreaController mNotificationAreaController;
     @Mock
     private HeadsUpManagerPhone mHeadsUpManager;
@@ -228,8 +227,6 @@
     @Mock
     private ConversationNotificationManager mConversationNotificationManager;
     @Mock
-    private BiometricUnlockController mBiometricUnlockController;
-    @Mock
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock
     private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
@@ -264,8 +261,6 @@
     @Mock
     private MediaDataManager mMediaDataManager;
     @Mock
-    private FeatureFlags mFeatureFlags;
-    @Mock
     private AmbientState mAmbientState;
     @Mock
     private UserManager mUserManager;
@@ -282,6 +277,8 @@
     @Mock
     private SecureSettings mSecureSettings;
     @Mock
+    private SplitShadeHeaderController mSplitShadeHeaderController;
+    @Mock
     private ContentResolver mContentResolver;
     @Mock
     private TapAgainViewController mTapAgainViewController;
@@ -296,11 +293,15 @@
     @Mock
     private NotificationRemoteInputManager mNotificationRemoteInputManager;
     @Mock
-    private RemoteInputController mRemoteInputController;
-    @Mock
     private RecordingController mRecordingController;
     @Mock
     private ControlsComponent mControlsComponent;
+    @Mock
+    private LockscreenGestureLogger mLockscreenGestureLogger;
+    @Mock
+    private DumpManager mDumpManager;
+    @Mock
+    private NotificationsQSContainerController mNotificationsQSContainerController;
 
     private SysuiStatusBarStateController mStatusBarStateController;
     private NotificationPanelViewController mNotificationPanelViewController;
@@ -312,7 +313,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger);
+        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager);
 
         mKeyguardStatusView = new KeyguardStatusView(mContext);
         mKeyguardStatusView.setId(R.id.keyguard_status_view);
@@ -346,7 +347,6 @@
         when(mKeyguardBottomArea.getLeftView()).thenReturn(mock(KeyguardAffordanceView.class));
         when(mKeyguardBottomArea.getRightView()).thenReturn(mock(KeyguardAffordanceView.class));
         when(mKeyguardBottomArea.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mView.findViewById(R.id.big_clock_container)).thenReturn(mBigClockContainer);
         when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame);
         when(mView.findViewById(R.id.keyguard_status_view))
                 .thenReturn(mock(KeyguardStatusView.class));
@@ -354,6 +354,7 @@
         mNotificationContainerParent.addView(newViewWithId(R.id.qs_frame));
         mNotificationContainerParent.addView(newViewWithId(R.id.notification_stack_scroller));
         mNotificationContainerParent.addView(mKeyguardStatusView);
+        mNotificationContainerParent.onFinishInflate();
         when(mView.findViewById(R.id.notification_container_parent))
                 .thenReturn(mNotificationContainerParent);
         when(mFragmentService.getFragmentHostManager(mView)).thenReturn(mFragmentHostManager);
@@ -368,7 +369,7 @@
         NotificationWakeUpCoordinator coordinator =
                 new NotificationWakeUpCoordinator(
                         mock(HeadsUpManagerPhone.class),
-                        new StatusBarStateControllerImpl(new UiEventLoggerFake()),
+                        new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager),
                         mKeyguardBypassController,
                         mDozeParameters,
                         mUnlockedScreenOffAnimationController);
@@ -389,7 +390,7 @@
                 .thenReturn(mKeyguardClockSwitchController);
         when(mKeyguardStatusViewComponent.getKeyguardStatusViewController())
                 .thenReturn(mKeyguardStatusViewController);
-        when(mKeyguardStatusBarViewComponentFactory.build(any()))
+        when(mKeyguardStatusBarViewComponentFactory.build(any(), any()))
                 .thenReturn(mKeyguardStatusBarViewComponent);
         when(mKeyguardStatusBarViewComponent.getKeyguardStatusBarViewController())
                 .thenReturn(mKeyguardStatusBarViewController);
@@ -397,8 +398,7 @@
                 .thenReturn(mKeyguardStatusView);
         when(mLayoutInflater.inflate(eq(R.layout.keyguard_bottom_area), any(), anyBoolean()))
                 .thenReturn(mKeyguardBottomArea);
-        when(mNotificationRemoteInputManager.getController()).thenReturn(mRemoteInputController);
-        when(mRemoteInputController.isRemoteInputActive()).thenReturn(false);
+        when(mNotificationRemoteInputManager.isRemoteInputActive()).thenReturn(false);
 
         reset(mView);
 
@@ -415,7 +415,8 @@
                 mMetricsLogger, mActivityManager, mConfigurationController,
                 () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
                 mConversationNotificationManager, mMediaHiearchyManager,
-                mBiometricUnlockController, mStatusBarKeyguardViewManager,
+                mStatusBarKeyguardViewManager,
+                mNotificationsQSContainerController,
                 mNotificationStackScrollLayoutController,
                 mKeyguardStatusViewComponentFactory,
                 mKeyguardQsUserSwitchComponentFactory,
@@ -432,7 +433,6 @@
                 mNotificationShadeDepthController,
                 mAmbientState,
                 mLockIconViewController,
-                mFeatureFlags,
                 mKeyguardMediaController,
                 mPrivacyDotViewController,
                 mTapAgainViewController,
@@ -443,11 +443,14 @@
                 mRecordingController,
                 new FakeExecutor(new FakeSystemClock()),
                 mSecureSettings,
+                mSplitShadeHeaderController,
                 mUnlockedScreenOffAnimationController,
+                mLockscreenGestureLogger,
                 mNotificationRemoteInputManager,
                 mControlsComponent);
         mNotificationPanelViewController.initDependencies(
                 mStatusBar,
+                () -> {},
                 mNotificationShelfController);
         mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
         mNotificationPanelViewController.setBar(mPanelBar);
@@ -470,8 +473,8 @@
     }
 
     @Test
-    public void testSetMinFraction() {
-        mNotificationPanelViewController.setMinFraction(0.5f);
+    public void testSetPanelScrimMinFraction() {
+        mNotificationPanelViewController.setPanelScrimMinFraction(0.5f);
         verify(mNotificationShadeDepthController).setPanelPullDownMinFraction(eq(0.5f));
     }
 
@@ -516,17 +519,48 @@
     }
 
     @Test
-    public void testKeyguardStatusBarVisibility_hiddenForBypass() {
-        when(mUpdateMonitor.shouldListenForFace()).thenReturn(true);
-        mNotificationPanelViewController.mKeyguardUpdateCallback.onBiometricRunningStateChanged(
-                true, BiometricSourceType.FACE);
-        verify(mKeyguardStatusBar, never()).setVisibility(View.VISIBLE);
+    public void onTouchForwardedFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
+        when(mCommandQueue.panelsEnabled()).thenReturn(false);
 
-        when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
-        mNotificationPanelViewController.mKeyguardUpdateCallback.onFinishedGoingToSleep(0);
-        mNotificationPanelViewController.mKeyguardUpdateCallback.onBiometricRunningStateChanged(
-                true, BiometricSourceType.FACE);
-        verify(mKeyguardStatusBar, never()).setVisibility(View.VISIBLE);
+        boolean returnVal = mTouchHandler.onTouchForwardedFromStatusBar(
+                MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0));
+
+        assertThat(returnVal).isFalse();
+        verify(mView, never()).dispatchTouchEvent(any());
+    }
+
+    @Test
+    public void onTouchForwardedFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() {
+        when(mCommandQueue.panelsEnabled()).thenReturn(true);
+        when(mView.isEnabled()).thenReturn(false);
+
+        boolean returnVal = mTouchHandler.onTouchForwardedFromStatusBar(
+                MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0));
+
+        assertThat(returnVal).isTrue();
+        verify(mView, never()).dispatchTouchEvent(any());
+    }
+
+    @Test
+    public void onTouchForwardedFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() {
+        when(mCommandQueue.panelsEnabled()).thenReturn(true);
+        when(mView.isEnabled()).thenReturn(false);
+        MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0);
+
+        mTouchHandler.onTouchForwardedFromStatusBar(event);
+
+        verify(mView).dispatchTouchEvent(event);
+    }
+
+    @Test
+    public void onTouchForwardedFromStatusBar_panelAndViewEnabled_viewReceivesEvent() {
+        when(mCommandQueue.panelsEnabled()).thenReturn(true);
+        when(mView.isEnabled()).thenReturn(true);
+        MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0);
+
+        mTouchHandler.onTouchForwardedFromStatusBar(event);
+
+        verify(mView).dispatchTouchEvent(event);
     }
 
     @Test
@@ -564,7 +598,7 @@
 
     @Test
     public void testAllChildrenOfNotificationContainer_haveIds() {
-        enableSplitShade();
+        enableSplitShade(/* enabled= */ true);
         mNotificationContainerParent.removeAllViews();
         mNotificationContainerParent.addView(newViewWithId(1));
         mNotificationContainerParent.addView(newViewWithId(View.NO_ID));
@@ -577,7 +611,7 @@
 
     @Test
     public void testSinglePaneShadeLayout_isAlignedToParent() {
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(false);
+        enableSplitShade(/* enabled= */ false);
 
         mNotificationPanelViewController.updateResources();
 
@@ -588,17 +622,19 @@
     }
 
     @Test
-    public void testKeyguardStatusView_isAlignedToGuidelineInSplitShadeMode() {
+    public void testKeyguardStatusViewInSplitShade_changesConstraintsDependingOnNotifications() {
+        mStatusBarStateController.setState(KEYGUARD);
+        enableSplitShade(/* enabled= */ true);
+
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         mNotificationPanelViewController.updateResources();
-
-        assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd)
-                .isEqualTo(ConstraintSet.PARENT_ID);
-
-        enableSplitShade();
-        mNotificationPanelViewController.updateResources();
-
         assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd)
                 .isEqualTo(R.id.qs_edge_guideline);
+
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+        mNotificationPanelViewController.updateResources();
+        assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd)
+                .isEqualTo(ConstraintSet.PARENT_ID);
     }
 
     @Test
@@ -635,7 +671,7 @@
 
     @Test
     public void testSplitShadeLayout_isAlignedToGuideline() {
-        enableSplitShade();
+        enableSplitShade(/* enabled= */ true);
 
         mNotificationPanelViewController.updateResources();
 
@@ -647,7 +683,7 @@
 
     @Test
     public void testSinglePaneShadeLayout_childrenHaveConstantWidth() {
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(false);
+        enableSplitShade(/* enabled= */ false);
 
         mNotificationPanelViewController.updateResources();
 
@@ -659,7 +695,7 @@
 
     @Test
     public void testSplitShadeLayout_childrenHaveZeroWidth() {
-        enableSplitShade();
+        enableSplitShade(/* enabled= */ true);
 
         mNotificationPanelViewController.updateResources();
 
@@ -668,18 +704,6 @@
     }
 
     @Test
-    public void testOnDragDownEvent_horizontalTranslationIsZeroForSplitShade() {
-        when(mNotificationStackScrollLayoutController.getWidth()).thenReturn(350f);
-        when(mView.getWidth()).thenReturn(800);
-        enableSplitShade();
-
-        onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN,
-                200f /* x position */, 0f, 0));
-
-        verify(mQsFrame).setTranslationX(0);
-    }
-
-    @Test
     public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
         mStatusBarStateController.setState(KEYGUARD);
 
@@ -705,7 +729,7 @@
     @Test
     public void testCanCollapsePanelOnTouch_falseInDualPaneShade() {
         mStatusBarStateController.setState(SHADE);
-        enableSplitShade();
+        enableSplitShade(/* enabled= */ true);
         mNotificationPanelViewController.setQsExpanded(true);
 
         assertThat(mNotificationPanelViewController.canCollapsePanelOnTouch()).isFalse();
@@ -759,6 +783,68 @@
         verify(mTapAgainViewController).show();
     }
 
+    @Test
+    public void testSwitchesToCorrectClockInSinglePaneShade() {
+        mStatusBarStateController.setState(KEYGUARD);
+
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+        triggerPositionClockAndNotifications();
+        verify(mKeyguardStatusViewController).displayClock(LARGE);
+
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+        mNotificationPanelViewController.closeQs();
+        verify(mKeyguardStatusViewController).displayClock(SMALL);
+    }
+
+    @Test
+    public void testSwitchesToCorrectClockInSplitShade() {
+        mStatusBarStateController.setState(KEYGUARD);
+        enableSplitShade(/* enabled= */ true);
+
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+        triggerPositionClockAndNotifications();
+        verify(mKeyguardStatusViewController).displayClock(LARGE);
+
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+        triggerPositionClockAndNotifications();
+        verify(mKeyguardStatusViewController, times(2)).displayClock(LARGE);
+        verify(mKeyguardStatusViewController, never()).displayClock(SMALL);
+    }
+
+    @Test
+    public void testSwitchesToBigClockInSplitShadeOnAod() {
+        mStatusBarStateController.setState(KEYGUARD);
+        enableSplitShade(/* enabled= */ true);
+        when(mMediaDataManager.hasActiveMedia()).thenReturn(true);
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+
+        mNotificationPanelViewController.setDozing(true, false, null);
+
+        verify(mKeyguardStatusViewController).displayClock(LARGE);
+    }
+
+    @Test
+    public void testDisplaysSmallClockOnLockscreenInSplitShadeWhenMediaIsPlaying() {
+        mStatusBarStateController.setState(KEYGUARD);
+        enableSplitShade(/* enabled= */ true);
+        when(mMediaDataManager.hasActiveMedia()).thenReturn(true);
+
+        // one notification + media player visible
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+        triggerPositionClockAndNotifications();
+        verify(mKeyguardStatusViewController).displayClock(SMALL);
+
+        // only media player visible
+        when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+        triggerPositionClockAndNotifications();
+        verify(mKeyguardStatusViewController, times(2)).displayClock(SMALL);
+        verify(mKeyguardStatusViewController, never()).displayClock(LARGE);
+    }
+
+    private void triggerPositionClockAndNotifications() {
+        mNotificationPanelViewController.closeQs();
+    }
+
     private FalsingManager.FalsingTapListener getFalsingTapListener() {
         for (View.OnAttachStateChangeListener listener : mOnAttachStateChangeListeners) {
             listener.onViewAttachedToWindow(mView);
@@ -789,9 +875,8 @@
         return constraintSet.getConstraint(id).layout;
     }
 
-    private void enableSplitShade() {
-        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(true);
-        when(mFeatureFlags.isTwoColumnNotificationShadeEnabled()).thenReturn(true);
+    private void enableSplitShade(boolean enabled) {
+        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(enabled);
         mNotificationPanelViewController.updateResources();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt
new file mode 100644
index 0000000..337e645
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt
@@ -0,0 +1,254 @@
+package com.android.systemui.statusbar.phone
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.WindowInsets
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
+import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.util.function.Consumer
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NotificationQSContainerControllerTest : SysuiTestCase() {
+
+    companion object {
+        const val STABLE_INSET_BOTTOM = 100
+        const val CUTOUT_HEIGHT = 50
+        const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
+        const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
+        const val NOTIFICATIONS_MARGIN = 50
+    }
+
+    @Mock
+    private lateinit var navigationModeController: NavigationModeController
+    @Mock
+    private lateinit var overviewProxyService: OverviewProxyService
+    @Mock
+    private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
+    @Captor
+    lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
+    @Captor
+    lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
+    @Captor
+    lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
+
+    private lateinit var notificationsQSContainerController: NotificationsQSContainerController
+    private lateinit var navigationModeCallback: ModeChangedListener
+    private lateinit var taskbarVisibilityCallback: OverviewProxyListener
+    private lateinit var windowInsetsCallback: Consumer<WindowInsets>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        notificationsQSContainerController = NotificationsQSContainerController(
+                notificationsQSContainer,
+                navigationModeController,
+                overviewProxyService
+        )
+        whenever(notificationsQSContainer.defaultNotificationsMarginBottom)
+                .thenReturn(NOTIFICATIONS_MARGIN)
+        whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
+                .thenReturn(GESTURES_NAVIGATION)
+        doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
+        doNothing().`when`(notificationsQSContainer)
+                .setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
+
+        notificationsQSContainerController.init()
+        notificationsQSContainerController.onViewAttached()
+
+        navigationModeCallback = navigationModeCaptor.value
+        taskbarVisibilityCallback = taskbarVisibilityCaptor.value
+        windowInsetsCallback = windowInsetsCallbackCaptor.value
+    }
+
+    @Test
+    fun testTaskbarVisibleInSplitShade() {
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = true,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
+                expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
+
+        given(taskbarVisible = true,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = STABLE_INSET_BOTTOM,
+                expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShade() {
+        // when taskbar is not visible, it means we're on the home screen
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
+                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShadeWithCutout() {
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withCutout())
+        then(expectedContainerPadding = CUTOUT_HEIGHT)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withCutout().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
+    }
+
+    @Test
+    fun testTaskbarVisibleInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        given(taskbarVisible = true,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0)
+
+        given(taskbarVisible = true,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = STABLE_INSET_BOTTOM)
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withCutout().withStableBottom())
+        then(expectedContainerPadding = CUTOUT_HEIGHT)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedQsPadding = STABLE_INSET_BOTTOM)
+    }
+
+    @Test
+    fun testCustomizingInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        notificationsQSContainerController.setCustomizerShowing(true)
+        // always sets spacings to 0
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+    }
+
+    @Test
+    fun testDetailShowingInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        notificationsQSContainerController.setDetailShowing(true)
+        // always sets spacings to 0
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+    }
+
+    @Test
+    fun testDetailShowingInSplitShade() {
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0)
+
+        notificationsQSContainerController.setDetailShowing(true)
+        // should not influence spacing
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+    }
+
+    private fun given(
+        taskbarVisible: Boolean,
+        navigationMode: Int,
+        insets: WindowInsets
+    ) {
+        Mockito.clearInvocations(notificationsQSContainer)
+        taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
+        navigationModeCallback.onNavigationModeChanged(navigationMode)
+        windowInsetsCallback.accept(insets)
+    }
+
+    fun then(
+        expectedContainerPadding: Int,
+        expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
+        expectedQsPadding: Int = 0
+    ) {
+        verify(notificationsQSContainer)
+                .setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
+        verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin)
+        verify(notificationsQSContainer).setQSScrollPaddingBottom(expectedQsPadding)
+        Mockito.clearInvocations(notificationsQSContainer)
+    }
+
+    private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
+
+    private fun emptyInsets() = mock(WindowInsets::class.java)
+
+    private fun WindowInsets.withCutout(): WindowInsets {
+        whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
+        return this
+    }
+
+    private fun WindowInsets.withStableBottom(): WindowInsets {
+        whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
+        return this
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
index aafaebd..c62d73c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
@@ -47,7 +47,6 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -93,7 +92,7 @@
     @Mock private NotificationPanelViewController mNotificationPanelViewController;
     @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout;
     @Mock private NotificationShadeDepthController mNotificationShadeDepthController;
-    @Mock private SuperStatusBarViewFactory mStatusBarViewFactory;
+    @Mock private StatusBarWindowView mStatusBarWindowView;
     @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
     @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -142,7 +141,7 @@
                 mNotificationShadeDepthController,
                 mView,
                 mNotificationPanelViewController,
-                mStatusBarViewFactory,
+                mStatusBarWindowView,
                 mNotificationStackScrollLayoutController,
                 mStatusBarKeyguardViewManager,
                 mLockIconViewController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
new file mode 100644
index 0000000..033f689
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class PhoneStatusBarViewControllerTest : SysuiTestCase() {
+
+    private val touchEventHandler = TestTouchEventHandler()
+
+    @Mock
+    private lateinit var panelViewController: PanelViewController
+    @Mock
+    private lateinit var panelView: ViewGroup
+    @Mock
+    private lateinit var scrimController: ScrimController
+
+    @Mock
+    private lateinit var moveFromCenterAnimation: StatusBarMoveFromCenterAnimationController
+
+    private lateinit var view: PhoneStatusBarView
+    private lateinit var controller: PhoneStatusBarViewController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        `when`(panelViewController.view).thenReturn(panelView)
+
+        // create the view on main thread as it requires main looper
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val parent = FrameLayout(mContext) // add parent to keep layout params
+            view = LayoutInflater.from(mContext)
+                .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView
+            view.setScrimController(scrimController)
+            view.setBar(mock(StatusBar::class.java))
+        }
+
+        controller = PhoneStatusBarViewController(
+                view,
+                null,
+                touchEventHandler,
+        )
+    }
+
+    @Test
+    fun constructor_setsTouchHandlerOnView() {
+        val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+        view.onTouchEvent(event)
+
+        assertThat(touchEventHandler.lastEvent).isEqualTo(event)
+    }
+
+    @Test
+    fun constructor_moveFromCenterAnimationIsNotNull_moveFromCenterAnimationInitialized() {
+        controller = PhoneStatusBarViewController(
+                view, moveFromCenterAnimation, touchEventHandler
+        )
+
+        verify(moveFromCenterAnimation).init(any(), any())
+    }
+
+    private class TestTouchEventHandler : PhoneStatusBarView.TouchEventHandler {
+        var lastEvent: MotionEvent? = null
+        override fun handleTouchEvent(event: MotionEvent?): Boolean {
+            lastEvent = event
+            return false
+        }
+
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
new file mode 100644
index 0000000..fe34903
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.view.MotionEvent
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class PhoneStatusBarViewTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var panelViewController: PanelViewController
+    @Mock
+    private lateinit var panelView: ViewGroup
+    @Mock
+    private lateinit var scrimController: ScrimController
+    @Mock
+    private lateinit var statusBar: StatusBar
+
+    private lateinit var view: PhoneStatusBarView
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        // TODO(b/197137564): Setting up a panel view and its controller feels unnecessary when
+        //   testing just [PhoneStatusBarView].
+        `when`(panelViewController.view).thenReturn(panelView)
+
+        view = PhoneStatusBarView(mContext, null)
+        view.setScrimController(scrimController)
+        view.setBar(statusBar)
+    }
+
+    @Test
+    fun panelExpansionChanged_expansionChangeListenerNotified() {
+        val listener = TestExpansionChangedListener()
+        view.setExpansionChangedListeners(listOf(listener))
+        val fraction = 0.4f
+        val isExpanded = true
+
+        view.panelExpansionChanged(fraction, isExpanded)
+
+        assertThat(listener.fraction).isEqualTo(fraction)
+        assertThat(listener.isExpanded).isEqualTo(isExpanded)
+    }
+
+    @Test
+    fun panelExpansionChanged_noListeners_noCrash() {
+        view.panelExpansionChanged(1f, false)
+        // No assert needed, just testing no crash
+    }
+
+    @Test
+    fun panelStateChanged_toStateOpening_listenerNotified() {
+        val listener = TestStateChangedListener()
+        view.setPanelStateChangeListener(listener)
+
+        view.panelExpansionChanged(0.5f, true)
+
+        assertThat(listener.state).isEqualTo(PanelBar.STATE_OPENING)
+    }
+
+    @Test
+    fun panelStateChanged_toStateOpen_listenerNotified() {
+        val listener = TestStateChangedListener()
+        view.setPanelStateChangeListener(listener)
+
+        view.panelExpansionChanged(1f, true)
+
+        assertThat(listener.state).isEqualTo(PanelBar.STATE_OPEN)
+    }
+
+    @Test
+    fun panelStateChanged_toStateClosed_listenerNotified() {
+        val listener = TestStateChangedListener()
+        view.setPanelStateChangeListener(listener)
+
+        // First, open the panel
+        view.panelExpansionChanged(1f, true)
+
+        // Then, close it again
+        view.panelExpansionChanged(0f, false)
+
+        assertThat(listener.state).isEqualTo(PanelBar.STATE_CLOSED)
+    }
+
+    @Test
+    fun onTouchEvent_listenerNotified() {
+        val handler = TestTouchEventHandler()
+        view.setTouchEventHandler(handler)
+
+        val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+        view.onTouchEvent(event)
+
+        assertThat(handler.lastEvent).isEqualTo(event)
+    }
+
+    @Test
+    fun onTouchEvent_listenerReturnsTrue_viewReturnsTrue() {
+        val handler = TestTouchEventHandler()
+        view.setTouchEventHandler(handler)
+        val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+        handler.returnValue = true
+
+        assertThat(view.onTouchEvent(event)).isTrue()
+    }
+
+    @Test
+    fun onTouchEvent_listenerReturnsFalse_viewReturnsFalse() {
+        val handler = TestTouchEventHandler()
+        view.setTouchEventHandler(handler)
+        val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+        handler.returnValue = false
+
+        assertThat(view.onTouchEvent(event)).isFalse()
+    }
+
+    @Test
+    fun onTouchEvent_noListener_noCrash() {
+        view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
+        // No assert needed, just testing no crash
+    }
+
+    private class TestExpansionChangedListener
+        : StatusBar.ExpansionChangedListener {
+        var fraction: Float = 0f
+        var isExpanded: Boolean = false
+
+        override fun onExpansionChanged(expansion: Float, expanded: Boolean) {
+            this.fraction = expansion
+            this.isExpanded = expanded
+        }
+    }
+
+    private class TestStateChangedListener : PanelBar.PanelStateChangeListener {
+        var state: Int = 0
+        override fun onStateChanged(state: Int) {
+            this.state = state
+        }
+    }
+
+    private class TestTouchEventHandler : PhoneStatusBarView.TouchEventHandler {
+        var lastEvent: MotionEvent? = null
+        var returnValue: Boolean = false
+        override fun handleTouchEvent(event: MotionEvent?): Boolean {
+            lastEvent = event
+            return returnValue
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 47c8806..6849dab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -86,7 +86,6 @@
     private ScrimView mScrimBehind;
     private ScrimView mNotificationsScrim;
     private ScrimView mScrimInFront;
-    private ScrimView mScrimForBubble;
     private ScrimState mScrimState;
     private float mScrimBehindAlpha;
     private GradientColors mScrimInFrontColor;
@@ -170,7 +169,6 @@
         endAnimation(mNotificationsScrim);
         endAnimation(mScrimBehind);
         endAnimation(mScrimInFront);
-        endAnimation(mScrimForBubble);
 
         assertEquals("Animators did not finish",
                 mAnimatorListener.getNumStarts(), mAnimatorListener.getNumEnds());
@@ -193,7 +191,6 @@
 
         mScrimBehind = spy(new ScrimView(getContext()));
         mScrimInFront = new ScrimView(getContext());
-        mScrimForBubble = new ScrimView(getContext());
         mNotificationsScrim = new ScrimView(getContext());
         mAlwaysOnEnabled = true;
         mLooper = TestableLooper.get(this);
@@ -229,8 +226,7 @@
                 mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()),
                 mUnlockedScreenOffAnimationController);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
-        mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront,
-                mScrimForBubble);
+        mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
 
         mScrimController.setHasBackdrop(false);
@@ -260,8 +256,8 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, true,
-                mScrimBehind, true,
-                mScrimForBubble, false));
+                mScrimBehind, true
+        ));
     }
 
     @Test
@@ -277,8 +273,7 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, false,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
     }
 
@@ -296,8 +291,7 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, false,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
     }
 
@@ -312,8 +306,7 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, true,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
 
         assertEquals(1f, mScrimController.getState().getMaxLightRevealScrimAlpha(), 0f);
@@ -332,8 +325,7 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, true,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
     }
 
@@ -372,8 +364,7 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, true,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
     }
 
@@ -392,8 +383,7 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, true,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
     }
 
@@ -513,8 +503,7 @@
 
         assertScrimTinted(Map.of(
                 mScrimInFront, true,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
 
         // ... and when ambient goes dark, front scrim should be semi-transparent
@@ -552,8 +541,7 @@
         assertScrimTinted(Map.of(
                 mScrimInFront, false,
                 mScrimBehind, false,
-                mNotificationsScrim, false,
-                mScrimForBubble, false
+                mNotificationsScrim, false
         ));
     }
 
@@ -573,8 +561,50 @@
         assertScrimTinted(Map.of(
                 mScrimInFront, false,
                 mScrimBehind, true,
-                mNotificationsScrim, false,
-                mScrimForBubble, false
+                mNotificationsScrim, false
+        ));
+    }
+
+    @Test
+    public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
+        mScrimController.setClipsQsScrim(true);
+        mScrimController.transitionTo(ScrimState.BOUNCER);
+
+        mScrimController.setClipsQsScrim(false);
+
+        finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be visible without tint
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, OPAQUE));
+        assertScrimTinted(Map.of(
+                mScrimInFront, false,
+                mScrimBehind, false,
+                mNotificationsScrim, false
+        ));
+    }
+
+    @Test
+    public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() {
+        mScrimController.setClipsQsScrim(false);
+        mScrimController.transitionTo(ScrimState.BOUNCER);
+
+        mScrimController.setClipsQsScrim(true);
+
+        finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be clipping QS
+        // Notif scrim should be visible without tint
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, OPAQUE,
+                mScrimBehind, OPAQUE));
+        assertScrimTinted(Map.of(
+                mScrimInFront, false,
+                mScrimBehind, true,
+                mNotificationsScrim, false
         ));
     }
 
@@ -587,14 +617,13 @@
                 mScrimBehind, TRANSPARENT));
         assertScrimTinted(Map.of(
                 mScrimInFront, false,
-                mScrimBehind, false,
-                mScrimForBubble, false
+                mScrimBehind, false
         ));
     }
 
     @Test
     public void transitionToUnlocked() {
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
         assertScrimAlpha(Map.of(
@@ -605,39 +634,17 @@
         assertScrimTinted(Map.of(
                 mNotificationsScrim, false,
                 mScrimInFront, false,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
 
         // Back scrim should be visible after start dragging
-        mScrimController.setPanelExpansion(0.3f);
+        mScrimController.setRawPanelExpansionFraction(0.3f);
         assertScrimAlpha(Map.of(
                 mScrimInFront, TRANSPARENT,
                 mNotificationsScrim, SEMI_TRANSPARENT,
                 mScrimBehind, SEMI_TRANSPARENT));
     }
 
-    @Test
-    public void transitionToBubbleExpanded() {
-        mScrimController.transitionTo(ScrimState.BUBBLE_EXPANDED);
-        finishAnimationsImmediately();
-
-        assertScrimTinted(Map.of(
-                mScrimInFront, false,
-                mScrimBehind, true,
-                mScrimForBubble, true
-        ));
-
-        // Front scrim should be transparent
-        assertEquals(ScrimController.TRANSPARENT,
-                mScrimInFront.getViewAlpha(), 0.0f);
-        // Back scrim should be visible
-        assertEquals(ScrimController.BUSY_SCRIM_ALPHA,
-                mScrimBehind.getViewAlpha(), 0.0f);
-        // Bubble scrim should be visible
-        assertEquals(ScrimController.BUBBLE_SCRIM_ALPHA,
-                mScrimForBubble.getViewAlpha(), 0.0f);
-    }
 
     @Test
     public void scrimStateCallback() {
@@ -656,20 +663,20 @@
 
     @Test
     public void panelExpansion() {
-        mScrimController.setPanelExpansion(0f);
-        mScrimController.setPanelExpansion(0.5f);
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setRawPanelExpansionFraction(0.5f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
 
         reset(mScrimBehind);
-        mScrimController.setPanelExpansion(0f);
-        mScrimController.setPanelExpansion(1.0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setRawPanelExpansionFraction(1.0f);
         finishAnimationsImmediately();
 
         assertEquals("Scrim alpha should change after setPanelExpansion",
                 mScrimBehindAlpha, mScrimBehind.getViewAlpha(), 0.01f);
 
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         finishAnimationsImmediately();
 
         assertEquals("Scrim alpha should change after setPanelExpansion",
@@ -716,21 +723,21 @@
 
     @Test
     public void panelExpansionAffectsAlpha() {
-        mScrimController.setPanelExpansion(0f);
-        mScrimController.setPanelExpansion(0.5f);
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setRawPanelExpansionFraction(0.5f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
 
         final float scrimAlpha = mScrimBehind.getViewAlpha();
         reset(mScrimBehind);
         mScrimController.setExpansionAffectsAlpha(false);
-        mScrimController.setPanelExpansion(0.8f);
+        mScrimController.setRawPanelExpansionFraction(0.8f);
         verifyZeroInteractions(mScrimBehind);
         assertEquals("Scrim opacity shouldn't change when setExpansionAffectsAlpha "
                 + "is false", scrimAlpha, mScrimBehind.getViewAlpha(), 0.01f);
 
         mScrimController.setExpansionAffectsAlpha(true);
-        mScrimController.setPanelExpansion(0.1f);
+        mScrimController.setRawPanelExpansionFraction(0.1f);
         finishAnimationsImmediately();
         Assert.assertNotEquals("Scrim opacity should change when setExpansionAffectsAlpha "
                 + "is true", scrimAlpha, mScrimBehind.getViewAlpha(), 0.01f);
@@ -740,7 +747,7 @@
     public void transitionToUnlockedFromOff() {
         // Simulate unlock with fingerprint without AOD
         mScrimController.transitionTo(ScrimState.OFF);
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         finishAnimationsImmediately();
         mScrimController.transitionTo(ScrimState.UNLOCKED);
 
@@ -749,14 +756,12 @@
         // All scrims should be transparent at the end of fade transition.
         assertScrimAlpha(Map.of(
                 mScrimInFront, TRANSPARENT,
-                mScrimBehind, TRANSPARENT,
-                mScrimForBubble, TRANSPARENT));
+                mScrimBehind, TRANSPARENT));
 
         // Make sure at the very end of the animation, we're reset to transparent
         assertScrimTinted(Map.of(
                 mScrimInFront, false,
-                mScrimBehind, true,
-                mScrimForBubble, false
+                mScrimBehind, true
         ));
     }
 
@@ -764,7 +769,7 @@
     public void transitionToUnlockedFromAod() {
         // Simulate unlock with fingerprint
         mScrimController.transitionTo(ScrimState.AOD);
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         finishAnimationsImmediately();
         mScrimController.transitionTo(ScrimState.UNLOCKED);
 
@@ -796,8 +801,7 @@
                                 + mScrimInFront.getViewAlpha(), mScrimInFront.getViewAlpha() > 0);
                         assertScrimTinted(Map.of(
                                 mScrimInFront, true,
-                                mScrimBehind, true,
-                                mScrimForBubble, true
+                                mScrimBehind, true
                         ));
                         Assert.assertSame("Scrim should be visible during transition.",
                                 mScrimVisibility, OPAQUE);
@@ -944,7 +948,7 @@
     @Test
     public void testConservesExpansionOpacityAfterTransition() {
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(0.5f);
+        mScrimController.setRawPanelExpansionFraction(0.5f);
         finishAnimationsImmediately();
 
         final float expandedAlpha = mScrimBehind.getViewAlpha();
@@ -1050,8 +1054,6 @@
                 mScrimInFront.getDefaultFocusHighlightEnabled());
         Assert.assertFalse("Scrim shouldn't have focus highlight",
                 mScrimBehind.getDefaultFocusHighlightEnabled());
-        Assert.assertFalse("Scrim shouldn't have focus highlight",
-                mScrimForBubble.getDefaultFocusHighlightEnabled());
     }
 
     @Test
@@ -1061,7 +1063,7 @@
         HashSet<ScrimState> regularStates = new HashSet<>(Arrays.asList(
                 ScrimState.UNINITIALIZED, ScrimState.KEYGUARD, ScrimState.BOUNCER,
                 ScrimState.BOUNCER_SCRIMMED, ScrimState.BRIGHTNESS_MIRROR, ScrimState.UNLOCKED,
-                ScrimState.BUBBLE_EXPANDED, ScrimState.SHADE_LOCKED, ScrimState.AUTH_SCRIMMED));
+                ScrimState.SHADE_LOCKED, ScrimState.AUTH_SCRIMMED, ScrimState.AUTH_SCRIMMED_SHADE));
 
         for (ScrimState state : ScrimState.values()) {
             if (!lowPowerModeStates.contains(state) && !regularStates.contains(state)) {
@@ -1073,7 +1075,7 @@
     @Test
     public void testScrimsOpaque_whenShadeFullyExpanded() {
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(1);
+        mScrimController.setRawPanelExpansionFraction(1);
         // notifications scrim alpha change require calling setQsPosition
         mScrimController.setQsPosition(0, 300);
         finishAnimationsImmediately();
@@ -1085,9 +1087,47 @@
     }
 
     @Test
+    public void testAuthScrim_notifScrimOpaque_whenShadeFullyExpanded() {
+        // GIVEN device has an activity showing ('UNLOCKED' state can occur on the lock screen
+        // with the camera app occluding the keyguard)
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        mScrimController.setRawPanelExpansionFraction(1);
+        // notifications scrim alpha change require calling setQsPosition
+        mScrimController.setQsPosition(0, 300);
+        finishAnimationsImmediately();
+
+        // WHEN the user triggers the auth bouncer
+        mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE);
+        finishAnimationsImmediately();
+
+        assertEquals("Behind scrim should be opaque",
+                mScrimBehind.getViewAlpha(), 1, 0.0);
+        assertEquals("Notifications scrim should be opaque",
+                mNotificationsScrim.getViewAlpha(), 1, 0.0);
+    }
+
+    @Test
+    public void testAuthScrimKeyguard() {
+        // GIVEN device is on the keyguard
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        finishAnimationsImmediately();
+
+        // WHEN the user triggers the auth bouncer
+        mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED);
+        finishAnimationsImmediately();
+
+        // THEN the front scrim is updated and the KEYGUARD scrims are the same as the
+        // KEYGUARD scrim state
+        assertScrimAlpha(Map.of(
+                mScrimInFront, SEMI_TRANSPARENT,
+                mScrimBehind, SEMI_TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT));
+    }
+
+    @Test
     public void testScrimsVisible_whenShadeVisible() {
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(0.3f);
+        mScrimController.setRawPanelExpansionFraction(0.3f);
         // notifications scrim alpha change require calling setQsPosition
         mScrimController.setQsPosition(0, 300);
         finishAnimationsImmediately();
@@ -1122,7 +1162,7 @@
     public void testScrimsVisible_whenShadeVisible_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(0.3f);
+        mScrimController.setRawPanelExpansionFraction(0.3f);
         // notifications scrim alpha change require calling setQsPosition
         mScrimController.setQsPosition(0.5f, 300);
         finishAnimationsImmediately();
@@ -1148,7 +1188,7 @@
     public void testNotificationScrimTransparent_whenOnLockscreen() {
         mScrimController.transitionTo(ScrimState.KEYGUARD);
         // even if shade is not pulled down, panel has expansion of 1 on the lockscreen
-        mScrimController.setPanelExpansion(1);
+        mScrimController.setRawPanelExpansionFraction(1);
         mScrimController.setQsPosition(0f, /*qs panel bottom*/ 0);
 
         assertScrimAlpha(Map.of(
@@ -1158,7 +1198,7 @@
 
     @Test
     public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() {
-        mScrimController.setPanelExpansion(1);
+        mScrimController.setRawPanelExpansionFraction(1);
         mScrimController.transitionTo(ScrimState.SHADE_LOCKED);
         finishAnimationsImmediately();
 
@@ -1201,11 +1241,11 @@
     @Test
     public void testNotificationTransparency_followsTransitionToFullShade() {
         mScrimController.transitionTo(ScrimState.SHADE_LOCKED);
-        mScrimController.setPanelExpansion(1.0f);
+        mScrimController.setRawPanelExpansionFraction(1.0f);
         finishAnimationsImmediately();
         float shadeLockedAlpha = mNotificationsScrim.getViewAlpha();
         mScrimController.transitionTo(ScrimState.KEYGUARD);
-        mScrimController.setPanelExpansion(1.0f);
+        mScrimController.setRawPanelExpansionFraction(1.0f);
         finishAnimationsImmediately();
         float keyguardAlpha = mNotificationsScrim.getViewAlpha();
 
@@ -1225,7 +1265,7 @@
     }
 
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
-        mScrimController.setPanelExpansion(expansion);
+        mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
         // alpha is not changing linearly thus 0.2 of leeway when asserting
         assertEquals(expectedAlpha, mNotificationsScrim.getViewAlpha(), 0.2);
@@ -1249,21 +1289,16 @@
             return "behind";
         } else if (scrim == mNotificationsScrim) {
             return "notifications";
-        } else if (scrim == mScrimForBubble) {
-            return "bubble";
         }
         return "unknown_scrim";
     }
 
     /**
-     * If {@link #mScrimForBubble} or {@link #mNotificationsScrim} is not passed in the map
+     * If {@link #mNotificationsScrim} is not passed in the map
      * we assume it must be transparent
      */
     private void assertScrimAlpha(Map<ScrimView, Integer> scrimToAlpha) {
         // Check single scrim visibility.
-        if (!scrimToAlpha.containsKey(mScrimForBubble)) {
-            assertScrimAlpha(mScrimForBubble, TRANSPARENT);
-        }
         if (!scrimToAlpha.containsKey(mNotificationsScrim)) {
             assertScrimAlpha(mNotificationsScrim, TRANSPARENT);
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarCommandQueueCallbacksTest.java
new file mode 100644
index 0000000..8555306
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarCommandQueueCallbacksTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.StatusBarManager;
+import android.os.PowerManager;
+import android.os.Vibrator;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.testing.FakeMetricsLogger;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.DisableFlagsLogger;
+import com.android.systemui.statusbar.StatusBarStateControllerImpl;
+import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.util.Optional;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class StatusBarCommandQueueCallbacksTest extends SysuiTestCase {
+    @Mock private StatusBar mStatusBar;
+    @Mock private ShadeController mShadeController;
+    @Mock private CommandQueue mCommandQueue;
+    @Mock private NotificationPanelViewController mNotificationPanelViewController;
+    @Mock private LegacySplitScreen mLegacySplitScreen;
+    @Mock private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
+    private final MetricsLogger mMetricsLogger = new FakeMetricsLogger();
+    @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock private KeyguardStateController mKeyguardStateController;
+    @Mock private HeadsUpManagerPhone mHeadsUpManager;
+    @Mock private WakefulnessLifecycle mWakefulnessLifecycle;
+    @Mock private DeviceProvisionedController mDeviceProvisionedController;
+    @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    @Mock private AssistManager mAssistManager;
+    @Mock private DozeServiceHost mDozeServiceHost;
+    @Mock private StatusBarStateControllerImpl mStatusBarStateController;
+    @Mock private NotificationShadeWindowView mNotificationShadeWindowView;
+    @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
+    @Mock private PowerManager mPowerManager;
+    @Mock private VibratorHelper mVibratorHelper;
+    @Mock private Vibrator mVibrator;
+    @Mock private LightBarController mLightBarController;
+
+    StatusBarCommandQueueCallbacks mSbcqCallbacks;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mSbcqCallbacks = new StatusBarCommandQueueCallbacks(
+                mStatusBar,
+                mContext,
+                mContext.getResources(),
+                mShadeController,
+                mCommandQueue,
+                mNotificationPanelViewController,
+                Optional.of(mLegacySplitScreen),
+                mRemoteInputQuickSettingsDisabler,
+                mMetricsLogger,
+                mKeyguardUpdateMonitor,
+                mKeyguardStateController,
+                mHeadsUpManager,
+                mWakefulnessLifecycle,
+                mDeviceProvisionedController,
+                mStatusBarKeyguardViewManager,
+                mAssistManager,
+                mDozeServiceHost,
+                mStatusBarStateController,
+                mNotificationShadeWindowView,
+                mNotificationStackScrollLayoutController,
+                mPowerManager,
+                mVibratorHelper,
+                Optional.of(mVibrator),
+                mLightBarController,
+                new DisableFlagsLogger(),
+                DEFAULT_DISPLAY);
+
+        when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
+        when(mRemoteInputQuickSettingsDisabler.adjustDisableFlags(anyInt()))
+                .thenAnswer((Answer<Integer>) invocation -> invocation.getArgument(0));
+    }
+
+    @Test
+    public void testDisableNotificationShade() {
+        when(mStatusBar.getDisabled1()).thenReturn(StatusBarManager.DISABLE_NONE);
+        when(mStatusBar.getDisabled2()).thenReturn(StatusBarManager.DISABLE_NONE);
+        when(mCommandQueue.panelsEnabled()).thenReturn(false);
+        mSbcqCallbacks.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
+                StatusBarManager.DISABLE2_NOTIFICATION_SHADE, false);
+
+        verify(mStatusBar).updateQsExpansionEnabled();
+        verify(mShadeController).animateCollapsePanels();
+
+        // Trying to open it does nothing.
+        mSbcqCallbacks.animateExpandNotificationsPanel();
+        verify(mNotificationPanelViewController, never()).expandWithoutQs();
+        mSbcqCallbacks.animateExpandSettingsPanel(null);
+        verify(mNotificationPanelViewController, never()).expand(anyBoolean());
+    }
+
+    @Test
+    public void testEnableNotificationShade() {
+        when(mStatusBar.getDisabled1()).thenReturn(StatusBarManager.DISABLE_NONE);
+        when(mStatusBar.getDisabled2()).thenReturn(StatusBarManager.DISABLE2_NOTIFICATION_SHADE);
+        when(mCommandQueue.panelsEnabled()).thenReturn(true);
+        mSbcqCallbacks.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
+                StatusBarManager.DISABLE2_NONE, false);
+        verify(mStatusBar).updateQsExpansionEnabled();
+        verify(mShadeController, never()).animateCollapsePanels();
+
+        // Can now be opened.
+        mSbcqCallbacks.animateExpandNotificationsPanel();
+        verify(mNotificationPanelViewController).expandWithoutQs();
+        mSbcqCallbacks.animateExpandSettingsPanel(null);
+        verify(mNotificationPanelViewController).expandWithQs();
+    }
+
+    @Test
+    public void testSuppressAmbientDisplay_suppress() {
+        mSbcqCallbacks.suppressAmbientDisplay(true);
+        verify(mDozeServiceHost).setDozeSuppressed(true);
+    }
+
+    @Test
+    public void testSuppressAmbientDisplay_unsuppress() {
+        mSbcqCallbacks.suppressAmbientDisplay(false);
+        verify(mDozeServiceHost).setDozeSuppressed(false);
+    }
+
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
index 10eb71f..1503af8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
@@ -16,32 +16,54 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.content.Context
+import android.content.res.Configuration
 import android.graphics.Rect
 import android.test.suitebuilder.annotation.SmallTest
+import android.view.Display
 import android.view.DisplayCutout
-import android.view.WindowMetrics
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.leak.RotationUtils
 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
 import com.android.systemui.util.leak.RotationUtils.Rotation
+import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
+import org.mockito.ArgumentMatchers.any
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 class StatusBarContentInsetsProviderTest : SysuiTestCase() {
+
     @Mock private lateinit var dc: DisplayCutout
-    @Mock private lateinit var windowMetrics: WindowMetrics
+    @Mock private lateinit var contextMock: Context
+    @Mock private lateinit var display: Display
+    private lateinit var configurationController: ConfigurationController
+
+    private val configuration = Configuration()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        `when`(contextMock.display).thenReturn(display)
+
+        context.ensureTestableResources()
+        `when`(contextMock.resources).thenReturn(context.resources)
+        `when`(contextMock.resources.configuration).thenReturn(configuration)
+        `when`(contextMock.createConfigurationContext(any())).thenAnswer {
+            context.createConfigurationContext(it.arguments[0] as Configuration)
+        }
+
+        configurationController = ConfigurationControllerImpl(contextMock)
     }
 
     @Test
@@ -55,15 +77,13 @@
         val chipWidth = 30
         val dotWidth = 10
 
-        `when`(windowMetrics.bounds).thenReturn(screenBounds)
-
         var isRtl = false
         var targetRotation = ROTATION_NONE
         var bounds = calculateInsetsForRotationWithRotatedResources(
                 currentRotation,
                 targetRotation,
                 null,
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -92,7 +112,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightLandscape,
                 minLeftPadding,
                 minRightPadding)
@@ -127,7 +147,6 @@
         val sbHeightLandscape = 60
         val currentRotation = ROTATION_NONE
 
-        `when`(windowMetrics.bounds).thenReturn(screenBounds)
         `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
 
         // THEN rotations which share a short side should use the greater value between rounded
@@ -142,7 +161,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -159,7 +178,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightLandscape,
                 minLeftPadding,
                 minRightPadding)
@@ -178,7 +197,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -196,7 +215,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightLandscape,
                 minLeftPadding,
                 minRightPadding)
@@ -219,7 +238,6 @@
         val sbHeightLandscape = 60
         val currentRotation = ROTATION_NONE
 
-        `when`(windowMetrics.bounds).thenReturn(screenBounds)
         `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
 
         // THEN only the landscape/seascape rotations should avoid the cutout area because of the
@@ -234,7 +252,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -251,7 +269,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightLandscape,
                 minLeftPadding,
                 minRightPadding)
@@ -268,7 +286,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -285,7 +303,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightLandscape,
                 minLeftPadding,
                 minRightPadding)
@@ -303,8 +321,6 @@
         val sbHeightPortrait = 100
         val sbHeightLandscape = 60
 
-        `when`(windowMetrics.bounds).thenReturn(screenBounds)
-
         // THEN content insets should only use rounded corner padding
         var targetRotation = ROTATION_NONE
         var expectedBounds = Rect(minLeftPadding,
@@ -316,7 +332,7 @@
                 currentRotation,
                 targetRotation,
                 null, /* no cutout */
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -332,7 +348,7 @@
                 currentRotation,
                 targetRotation,
                 null, /* no cutout */
-                windowMetrics,
+                screenBounds,
                 sbHeightLandscape,
                 minLeftPadding,
                 minRightPadding)
@@ -348,7 +364,7 @@
                 currentRotation,
                 targetRotation,
                 null, /* no cutout */
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -364,7 +380,7 @@
                 currentRotation,
                 targetRotation,
                 null, /* no cutout */
-                windowMetrics,
+                screenBounds,
                 sbHeightLandscape,
                 minLeftPadding,
                 minRightPadding)
@@ -382,7 +398,6 @@
         val sbHeightLandscape = 60
         val currentRotation = ROTATION_NONE
 
-        `when`(windowMetrics.bounds).thenReturn(screenBounds)
         `when`(dc.boundingRects).thenReturn(listOf(dcBounds))
 
         // THEN left should be set to the display cutout width, and right should use the minRight
@@ -396,7 +411,7 @@
                 currentRotation,
                 targetRotation,
                 dc,
-                windowMetrics,
+                screenBounds,
                 sbHeightPortrait,
                 minLeftPadding,
                 minRightPadding)
@@ -404,6 +419,67 @@
         assertRects(expectedBounds, bounds, currentRotation, targetRotation)
     }
 
+    @Test
+    fun testDisplayChanged_returnsUpdatedInsets() {
+        // GIVEN: get insets on the first display and switch to the second display
+        val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
+            mock(DumpManager::class.java))
+
+        givenDisplay(
+            screenBounds = Rect(0, 0, 1080, 2160),
+            displayUniqueId = "1"
+        )
+        val firstDisplayInsets = provider.getStatusBarContentInsetsForRotation(ROTATION_NONE)
+        givenDisplay(
+            screenBounds = Rect(0, 0, 800, 600),
+            displayUniqueId = "2"
+        )
+        configurationController.onConfigurationChanged(configuration)
+
+        // WHEN: get insets on the second display
+        val secondDisplayInsets = provider.getStatusBarContentInsetsForRotation(ROTATION_NONE)
+
+        // THEN: insets are updated
+        assertThat(firstDisplayInsets).isNotEqualTo(secondDisplayInsets)
+    }
+
+    @Test
+    fun testDisplayChangedAndReturnedBack_returnsTheSameInsets() {
+        // GIVEN: get insets on the first display, switch to the second display,
+        // get insets and switch back
+        val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
+            mock(DumpManager::class.java))
+        givenDisplay(
+            screenBounds = Rect(0, 0, 1080, 2160),
+            displayUniqueId = "1"
+        )
+        val firstDisplayInsetsFirstCall = provider
+            .getStatusBarContentInsetsForRotation(ROTATION_NONE)
+        givenDisplay(
+            screenBounds = Rect(0, 0, 800, 600),
+            displayUniqueId = "2"
+        )
+        configurationController.onConfigurationChanged(configuration)
+        provider.getStatusBarContentInsetsForRotation(ROTATION_NONE)
+        givenDisplay(
+            screenBounds = Rect(0, 0, 1080, 2160),
+            displayUniqueId = "1"
+        )
+        configurationController.onConfigurationChanged(configuration)
+
+        // WHEN: get insets on the first display again
+        val firstDisplayInsetsSecondCall = provider
+            .getStatusBarContentInsetsForRotation(ROTATION_NONE)
+
+        // THEN: insets for the first and second calls for the first display are the same
+        assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall)
+    }
+
+    private fun givenDisplay(screenBounds: Rect, displayUniqueId: String) {
+        `when`(display.uniqueId).thenReturn(displayUniqueId)
+        configuration.windowConfiguration.maxBounds = screenBounds
+    }
+
     private fun assertRects(
         expected: Rect,
         actual: Rect,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index e3263d4..0f1c40b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -30,8 +30,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.DarkIconDispatcher;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarMobileView;
 import com.android.systemui.statusbar.StatusBarWifiView;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 2b569f3..3fcd071 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -16,15 +16,12 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -45,6 +42,7 @@
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dock.DockManager;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.FaceAuthScreenBrightnessController;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -64,6 +62,8 @@
 
 import java.util.Optional;
 
+import dagger.Lazy;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -103,6 +103,8 @@
     private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     @Mock
     private KeyguardMessageArea mKeyguardMessageArea;
+    @Mock
+    private Lazy<ShadeController> mShadeController;
 
     private WakefulnessLifecycle mWakefulnessLifecycle;
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -114,9 +116,12 @@
                 any(ViewGroup.class),
                 any(KeyguardBouncer.BouncerExpansionCallback.class)))
                 .thenReturn(mBouncer);
-
+        when(mStatusBar.getBouncerContainer()).thenReturn(mContainer);
         when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
-        mWakefulnessLifecycle = new WakefulnessLifecycle(getContext(), null);
+        mWakefulnessLifecycle = new WakefulnessLifecycle(
+                getContext(),
+                null,
+                mock(DumpManager.class));
         mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager(
                 getContext(),
                 mViewMediatorCallback,
@@ -133,8 +138,9 @@
                 mKeyguardBouncerFactory,
                 mWakefulnessLifecycle,
                 mUnlockedScreenOffAnimationController,
-                mKeyguardMessageAreaFactory);
-        mStatusBarKeyguardViewManager.registerStatusBar(mStatusBar, mContainer,
+                mKeyguardMessageAreaFactory,
+                mShadeController);
+        mStatusBarKeyguardViewManager.registerStatusBar(mStatusBar,
                 mNotificationPanelView, mBiometrucUnlockController,
                 mNotificationContainer, mBypassController);
         mStatusBarKeyguardViewManager.show(null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 37a6d21..72a3d66 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -53,15 +53,14 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationClickNotifier;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -111,8 +110,6 @@
     @Mock
     private NotificationRemoteInputManager mRemoteInputManager;
     @Mock
-    private RemoteInputController mRemoteInputController;
-    @Mock
     private StatusBar mStatusBar;
     @Mock
     private KeyguardStateController mKeyguardStateController;
@@ -153,8 +150,6 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController);
-
         when(mContentIntent.isActivity()).thenReturn(true);
         when(mContentIntent.getCreatorUserHandle()).thenReturn(UserHandle.of(1));
         when(mContentIntent.getIntent()).thenReturn(mContentIntentInner);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index ce45f26..c80c072 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -31,9 +31,9 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.logging.testing.FakeMetricsLogger;
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.ForegroundServiceNotificationListener;
 import com.android.systemui.InitController;
 import com.android.systemui.SysuiTestCase;
@@ -46,13 +46,11 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -60,6 +58,7 @@
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
@@ -67,14 +66,10 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 
-import java.util.ArrayList;
-
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper()
 public class StatusBarNotificationPresenterTest extends SysuiTestCase {
-
-
     private StatusBarNotificationPresenter mStatusBarNotificationPresenter;
     private NotificationInterruptStateProvider mNotificationInterruptStateProvider =
             mock(NotificationInterruptStateProvider.class);
@@ -87,29 +82,16 @@
 
     @Before
     public void setup() {
-        NotificationRemoteInputManager notificationRemoteInputManager =
-                mock(NotificationRemoteInputManager.class);
-        when(notificationRemoteInputManager.getController())
-                .thenReturn(mock(RemoteInputController.class));
         mMetricsLogger = new FakeMetricsLogger();
-        mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
+        LockscreenGestureLogger lockscreenGestureLogger = new LockscreenGestureLogger(
+                mMetricsLogger);
         mCommandQueue = new CommandQueue(mContext);
         mDependency.injectTestDependency(StatusBarStateController.class,
                 mock(SysuiStatusBarStateController.class));
         mDependency.injectTestDependency(ShadeController.class, mShadeController);
-        mDependency.injectTestDependency(NotificationRemoteInputManager.class,
-                notificationRemoteInputManager);
-        mDependency.injectMockDependency(NotificationViewHierarchyManager.class);
         mDependency.injectMockDependency(NotificationRemoteInputManager.Callback.class);
-        mDependency.injectMockDependency(NotificationLockscreenUserManager.class);
-        mDependency.injectMockDependency(NotificationMediaManager.class);
-        mDependency.injectMockDependency(VisualStabilityManager.class);
-        mDependency.injectMockDependency(NotificationGutsManager.class);
         mDependency.injectMockDependency(NotificationShadeWindowController.class);
         mDependency.injectMockDependency(ForegroundServiceNotificationListener.class);
-        NotificationEntryManager entryManager =
-                mDependency.injectMockDependency(NotificationEntryManager.class);
-        when(entryManager.getActiveNotificationsForCurrentUser()).thenReturn(new ArrayList<>());
 
         NotificationShadeWindowView notificationShadeWindowView =
                 mock(NotificationShadeWindowView.class);
@@ -129,8 +111,19 @@
                 mock(KeyguardStateController.class),
                 mock(KeyguardIndicationController.class), mStatusBar,
                 mock(ShadeControllerImpl.class), mock(LockscreenShadeTransitionController.class),
-                mCommandQueue, mInitController,
-                mNotificationInterruptStateProvider);
+                mCommandQueue,
+                mock(NotificationViewHierarchyManager.class),
+                mock(NotificationLockscreenUserManager.class),
+                mock(SysuiStatusBarStateController.class),
+                mock(NotificationEntryManager.class),
+                mock(NotificationMediaManager.class),
+                mock(NotificationGutsManager.class),
+                mock(KeyguardUpdateMonitor.class),
+                lockscreenGestureLogger,
+                mInitController,
+                mNotificationInterruptStateProvider,
+                mock(NotificationRemoteInputManager.class),
+                mock(ConfigurationController.class));
         mInitController.executePostInitTasks();
         ArgumentCaptor<NotificationInterruptSuppressor> suppressorCaptor =
                 ArgumentCaptor.forClass(NotificationInterruptSuppressor.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 6051c56..741d95f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -18,7 +18,6 @@
 
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
-import static android.view.Display.DEFAULT_DISPLAY;
 
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -40,7 +39,7 @@
 
 import android.app.IWallpaperManager;
 import android.app.Notification;
-import android.app.StatusBarManager;
+import android.app.WallpaperManager;
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -78,28 +77,28 @@
 import com.android.systemui.InitController;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
+import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.demomode.DemoModeController;
-import com.android.systemui.keyguard.DismissCallbackRegistry;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.PluginDependencyProvider;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.settings.brightness.BrightnessSlider;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationListener;
@@ -109,16 +108,13 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
+import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.charging.WiredChargingRippleController;
+import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationFilter;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
@@ -142,11 +138,15 @@
 import com.android.systemui.statusbar.policy.ExtensionController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
+import com.android.systemui.unfold.UnfoldTransitionWallpaperController;
+import com.android.systemui.unfold.config.UnfoldTransitionConfig;
+import com.android.systemui.util.WallpaperController;
 import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.concurrency.MessageRouterImpl;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.wmshell.BubblesManager;
@@ -164,8 +164,6 @@
 import java.io.PrintWriter;
 import java.util.Optional;
 
-import javax.inject.Provider;
-
 import dagger.Lazy;
 
 @SmallTest
@@ -180,7 +178,6 @@
 
     @Mock private NotificationsController mNotificationsController;
     @Mock private LightBarController mLightBarController;
-    @Mock private StatusBarSignalPolicy mStatusBarSignalPolicy;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private KeyguardIndicationController mKeyguardIndicationController;
@@ -201,12 +198,10 @@
     @Mock private KeyguardViewMediator mKeyguardViewMediator;
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
-    @Mock private RemoteInputController mRemoteInputController;
     @Mock private StatusBarStateControllerImpl mStatusBarStateController;
     @Mock private BatteryController mBatteryController;
     @Mock private DeviceProvisionedController mDeviceProvisionedController;
     @Mock private StatusBarNotificationPresenter mNotificationPresenter;
-    @Mock private NotificationEntryListener mEntryListener;
     @Mock private NotificationFilter mNotificationFilter;
     @Mock private AmbientDisplayConfiguration mAmbientDisplayConfiguration;
     @Mock private NotificationLogger.ExpansionStateLogger mExpansionStateLogger;
@@ -214,10 +209,10 @@
     @Mock private NotificationShadeWindowView mNotificationShadeWindowView;
     @Mock private BroadcastDispatcher mBroadcastDispatcher;
     @Mock private AssistManager mAssistManager;
+    @Mock private NotificationEntryManager mNotificationEntryManager;
     @Mock private NotificationGutsManager mNotificationGutsManager;
     @Mock private NotificationMediaManager mNotificationMediaManager;
     @Mock private NavigationBarController mNavigationBarController;
-    @Mock private AccessibilityFloatingMenuController mAccessibilityFloatingMenuController;
     @Mock private BypassHeadsUpNotifier mBypassHeadsUpNotifier;
     @Mock private SysuiColorExtractor mColorExtractor;
     @Mock private ColorExtractor.GradientColors mGradientColors;
@@ -229,7 +224,6 @@
     @Mock private NotificationViewHierarchyManager mNotificationViewHierarchyManager;
     @Mock private UserSwitcherController mUserSwitcherController;
     @Mock private NetworkController mNetworkController;
-    @Mock private VibratorHelper mVibratorHelper;
     @Mock private BubblesManager mBubblesManager;
     @Mock private Bubbles mBubbles;
     @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
@@ -237,26 +231,24 @@
     @Mock private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
     @Mock private DozeParameters mDozeParameters;
     @Mock private Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
+    @Mock private LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock private LockscreenWallpaper mLockscreenWallpaper;
     @Mock private DozeServiceHost mDozeServiceHost;
     @Mock private ViewMediatorCallback mKeyguardVieMediatorCallback;
-    @Mock private KeyguardLiftController mKeyguardLiftController;
     @Mock private VolumeComponent mVolumeComponent;
     @Mock private CommandQueue mCommandQueue;
-    @Mock private Provider<StatusBarComponent.Builder> mStatusBarComponentBuilderProvider;
-    @Mock private StatusBarComponent.Builder mStatusBarComponentBuilder;
+    @Mock private CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger;
+    @Mock private StatusBarComponent.Factory mStatusBarComponentFactory;
     @Mock private StatusBarComponent mStatusBarComponent;
     @Mock private PluginManager mPluginManager;
     @Mock private LegacySplitScreen mLegacySplitScreen;
-    @Mock private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
+    @Mock private StatusBarWindowView mStatusBarWindowView;
     @Mock private LightsOutNotifController mLightsOutNotifController;
     @Mock private ViewMediatorCallback mViewMediatorCallback;
-    @Mock private DismissCallbackRegistry mDismissCallbackRegistry;
     @Mock private StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
     @Mock private ScreenPinningRequest mScreenPinningRequest;
     @Mock private StatusBarNotificationActivityStarter.Builder
             mStatusBarNotificationActivityStarterBuilder;
-    @Mock private DarkIconDispatcher mDarkIconDispatcher;
     @Mock private PluginDependencyProvider mPluginDependencyProvider;
     @Mock private KeyguardDismissUtil mKeyguardDismissUtil;
     @Mock private ExtensionController mExtensionController;
@@ -265,19 +257,31 @@
     @Mock private DemoModeController mDemoModeController;
     @Mock private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
     @Mock private BrightnessSlider.Factory mBrightnessSliderFactory;
-    @Mock private WiredChargingRippleController mWiredChargingRippleController;
+    @Mock private UnfoldTransitionConfig mUnfoldTransitionConfig;
+    @Mock private Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimationLazy;
+    @Mock private Lazy<StatusBarMoveFromCenterAnimationController> mMoveFromCenterAnimationLazy;
+    @Mock private Lazy<UnfoldTransitionWallpaperController> mUnfoldWallpaperController;
+    @Mock private WallpaperController mWallpaperController;
     @Mock private OngoingCallController mOngoingCallController;
     @Mock private SystemStatusAnimationScheduler mAnimationScheduler;
     @Mock private StatusBarLocationPublisher mLocationPublisher;
     @Mock private StatusBarIconController mIconController;
     @Mock private LockscreenShadeTransitionController mLockscreenTransitionController;
     @Mock private FeatureFlags mFeatureFlags;
-    @Mock private IWallpaperManager mWallpaperManager;
+    @Mock private WallpaperManager mWallpaperManager;
+    @Mock private IWallpaperManager mIWallpaperManager;
     @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     @Mock private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    @Mock private TunerService mTunerService;
     @Mock private StartingSurface mStartingSurface;
+    @Mock private OperatorNameViewController mOperatorNameViewController;
+    @Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
+    @Mock private ActivityLaunchAnimator mActivityLaunchAnimator;
+    @Mock private DialogLaunchAnimator mDialogLaunchAnimator;
     private ShadeController mShadeController;
-    private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
+    private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+    private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
+    private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
     private InitController mInitController = new InitController();
 
     @Before
@@ -332,10 +336,8 @@
             return null;
         }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any());
 
-        when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController);
-
         WakefulnessLifecycle wakefulnessLifecycle =
-                new WakefulnessLifecycle(mContext, mWallpaperManager);
+                new WakefulnessLifecycle(mContext, mIWallpaperManager, mock(DumpManager.class));
         wakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
         wakefulnessLifecycle.dispatchFinishedWakingUp();
 
@@ -346,15 +348,17 @@
         when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper);
         when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
 
-        when(mStatusBarComponentBuilderProvider.get()).thenReturn(mStatusBarComponentBuilder);
-        when(mStatusBarComponentBuilder.build()).thenReturn(mStatusBarComponent);
+        when(mStatusBarComponentFactory.create()).thenReturn(mStatusBarComponent);
         when(mStatusBarComponent.getNotificationShadeWindowViewController()).thenReturn(
                 mNotificationShadeWindowViewController);
 
         mShadeController = new ShadeControllerImpl(mCommandQueue,
                 mStatusBarStateController, mNotificationShadeWindowController,
                 mStatusBarKeyguardViewManager, mContext.getSystemService(WindowManager.class),
-                () -> mStatusBar, () -> mAssistManager, Optional.of(mBubbles));
+                () -> Optional.of(mStatusBar), () -> mAssistManager, Optional.of(mBubbles));
+
+        when(mOperatorNameViewControllerFactory.create(any()))
+                .thenReturn(mOperatorNameViewController);
 
         mStatusBar = new StatusBar(
                 mContext,
@@ -362,7 +366,6 @@
                 mLightBarController,
                 mAutoHideController,
                 mKeyguardUpdateMonitor,
-                mStatusBarSignalPolicy,
                 mPulseExpansionHandler,
                 mNotificationWakeUpCoordinator,
                 mKeyguardBypassController,
@@ -373,11 +376,7 @@
                 new FalsingManagerFake(),
                 new FalsingCollectorFake(),
                 mBroadcastDispatcher,
-                new RemoteInputQuickSettingsDisabler(
-                        mContext,
-                        configurationController,
-                        mCommandQueue
-                ),
+                mNotificationEntryManager,
                 mNotificationGutsManager,
                 notificationLogger,
                 mNotificationInterruptStateProvider,
@@ -393,36 +392,35 @@
                 mNetworkController,
                 mBatteryController,
                 mColorExtractor,
-                new ScreenLifecycle(),
+                new ScreenLifecycle(mock(DumpManager.class)),
                 wakefulnessLifecycle,
                 mStatusBarStateController,
-                mVibratorHelper,
                 Optional.of(mBubblesManager),
                 Optional.of(mBubbles),
                 mVisualStabilityManager,
                 mDeviceProvisionedController,
                 mNavigationBarController,
-                mAccessibilityFloatingMenuController,
                 () -> mAssistManager,
                 configurationController,
                 mNotificationShadeWindowController,
                 mDozeParameters,
                 mScrimController,
-                mKeyguardLiftController,
                 mLockscreenWallpaperLazy,
+                mLockscreenGestureLogger,
                 mBiometricUnlockControllerLazy,
                 mDozeServiceHost,
                 mPowerManager, mScreenPinningRequest,
                 mDozeScrimController,
                 mVolumeComponent,
                 mCommandQueue,
-                mStatusBarComponentBuilderProvider,
+                mCollapsedStatusBarFragmentLogger,
+                mStatusBarComponentFactory,
                 mPluginManager,
                 Optional.of(mLegacySplitScreen),
                 mLightsOutNotifController,
                 mStatusBarNotificationActivityStarterBuilder,
                 mShadeController,
-                mSuperStatusBarViewFactory,
+                mStatusBarWindowView,
                 mStatusBarKeyguardViewManager,
                 mViewMediatorCallback,
                 mInitController,
@@ -431,15 +429,19 @@
                 mKeyguardDismissUtil,
                 mExtensionController,
                 mUserInfoControllerImpl,
+                mOperatorNameViewControllerFactory,
                 mPhoneStatusBarPolicy,
                 mKeyguardIndicationController,
-                mDismissCallbackRegistry,
                 mDemoModeController,
                 mNotificationShadeDepthControllerLazy,
                 mStatusBarTouchableRegionManager,
                 mNotificationIconAreaController,
                 mBrightnessSliderFactory,
-                mWiredChargingRippleController,
+                mUnfoldTransitionConfig,
+                mUnfoldLightRevealOverlayAnimationLazy,
+                mUnfoldWallpaperController,
+                mMoveFromCenterAnimationLazy,
+                mWallpaperController,
                 mOngoingCallController,
                 mAnimationScheduler,
                 mLocationPublisher,
@@ -447,9 +449,17 @@
                 mLockscreenTransitionController,
                 mFeatureFlags,
                 mKeyguardUnlockAnimationController,
+                new Handler(TestableLooper.get(this).getLooper()),
+                mMainExecutor,
+                new MessageRouterImpl(mMainExecutor),
+                mWallpaperManager,
                 mUnlockedScreenOffAnimationController,
-                Optional.of(mStartingSurface));
-        when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
+                Optional.of(mStartingSurface),
+                mTunerService,
+                mock(DumpManager.class),
+                mActivityLaunchAnimator,
+                mDialogLaunchAnimator);
+        when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class),
                 any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
                 any(ViewGroup.class), any(KeyguardBypassController.class)))
                 .thenReturn(mStatusBarKeyguardViewManager);
@@ -711,7 +721,7 @@
         } catch (RemoteException e) {
             fail();
         }
-        TestableLooper.get(this).processAllMessages();
+        mMainExecutor.runAllReady();
     }
 
     @Test
@@ -730,7 +740,7 @@
         } catch (RemoteException e) {
             fail();
         }
-        TestableLooper.get(this).processAllMessages();
+        mMainExecutor.runAllReady();
     }
 
     @Test
@@ -748,32 +758,7 @@
         } catch (RemoteException e) {
             fail();
         }
-        TestableLooper.get(this).processAllMessages();
-    }
-
-    @Test
-    public void testDisableExpandStatusBar() {
-        mStatusBar.setBarStateForTest(StatusBarState.SHADE);
-        mStatusBar.setUserSetupForTest(true);
-        when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
-
-        when(mCommandQueue.panelsEnabled()).thenReturn(false);
-        mStatusBar.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
-                StatusBarManager.DISABLE2_NOTIFICATION_SHADE, false);
-        verify(mNotificationPanelViewController).setQsExpansionEnabledPolicy(false);
-        mStatusBar.animateExpandNotificationsPanel();
-        verify(mNotificationPanelViewController, never()).expand(anyBoolean());
-        mStatusBar.animateExpandSettingsPanel(null);
-        verify(mNotificationPanelViewController, never()).expand(anyBoolean());
-
-        when(mCommandQueue.panelsEnabled()).thenReturn(true);
-        mStatusBar.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
-                StatusBarManager.DISABLE2_NONE, false);
-        verify(mNotificationPanelViewController).setQsExpansionEnabledPolicy(true);
-        mStatusBar.animateExpandNotificationsPanel();
-        verify(mNotificationPanelViewController).expandWithoutQs();
-        mStatusBar.animateExpandSettingsPanel(null);
-        verify(mNotificationPanelViewController).expandWithQs();
+        mMainExecutor.runAllReady();
     }
 
     @Test
@@ -788,15 +773,6 @@
     }
 
     @Test
-    @RunWithLooper(setAsMainLooper = true)
-    public void testUpdateKeyguardState_DoesNotCrash() {
-        mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
-        when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(
-                new SparseArray<>());
-        mStatusBar.onStateChanged(StatusBarState.SHADE);
-    }
-
-    @Test
     public void testFingerprintNotification_UpdatesScrims() {
         mStatusBar.notifyBiometricAuthModeChanged();
         verify(mScrimController).transitionTo(any(), any());
@@ -955,18 +931,6 @@
     }
 
     @Test
-    public void testSuppressAmbientDisplay_suppress() {
-        mStatusBar.suppressAmbientDisplay(true);
-        verify(mDozeServiceHost).setDozeSuppressed(true);
-    }
-
-    @Test
-    public void testSuppressAmbientDisplay_unsuppress() {
-        mStatusBar.suppressAmbientDisplay(false);
-        verify(mDozeServiceHost).setDozeSuppressed(false);
-    }
-
-    @Test
     public void testUpdateResources_updatesBouncer() {
         mStatusBar.updateResources();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index b7c4d0a..3d2ff47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -33,12 +33,16 @@
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.FeatureFlags
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.phone.StatusBarWindowController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.time.FakeSystemClock
@@ -48,8 +52,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.nullable
+import org.mockito.ArgumentMatchers.*
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.eq
@@ -59,6 +62,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import java.util.Optional
 
 private const val CALL_UID = 900
 
@@ -80,9 +84,13 @@
     private lateinit var controller: OngoingCallController
     private lateinit var notifCollectionListener: NotifCollectionListener
 
+    @Mock private lateinit var mockFeatureFlags: FeatureFlags
+    @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler
     @Mock private lateinit var mockOngoingCallListener: OngoingCallListener
     @Mock private lateinit var mockActivityStarter: ActivityStarter
     @Mock private lateinit var mockIActivityManager: IActivityManager
+    @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController
+    @Mock private lateinit var mockStatusBarStateController: StatusBarStateController
 
     private lateinit var chipView: View
 
@@ -94,18 +102,22 @@
         }
 
         MockitoAnnotations.initMocks(this)
-        val featureFlags = mock(FeatureFlags::class.java)
-        `when`(featureFlags.isOngoingCallStatusBarChipEnabled).thenReturn(true)
+        `when`(mockFeatureFlags.isOngoingCallStatusBarChipEnabled).thenReturn(true)
         val notificationCollection = mock(CommonNotifCollection::class.java)
 
         controller = OngoingCallController(
                 notificationCollection,
-                featureFlags,
+                mockFeatureFlags,
                 clock,
                 mockActivityStarter,
                 mainExecutor,
                 mockIActivityManager,
-                OngoingCallLogger(uiEventLoggerFake))
+                OngoingCallLogger(uiEventLoggerFake),
+                DumpManager(),
+                Optional.of(mockStatusBarWindowController),
+                Optional.of(mockSwipeStatusBarAwayGestureHandler),
+                mockStatusBarStateController,
+            )
         controller.init()
         controller.addCallback(mockOngoingCallListener)
         controller.setChipView(chipView)
@@ -131,6 +143,13 @@
     }
 
     @Test
+    fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() {
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true)
+    }
+
+    @Test
     fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() {
         notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
 
@@ -221,16 +240,14 @@
         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
 
-    /** Regression test for b/201097913. */
     @Test
-    fun onEntryCleanUp_callNotifAddedThenRemoved_listenerNotified() {
+    fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() {
         val ongoingCallNotifEntry = createOngoingCallNotifEntry()
         notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-        reset(mockOngoingCallListener)
 
-        notifCollectionListener.onEntryCleanUp(ongoingCallNotifEntry)
+        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
 
-        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
+        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false)
     }
 
     /** Regression test for b/188491504. */
@@ -250,7 +267,6 @@
         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
 
-
     @Test
     fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() {
         notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry())
@@ -376,7 +392,7 @@
         // Update the process to visible.
         uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0)
         mainExecutor.advanceClockToLast()
-        mainExecutor.runAllReady();
+        mainExecutor.runAllReady()
 
         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
@@ -397,7 +413,7 @@
         // Update the process to invisible.
         uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0)
         mainExecutor.advanceClockToLast()
-        mainExecutor.runAllReady();
+        mainExecutor.runAllReady()
 
         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
@@ -424,6 +440,120 @@
     // Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since
     // [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class.
 
+    @Test
+    fun callNotificationAdded_chipIsClickable() {
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun fullscreenIsTrue_thenCallNotificationAdded_chipNotClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(false)
+
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        assertThat(chipView.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun callNotificationAdded_thenFullscreenIsTrue_chipNotClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(false)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+
+        assertThat(chipView.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun fullscreenChangesToFalse_chipClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(false)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        // First, update to true
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+        // Then, update to false
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ false)
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun fullscreenIsTrue_butChipClickInImmersiveEnabled_chipClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(true)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    // Swipe gesture tests
+
+    @Test
+    fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
+        getStateListener().onFullscreenStateChanged(true)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .addOnGestureDetectedCallback(anyString(), any())
+    }
+
+    @Test
+    fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() {
+        getStateListener().onFullscreenStateChanged(false)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        verify(mockSwipeStatusBarAwayGestureHandler, never())
+            .addOnGestureDetectedCallback(anyString(), any())
+    }
+
+    @Test
+    fun transitionToImmersiveMode_swipeGestureCallbackAdded() {
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        getStateListener().onFullscreenStateChanged(true)
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .addOnGestureDetectedCallback(anyString(), any())
+    }
+
+    @Test
+    fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() {
+        getStateListener().onFullscreenStateChanged(true)
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        reset(mockSwipeStatusBarAwayGestureHandler)
+
+        getStateListener().onFullscreenStateChanged(false)
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .removeOnGestureDetectedCallback(anyString())
+    }
+
+    @Test
+    fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() {
+        getStateListener().onFullscreenStateChanged(true)
+        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
+        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
+        reset(mockSwipeStatusBarAwayGestureHandler)
+
+        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .removeOnGestureDetectedCallback(anyString())
+    }
+
+    // TODO(b/195839150): Add test
+    //  swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's
+    //  difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s
+    //  callbacks in test.
+
+    // END swipe gesture tests
+
     private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle)
 
     private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle)
@@ -448,6 +578,13 @@
     }
 
     private fun createNotCallNotifEntry() = NotificationEntryBuilder().build()
+
+    private fun getStateListener(): StatusBarStateController.StateListener {
+        val statusBarStateListenerCaptor = ArgumentCaptor.forClass(
+            StatusBarStateController.StateListener::class.java)
+        verify(mockStatusBarStateController).addCallback(statusBarStateListenerCaptor.capture())
+        return statusBarStateListenerCaptor.value!!
+    }
 }
 
 private val person = Person.Builder().setName("name").build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
new file mode 100644
index 0000000..30717f4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableResources;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.view.RotationPolicy;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.settings.FakeSettings;
+import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.util.wrapper.RotationPolicyWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase {
+
+    private static final String[] DEFAULT_SETTINGS = new String[]{
+            "0:0",
+            "1:2"
+    };
+
+    private final FakeSettings mFakeSettings = new FakeSettings();
+    private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+    private final FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock);
+    @Mock DeviceStateManager mDeviceStateManager;
+    RotationPolicyWrapper mFakeRotationPolicy = new FakeRotationPolicy();
+    DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController;
+    private DeviceStateManager.DeviceStateCallback mDeviceStateCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(/* testClass= */ this);
+        TestableResources resources = mContext.getOrCreateTestableResources();
+
+        ArgumentCaptor<DeviceStateManager.DeviceStateCallback> deviceStateCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(
+                        DeviceStateManager.DeviceStateCallback.class);
+
+        mDeviceStateRotationLockSettingController = new DeviceStateRotationLockSettingController(
+                mFakeSettings,
+                mFakeRotationPolicy,
+                mDeviceStateManager,
+                mFakeExecutor,
+                DEFAULT_SETTINGS
+        );
+
+        mDeviceStateRotationLockSettingController.setListening(true);
+        verify(mDeviceStateManager).registerCallback(any(),
+                deviceStateCallbackArgumentCaptor.capture());
+        mDeviceStateCallback = deviceStateCallbackArgumentCaptor.getValue();
+    }
+
+    @Test
+    public void whenSavedSettingsEmpty_defaultsLoadedAndSaved() {
+        mFakeSettings.putStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK, "",
+                UserHandle.USER_CURRENT);
+
+        mDeviceStateRotationLockSettingController.initialize();
+
+        assertThat(mFakeSettings
+                .getStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                        UserHandle.USER_CURRENT))
+                .isEqualTo("0:0:1:2");
+    }
+
+    @Test
+    public void whenNoSavedValueForDeviceState_assumeIgnored() {
+        mFakeSettings.putStringForUser(
+                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                /* value= */"0:2:1:2",
+                UserHandle.USER_CURRENT);
+        mFakeRotationPolicy.setRotationLock(true);
+        mDeviceStateRotationLockSettingController.initialize();
+
+        mDeviceStateCallback.onStateChanged(1);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse();
+
+        // Settings only exist for state 0 and 1
+        mDeviceStateCallback.onStateChanged(2);
+
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse();
+    }
+
+    @Test
+    public void whenDeviceStateSwitched_loadCorrectSetting() {
+        mFakeSettings.putStringForUser(
+                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                /* value= */"0:2:1:1",
+                UserHandle.USER_CURRENT);
+        mFakeRotationPolicy.setRotationLock(true);
+        mDeviceStateRotationLockSettingController.initialize();
+
+        mDeviceStateCallback.onStateChanged(0);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse();
+
+        mDeviceStateCallback.onStateChanged(1);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isTrue();
+
+    }
+
+    @Test
+    public void whenUserChangesSetting_saveSettingForCurrentState() {
+        mFakeSettings.putStringForUser(
+                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                /* value= */"0:1:1:2",
+                UserHandle.USER_CURRENT);
+        mFakeRotationPolicy.setRotationLock(true);
+        mDeviceStateRotationLockSettingController.initialize();
+
+        mDeviceStateCallback.onStateChanged(0);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isTrue();
+
+        mDeviceStateRotationLockSettingController
+                .onRotationLockStateChanged(/* rotationLocked= */false,
+                        /* affordanceVisible= */ true);
+
+        assertThat(mFakeSettings
+                .getStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                        UserHandle.USER_CURRENT))
+                .isEqualTo("0:2:1:2");
+    }
+
+
+    @Test
+    public void whenDeviceStateSwitchedToIgnoredState_usePreviousSetting() {
+        mFakeSettings.putStringForUser(
+                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                /* value= */"0:0:1:2",
+                UserHandle.USER_CURRENT);
+        mFakeRotationPolicy.setRotationLock(true);
+        mDeviceStateRotationLockSettingController.initialize();
+
+        mDeviceStateCallback.onStateChanged(1);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse();
+
+        mDeviceStateCallback.onStateChanged(0);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse();
+    }
+
+    @Test
+    public void whenDeviceStateSwitchedToIgnoredState_newSettingsSaveForPreviousState() {
+        mFakeSettings.putStringForUser(
+                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                /* value= */"0:0:1:2",
+                UserHandle.USER_CURRENT);
+        mFakeRotationPolicy.setRotationLock(true);
+        mDeviceStateRotationLockSettingController.initialize();
+
+        mDeviceStateCallback.onStateChanged(1);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse();
+
+        mDeviceStateCallback.onStateChanged(0);
+        assertThat(mFakeRotationPolicy.isRotationLocked()).isFalse();
+
+        mDeviceStateRotationLockSettingController
+                .onRotationLockStateChanged(/* rotationLocked= */true,
+                        /* affordanceVisible= */ true);
+
+        assertThat(mFakeSettings
+                .getStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                        UserHandle.USER_CURRENT))
+                .isEqualTo("0:0:1:1");
+    }
+
+    private static class FakeRotationPolicy implements RotationPolicyWrapper {
+
+        private boolean mRotationLock;
+
+        @Override
+        public void setRotationLock(boolean enabled) {
+            mRotationLock = enabled;
+        }
+
+        @Override
+        public void setRotationLockAtAngle(boolean enabled, int rotation) {
+            mRotationLock = enabled;
+        }
+
+        @Override
+        public int getRotationLockOrientation() {
+            throw new AssertionError("Not implemented");
+        }
+
+        @Override
+        public boolean isRotationLockToggleVisible() {
+            throw new AssertionError("Not implemented");
+        }
+
+        @Override
+        public boolean isRotationLocked() {
+            return mRotationLock;
+        }
+
+        @Override
+        public void registerRotationPolicyListener(RotationPolicy.RotationPolicyListener listener,
+                int userHandle) {
+            throw new AssertionError("Not implemented");
+        }
+
+        @Override
+        public void unregisterRotationPolicyListener(
+                RotationPolicy.RotationPolicyListener listener) {
+            throw new AssertionError("Not implemented");
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ExtensionControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ExtensionControllerImplTest.java
index 1240a48..14cc032 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ExtensionControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ExtensionControllerImplTest.java
@@ -63,8 +63,12 @@
         mPluginManager = mDependency.injectMockDependency(PluginManager.class);
         mTunerService = mDependency.injectMockDependency(TunerService.class);
         mConfigurationController = mDependency.injectMockDependency(ConfigurationController.class);
-        mExtensionController = new ExtensionControllerImpl(mContext,
-                mock(LeakDetector.class), mPluginManager, mTunerService, mConfigurationController);
+        mExtensionController = new ExtensionControllerImpl(
+                mContext,
+                mock(LeakDetector.class),
+                mPluginManager,
+                mTunerService,
+                mConfigurationController);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
index 5771472..4f1fb02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
@@ -37,6 +37,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -57,6 +58,8 @@
 public class HotspotControllerImplTest extends SysuiTestCase {
 
     @Mock
+    private DumpManager mDumpManager;
+    @Mock
     private TetheringManager mTetheringManager;
     @Mock
     private WifiManager mWifiManager;
@@ -95,7 +98,7 @@
 
         Handler handler = new Handler(mLooper.getLooper());
 
-        mController = new HotspotControllerImpl(mContext, handler, handler);
+        mController = new HotspotControllerImpl(mContext, handler, handler, mDumpManager);
         verify(mTetheringManager)
                 .registerTetheringEventCallback(any(), mTetheringCallbackCaptor.capture());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java
index 4b87ec8..8ccaf93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java
@@ -32,6 +32,7 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController;
 
 import org.junit.Before;
@@ -52,12 +53,18 @@
     private KeyguardStateController mKeyguardStateController;
     @Mock
     private SmartspaceTransitionController mSmartSpaceTransitionController;
+    @Mock
+    private DumpManager mDumpManager;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mKeyguardStateController = new KeyguardStateControllerImpl(mContext,
-                mKeyguardUpdateMonitor, mLockPatternUtils, mSmartSpaceTransitionController);
+        mKeyguardStateController = new KeyguardStateControllerImpl(
+                mContext,
+                mKeyguardUpdateMonitor,
+                mLockPatternUtils,
+                mSmartSpaceTransitionController,
+                mDumpManager);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RotationLockControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RotationLockControllerImplTest.java
new file mode 100644
index 0000000..0581264
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RotationLockControllerImplTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.view.RotationPolicy;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.wrapper.RotationPolicyWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class RotationLockControllerImplTest extends SysuiTestCase {
+
+    private static final String[] DEFAULT_SETTINGS = new String[]{
+            "0:0",
+            "1:2"
+    };
+
+    @Mock RotationPolicyWrapper mRotationPolicyWrapper;
+    @Mock DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController;
+
+    private TestableResources mResources;
+    private ArgumentCaptor<RotationPolicy.RotationPolicyListener>
+            mRotationPolicyListenerCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(/* testClass= */ this);
+        mResources = mContext.getOrCreateTestableResources();
+
+        mRotationPolicyListenerCaptor = ArgumentCaptor.forClass(
+                RotationPolicy.RotationPolicyListener.class);
+    }
+
+    @Test
+    public void whenFlagOff_doesntInteractWithDeviceStateRotationController() {
+        createRotationLockController(new String[0]);
+
+        verifyZeroInteractions(mDeviceStateRotationLockSettingController);
+    }
+
+    @Test
+    public void whenFlagOn_setListeningSetsListeningOnDeviceStateRotationController() {
+        createRotationLockController();
+
+        verify(mDeviceStateRotationLockSettingController).setListening(/* listening= */ true);
+    }
+
+    @Test
+    public void whenFlagOn_initializesDeviceStateRotationController() {
+        createRotationLockController();
+
+        verify(mDeviceStateRotationLockSettingController).initialize();
+    }
+
+    @Test
+    public void whenFlagOn_dviceStateRotationControllerAddedToCallbacks() {
+        createRotationLockController();
+        captureRotationPolicyListener().onChange();
+
+        verify(mDeviceStateRotationLockSettingController)
+                .onRotationLockStateChanged(anyBoolean(), anyBoolean());
+    }
+
+    private RotationPolicy.RotationPolicyListener captureRotationPolicyListener() {
+        verify(mRotationPolicyWrapper)
+                .registerRotationPolicyListener(mRotationPolicyListenerCaptor.capture(), anyInt());
+        return mRotationPolicyListenerCaptor.getValue();
+    }
+
+    private void createRotationLockController() {
+        createRotationLockController(DEFAULT_SETTINGS);
+    }
+    private void createRotationLockController(String[] deviceStateRotationLockDefaults) {
+        new RotationLockControllerImpl(
+                mRotationPolicyWrapper,
+                mDeviceStateRotationLockSettingController,
+                deviceStateRotationLockDefaults
+        );
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
index c38a547..d44cdb2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
@@ -49,6 +49,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -56,6 +57,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -105,7 +107,8 @@
                 mContext,
                 mHandler,
                 mBroadcastDispatcher,
-                mBgExecutor);
+                mBgExecutor,
+                Mockito.mock(DumpManager.class));
 
         verify(mBroadcastDispatcher).registerReceiverWithHandler(
                 brCaptor.capture(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
index 3431a9d..69ab9c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy
 
+import android.app.IActivityManager
 import android.app.IActivityTaskManager
 import android.app.admin.DevicePolicyManager
 import android.content.Context
@@ -31,12 +32,15 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.internal.util.LatencyTracker
 import com.android.internal.util.UserIcons
 import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.QSUserSwitcherEvent
@@ -53,10 +57,11 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
+import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -64,6 +69,7 @@
 @SmallTest
 class UserSwitcherControllerTest : SysuiTestCase() {
     @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var activityManager: IActivityManager
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
     @Mock private lateinit var handler: Handler
@@ -76,6 +82,9 @@
     @Mock private lateinit var telephonyListenerManager: TelephonyListenerManager
     @Mock private lateinit var secureSettings: SecureSettings
     @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
+    @Mock private lateinit var latencyTracker: LatencyTracker
     private lateinit var testableLooper: TestableLooper
     private lateinit var uiBgExecutor: FakeExecutor
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -106,7 +115,9 @@
 
         `when`(userManager.canAddMoreUsers()).thenReturn(true)
 
-        userSwitcherController = UserSwitcherController(context,
+        userSwitcherController = UserSwitcherController(
+                context,
+                activityManager,
                 userManager,
                 userTracker,
                 keyguardStateController,
@@ -121,14 +132,17 @@
                 activityTaskManager,
                 userDetailAdapter,
                 secureSettings,
-                uiBgExecutor)
+                uiBgExecutor,
+                interactionJankMonitor,
+                latencyTracker,
+                dumpManager)
         userSwitcherController.mPauseRefreshUsers = true
 
         picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user))
     }
 
     @Test
-    fun testAddGuest_okButtonPressed_isLogged() {
+    fun testAddGuest_okButtonPressed() {
         val emptyGuestUserRecord = UserSwitcherController.UserRecord(
                 null,
                 null,
@@ -144,6 +158,9 @@
 
         userSwitcherController.onUserListItemClicked(emptyGuestUserRecord)
         testableLooper.processAllMessages()
+        verify(interactionJankMonitor).begin(any())
+        verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH)
+        verify(activityManager).switchUser(guestInfo.id)
         assertEquals(1, uiEventLogger.numLogs())
         assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0))
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
index dcf0ef7..336f2b1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
@@ -34,6 +34,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.policy.ZenModeController.Callback;
 
 import org.junit.Before;
@@ -54,6 +55,8 @@
     ZenModeConfig mConfig;
     @Mock
     BroadcastDispatcher mBroadcastDispatcher;
+    @Mock
+    DumpManager mDumpManager;
 
     private ZenModeControllerImpl mController;
 
@@ -63,8 +66,11 @@
         mContext.addMockSystemService(NotificationManager.class, mNm);
         when(mNm.getZenModeConfig()).thenReturn(mConfig);
 
-        mController = new ZenModeControllerImpl(mContext, Handler.createAsync(Looper.myLooper()),
-                mBroadcastDispatcher);
+        mController = new ZenModeControllerImpl(
+                mContext,
+                Handler.createAsync(Looper.myLooper()),
+                mBroadcastDispatcher,
+                mDumpManager);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 07d3fc2..f89bbe8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -52,9 +52,9 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.util.settings.SecureSettings;
@@ -167,7 +167,7 @@
         assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE))
                 .isEqualTo(new OverlayIdentifier("ffff0000"));
         assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_ACCENT_COLOR))
-                .isEqualTo(new OverlayIdentifier("ff0000ff"));
+                .isEqualTo(new OverlayIdentifier("ffff0000"));
 
         // Should not ask again if changed to same value
         mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java b/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java
index 5efe05f..84e6df2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java
@@ -60,9 +60,9 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.FeatureFlags;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
new file mode 100644
index 0000000..c316402
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
@@ -0,0 +1,32 @@
+package com.android.systemui.unfold
+
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+
+class TestUnfoldTransitionProvider : UnfoldTransitionProgressProvider, TransitionProgressListener {
+
+    private val listeners = arrayListOf<TransitionProgressListener>()
+
+    override fun destroy() {
+        listeners.clear()
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners.remove(listener)
+    }
+
+    override fun onTransitionStarted() {
+        listeners.forEach { it.onTransitionStarted() }
+    }
+
+    override fun onTransitionFinished() {
+        listeners.forEach { it.onTransitionFinished() }
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        listeners.forEach { it.onTransitionProgress(progress) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
new file mode 100644
index 0000000..6ec0251
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
@@ -0,0 +1,51 @@
+package com.android.systemui.unfold
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.WallpaperController
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.AdditionalMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class UnfoldTransitionWallpaperControllerTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var wallpaperController: WallpaperController
+
+    private val progressProvider = TestUnfoldTransitionProvider()
+
+    @JvmField
+    @Rule
+    val mockitoRule = MockitoJUnit.rule()
+
+    private lateinit var unfoldWallpaperController: UnfoldTransitionWallpaperController
+
+    @Before
+    fun setup() {
+        unfoldWallpaperController = UnfoldTransitionWallpaperController(progressProvider,
+            wallpaperController)
+        unfoldWallpaperController.init()
+    }
+
+    @Test
+    fun onTransitionProgress_zoomsIn() {
+        progressProvider.onTransitionProgress(0.8f)
+
+        verify(wallpaperController).setUnfoldTransitionZoom(eq(0.2f, 0.001f))
+    }
+
+    @Test
+    fun onTransitionFinished_resetsZoom() {
+        progressProvider.onTransitionFinished()
+
+        verify(wallpaperController).setUnfoldTransitionZoom(eq(0f, 0.001f))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
new file mode 100644
index 0000000..a1d9a7b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold.updates
+
+import android.hardware.devicestate.DeviceStateManager
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class DeviceFoldStateProviderTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var hingeAngleProvider: HingeAngleProvider
+
+    @Mock
+    private lateinit var screenStatusProvider: ScreenStatusProvider
+
+    @Mock
+    private lateinit var deviceStateManager: DeviceStateManager
+
+    private lateinit var foldStateProvider: FoldStateProvider
+
+    private val foldUpdates: MutableList<Int> = arrayListOf()
+    private val hingeAngleUpdates: MutableList<Float> = arrayListOf()
+
+    private val foldStateListenerCaptor = ArgumentCaptor.forClass(FoldStateListener::class.java)
+    private var foldedDeviceState: Int = 0
+    private var unfoldedDeviceState: Int = 0
+
+    private val screenOnListenerCaptor = ArgumentCaptor.forClass(ScreenListener::class.java)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val foldedDeviceStates: IntArray = context.resources.getIntArray(
+            com.android.internal.R.array.config_foldedDeviceStates)
+        assumeTrue("Test should be launched on a foldable device",
+            foldedDeviceStates.isNotEmpty())
+
+        foldedDeviceState = foldedDeviceStates.maxOrNull()!!
+        unfoldedDeviceState = foldedDeviceState + 1
+
+        foldStateProvider = DeviceFoldStateProvider(
+            context,
+            hingeAngleProvider,
+            screenStatusProvider,
+            deviceStateManager,
+            context.mainExecutor
+        )
+
+        foldStateProvider.addCallback(object : FoldStateProvider.FoldUpdatesListener {
+            override fun onHingeAngleUpdate(angle: Float) {
+                hingeAngleUpdates.add(angle)
+            }
+
+            override fun onFoldUpdate(update: Int) {
+                foldUpdates.add(update)
+            }
+        })
+        foldStateProvider.start()
+
+        verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture())
+        verify(screenStatusProvider).addCallback(screenOnListenerCaptor.capture())
+    }
+
+    @Test
+    fun testOnFolded_emitsFinishClosedEvent() {
+        setFoldState(folded = true)
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
+    }
+
+    @Test
+    fun testOnUnfolded_emitsStartOpeningEvent() {
+        setFoldState(folded = false)
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING)
+    }
+
+    @Test
+    fun testOnFolded_stopsHingeAngleProvider() {
+        setFoldState(folded = true)
+
+        verify(hingeAngleProvider).stop()
+    }
+
+    @Test
+    fun testOnUnfolded_startsHingeAngleProvider() {
+        setFoldState(folded = false)
+
+        verify(hingeAngleProvider).start()
+    }
+
+    @Test
+    fun testFirstScreenOnEventWhenFolded_doesNotEmitEvents() {
+        setFoldState(folded = true)
+        foldUpdates.clear()
+
+        fireScreenOnEvent()
+
+        // Power button turn on
+        assertThat(foldUpdates).isEmpty()
+    }
+
+    @Test
+    fun testFirstScreenOnEventWhenUnfolded_doesNotEmitEvents() {
+        setFoldState(folded = false)
+        foldUpdates.clear()
+
+        fireScreenOnEvent()
+
+        assertThat(foldUpdates).isEmpty()
+    }
+
+    @Test
+    fun testFirstScreenOnEventAfterFoldAndUnfold_emitsUnfoldedScreenAvailableEvent() {
+        setFoldState(folded = false)
+        setFoldState(folded = true)
+        fireScreenOnEvent()
+        setFoldState(folded = false)
+        foldUpdates.clear()
+
+        fireScreenOnEvent()
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE)
+    }
+
+    @Test
+    fun testSecondScreenOnEventWhenUnfolded_doesNotEmitEvents() {
+        setFoldState(folded = false)
+        fireScreenOnEvent()
+        foldUpdates.clear()
+
+        fireScreenOnEvent()
+
+        // No events as this is power button turn on
+        assertThat(foldUpdates).isEmpty()
+    }
+
+    private fun setFoldState(folded: Boolean) {
+        val state = if (folded) foldedDeviceState else unfoldedDeviceState
+        foldStateListenerCaptor.value.onStateChanged(state)
+    }
+
+    private fun fireScreenOnEvent() {
+        screenOnListenerCaptor.value.onScreenTurnedOn()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
index eebcbe6..3d55488 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
@@ -31,7 +31,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.lang.Exception
 
 /**
  * UsbPermissionActivityTest
@@ -54,7 +53,9 @@
                 putExtra(Intent.EXTRA_INTENT, PendingIntent.getBroadcast(
                         mContext,
                         334,
-                        Intent("NO_ACTION"),
+                        Intent("NO_ACTION").apply {
+                                setPackage("com.android.systemui.tests")
+                        },
                         PendingIntent.FLAG_MUTABLE))
                 putExtra(UsbManager.EXTRA_ACCESSORY, UsbAccessory(
                         "manufacturer",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java
index f1c9980..c7bcdef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java
@@ -22,7 +22,6 @@
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.Context;
-import android.provider.Settings;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.ArraySet;
 
@@ -68,51 +67,4 @@
         list.forEach((chan) -> assertTrue(ALL_CHANNELS.contains(chan.getId())));
     }
 
-    @Test
-    public void testChannelSetup_noLegacyScreenshot() {
-        // Assert old channel cleaned up.
-        // TODO: remove that code + this test after P.
-        NotificationChannels.createAll(mContext);
-        ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
-        verify(mMockNotificationManager).deleteNotificationChannel(
-                NotificationChannels.SCREENSHOTS_LEGACY);
-    }
-
-    @Test
-    public void testInheritFromLegacy_keepsUserLockedLegacySettings() {
-        NotificationChannel legacyChannel = new NotificationChannel("id", "oldName",
-                NotificationManager.IMPORTANCE_MIN);
-        legacyChannel.setImportance(NotificationManager.IMPORTANCE_NONE);;
-        legacyChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
-                legacyChannel.getAudioAttributes());
-        legacyChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE |
-                NotificationChannel.USER_LOCKED_SOUND);
-        NotificationChannel newChannel =
-                NotificationChannels.createScreenshotChannel("newName", legacyChannel);
-        // NONE importance user locked, so don't use HIGH for new channel.
-        assertEquals(NotificationManager.IMPORTANCE_NONE, newChannel.getImportance());
-        assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, newChannel.getSound());
-    }
-
-    @Test
-    public void testInheritFromLegacy_dropsUnlockedLegacySettings() {
-        NotificationChannel legacyChannel = new NotificationChannel("id", "oldName",
-                NotificationManager.IMPORTANCE_MIN);
-        NotificationChannel newChannel =
-                NotificationChannels.createScreenshotChannel("newName", legacyChannel);
-        assertEquals(null, newChannel.getSound());
-        assertEquals("newName", newChannel.getName());
-        // MIN importance not user locked, so HIGH wins out.
-        assertEquals(NotificationManager.IMPORTANCE_HIGH, newChannel.getImportance());
-    }
-
-    @Test
-    public void testInheritFromLegacy_noLegacyExists() {
-        NotificationChannel newChannel =
-                NotificationChannels.createScreenshotChannel("newName", null);
-        assertEquals(null, newChannel.getSound());
-        assertEquals("newName", newChannel.getName());
-        assertEquals(NotificationManager.IMPORTANCE_HIGH, newChannel.getImportance());
-    }
-
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
new file mode 100644
index 0000000..d8e418a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import android.app.WallpaperInfo
+import android.app.WallpaperManager
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.view.ViewRootImpl
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+@SmallTest
+class WallpaperControllerTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var wallpaperManager: WallpaperManager
+    @Mock
+    private lateinit var root: View
+    @Mock
+    private lateinit var viewRootImpl: ViewRootImpl
+    @Mock
+    private lateinit var windowToken: IBinder
+
+    @JvmField
+    @Rule
+    val mockitoRule = MockitoJUnit.rule()
+
+    private lateinit var wallaperController: WallpaperController
+
+    @Before
+    fun setup() {
+        `when`(root.viewRootImpl).thenReturn(viewRootImpl)
+        `when`(root.windowToken).thenReturn(windowToken)
+        `when`(root.isAttachedToWindow).thenReturn(true)
+
+        wallaperController = WallpaperController(wallpaperManager)
+
+        wallaperController.rootView = root
+    }
+
+    @Test
+    fun setNotificationShadeZoom_updatesWallpaperManagerZoom() {
+        wallaperController.setNotificationShadeZoom(0.5f)
+
+        verify(wallpaperManager).setWallpaperZoomOut(any(), eq(0.5f))
+    }
+
+    @Test
+    fun setUnfoldTransitionZoom_updatesWallpaperManagerZoom() {
+        wallaperController.setUnfoldTransitionZoom(0.5f)
+
+        verify(wallpaperManager).setWallpaperZoomOut(any(), eq(0.5f))
+    }
+
+    @Test
+    fun setUnfoldTransitionZoom_defaultUnfoldTransitionIsDisabled_doesNotUpdateWallpaperZoom() {
+        wallaperController.onWallpaperInfoUpdated(createWallpaperInfo(
+            useDefaultTransition = false
+        ))
+
+        wallaperController.setUnfoldTransitionZoom(0.5f)
+
+        verify(wallpaperManager, never()).setWallpaperZoomOut(any(), anyFloat())
+    }
+
+    @Test
+    fun setUnfoldTransitionZoomAndNotificationShadeZoom_updatesWithMaximumZoom() {
+        wallaperController.setUnfoldTransitionZoom(0.7f)
+        clearInvocations(wallpaperManager)
+
+        wallaperController.setNotificationShadeZoom(0.5f)
+
+        verify(wallpaperManager).setWallpaperZoomOut(any(), eq(0.7f))
+    }
+
+    @Test
+    fun setNotificationShadeZoomAndThenUnfoldTransition_updatesWithMaximumZoom() {
+        wallaperController.setNotificationShadeZoom(0.7f)
+        clearInvocations(wallpaperManager)
+
+        wallaperController.setUnfoldTransitionZoom(0.5f)
+
+        verify(wallpaperManager).setWallpaperZoomOut(any(), eq(0.7f))
+    }
+
+    @Test
+    fun setNotificationZoom_invalidWindow_doesNotSetZoom() {
+        `when`(root.isAttachedToWindow).thenReturn(false)
+
+        verify(wallpaperManager, times(0)).setWallpaperZoomOut(any(), anyFloat())
+    }
+
+    @Test
+    fun setNotificationZoom_exceptionWhenUpdatingZoom_doesNotFail() {
+        doThrow(IllegalArgumentException("test exception")).`when`(wallpaperManager)
+            .setWallpaperZoomOut(any(), anyFloat())
+
+        wallaperController.setNotificationShadeZoom(0.5f)
+
+        verify(wallpaperManager).setWallpaperZoomOut(any(), anyFloat())
+    }
+
+    private fun createWallpaperInfo(useDefaultTransition: Boolean = true): WallpaperInfo {
+        val info = mock(WallpaperInfo::class.java)
+        whenever(info.shouldUseDefaultUnfoldTransition())
+            .thenReturn(useDefaultTransition)
+        return info
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MessageRouterImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MessageRouterImplTest.java
new file mode 100644
index 0000000..78fc680
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/MessageRouterImplTest.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.concurrency;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class MessageRouterImplTest extends SysuiTestCase {
+    private static final int MESSAGE_A = 0;
+    private static final int MESSAGE_B = 1;
+    private static final int MESSAGE_C = 2;
+
+    private static final String METADATA_A = "A";
+    private static final String METADATA_B = "B";
+    private static final String METADATA_C = "C";
+    private static final Foobar METADATA_FOO = new Foobar();
+
+    FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+    @Mock
+    MessageRouter.SimpleMessageListener mNoMdListener;
+    @Mock
+    MessageRouter.DataMessageListener<String> mStringListener;
+    @Mock
+    MessageRouter.DataMessageListener<Foobar> mFoobarListener;
+    private MessageRouterImpl mMR;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mMR = new MessageRouterImpl(mFakeExecutor);
+    }
+
+    @Test
+    public void testSingleMessage_NoMetaData() {
+        mMR.subscribeTo(MESSAGE_A, mNoMdListener);
+
+        mMR.sendMessage(MESSAGE_A);
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener).onMessage(MESSAGE_A);
+    }
+
+    @Test
+    public void testSingleMessage_WithMetaData() {
+        mMR.subscribeTo(String.class, mStringListener);
+
+        mMR.sendMessage(METADATA_A);
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener).onMessage(METADATA_A);
+    }
+
+    @Test
+    public void testMessages_WithMixedMetaData() {
+        mMR.subscribeTo(String.class, mStringListener);
+        mMR.subscribeTo(Foobar.class, mFoobarListener);
+
+        mMR.sendMessage(METADATA_A);
+        verify(mStringListener, never()).onMessage(anyString());
+        verify(mFoobarListener, never()).onMessage(any(Foobar.class));
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener).onMessage(METADATA_A);
+        verify(mFoobarListener, never()).onMessage(any(Foobar.class));
+
+        reset(mStringListener);
+        reset(mFoobarListener);
+
+        mMR.sendMessage(METADATA_FOO);
+        verify(mStringListener, never()).onMessage(anyString());
+        verify(mFoobarListener, never()).onMessage(any(Foobar.class));
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener, never()).onMessage(anyString());
+        verify(mFoobarListener).onMessage(METADATA_FOO);
+    }
+
+    @Test
+    public void testMessages_WithAndWithoutMetaData() {
+        mMR.subscribeTo(MESSAGE_A, mNoMdListener);
+        mMR.subscribeTo(String.class, mStringListener);
+
+        mMR.sendMessage(MESSAGE_A);
+        verify(mNoMdListener, never()).onMessage(anyInt());
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener).onMessage(MESSAGE_A);
+        verify(mStringListener, never()).onMessage(anyString());
+
+        reset(mNoMdListener);
+        reset(mStringListener);
+
+        mMR.sendMessage(METADATA_A);
+        verify(mNoMdListener, never()).onMessage(anyInt());
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener, never()).onMessage(anyInt());
+        verify(mStringListener).onMessage(METADATA_A);
+    }
+
+    @Test
+    public void testRepeatedMessage() {
+        mMR.subscribeTo(String.class, mStringListener);
+
+        mMR.sendMessage(METADATA_A);
+        mMR.sendMessage(METADATA_A);
+        mMR.sendMessage(METADATA_B);
+        verify(mStringListener, never()).onMessage(anyString());
+
+        InOrder ordered = inOrder(mStringListener);
+        mFakeExecutor.runNextReady();
+        ordered.verify(mStringListener).onMessage(METADATA_A);
+        mFakeExecutor.runNextReady();
+        ordered.verify(mStringListener).onMessage(METADATA_A);
+        mFakeExecutor.runNextReady();
+        ordered.verify(mStringListener).onMessage(METADATA_B);
+    }
+
+    @Test
+    public void testCancelMessage() {
+        mMR.subscribeTo(MESSAGE_A, mNoMdListener);
+        mMR.subscribeTo(MESSAGE_B, mNoMdListener);
+        mMR.subscribeTo(MESSAGE_C, mNoMdListener);
+
+        mMR.sendMessage(MESSAGE_A);
+        mMR.sendMessage(MESSAGE_B);
+        mMR.sendMessage(MESSAGE_B);
+        mMR.sendMessage(MESSAGE_B);
+        mMR.sendMessage(MESSAGE_B);
+        mMR.sendMessage(MESSAGE_C);
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mMR.cancelMessages(MESSAGE_B);
+
+        mFakeExecutor.runAllReady();
+
+        InOrder ordered = inOrder(mNoMdListener);
+        ordered.verify(mNoMdListener).onMessage(MESSAGE_A);
+        ordered.verify(mNoMdListener).onMessage(MESSAGE_C);
+    }
+
+    @Test
+    public void testSendMessage_NoSubscriber() {
+        mMR.sendMessage(MESSAGE_A);
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mMR.subscribeTo(MESSAGE_A, mNoMdListener);
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mMR.sendMessage(MESSAGE_A);
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener).onMessage(MESSAGE_A);
+    }
+
+    @Test
+    public void testUnsubscribe_SpecificMessage_NoMetadata() {
+        mMR.subscribeTo(MESSAGE_A, mNoMdListener);
+        mMR.subscribeTo(MESSAGE_B, mNoMdListener);
+        mMR.sendMessage(MESSAGE_A);
+        mMR.sendMessage(MESSAGE_B);
+
+        mFakeExecutor.runAllReady();
+        InOrder ordered = inOrder(mNoMdListener);
+        ordered.verify(mNoMdListener).onMessage(MESSAGE_A);
+        ordered.verify(mNoMdListener).onMessage(MESSAGE_B);
+
+        reset(mNoMdListener);
+        mMR.unsubscribeFrom(MESSAGE_A, mNoMdListener);
+        mMR.sendMessage(MESSAGE_A);
+        mMR.sendMessage(MESSAGE_B);
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener, never()).onMessage(MESSAGE_A);
+        verify(mNoMdListener).onMessage(MESSAGE_B);
+    }
+
+    @Test
+    public void testUnsubscribe_SpecificMessage_WithMetadata() {
+        mMR.subscribeTo(String.class, mStringListener);
+        mMR.subscribeTo(Foobar.class, mFoobarListener);
+        mMR.sendMessage(METADATA_A);
+        mMR.sendMessage(METADATA_FOO);
+
+        mFakeExecutor.runNextReady();
+        verify(mStringListener).onMessage(METADATA_A);
+        mFakeExecutor.runNextReady();
+        verify(mFoobarListener).onMessage(METADATA_FOO);
+
+        reset(mStringListener);
+        reset(mFoobarListener);
+        mMR.unsubscribeFrom(String.class, mStringListener);
+        mMR.sendMessage(METADATA_A);
+        mMR.sendMessage(METADATA_FOO);
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener, never()).onMessage(METADATA_A);
+        verify(mFoobarListener).onMessage(METADATA_FOO);
+    }
+
+    @Test
+    public void testUnsubscribe_AllMessages_NoMetadata() {
+        mMR.subscribeTo(MESSAGE_A, mNoMdListener);
+        mMR.subscribeTo(MESSAGE_B, mNoMdListener);
+        mMR.subscribeTo(MESSAGE_C, mNoMdListener);
+
+        mMR.sendMessage(MESSAGE_A);
+        mMR.sendMessage(MESSAGE_B);
+        mMR.sendMessage(MESSAGE_C);
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener).onMessage(MESSAGE_A);
+        verify(mNoMdListener).onMessage(MESSAGE_B);
+        verify(mNoMdListener).onMessage(MESSAGE_C);
+
+        reset(mNoMdListener);
+
+        mMR.unsubscribeFrom(mNoMdListener);
+        mMR.sendMessage(MESSAGE_A);
+        mMR.sendMessage(MESSAGE_B);
+        mMR.sendMessage(MESSAGE_C);
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener, never()).onMessage(MESSAGE_A);
+        verify(mNoMdListener, never()).onMessage(MESSAGE_B);
+        verify(mNoMdListener, never()).onMessage(MESSAGE_C);
+    }
+
+    @Test
+    public void testUnsubscribe_AllMessages_WithMetadata() {
+        mMR.subscribeTo(String.class, mStringListener);
+
+        mMR.sendMessage(METADATA_A);
+        mMR.sendMessage(METADATA_B);
+        mMR.sendMessage(METADATA_C);
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener).onMessage(METADATA_A);
+        verify(mStringListener).onMessage(METADATA_B);
+        verify(mStringListener).onMessage(METADATA_C);
+
+        reset(mStringListener);
+
+        mMR.unsubscribeFrom(mStringListener);
+        mMR.sendMessage(METADATA_A);
+        mMR.sendMessage(METADATA_B);
+        mMR.sendMessage(METADATA_C);
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener, never()).onMessage(METADATA_A);
+        verify(mStringListener, never()).onMessage(METADATA_B);
+        verify(mStringListener, never()).onMessage(METADATA_C);
+    }
+
+    @Test
+    public void testSingleDelayedMessage_NoMetaData() {
+        mMR.subscribeTo(MESSAGE_A, mNoMdListener);
+
+        mMR.sendMessageDelayed(MESSAGE_A, 100);
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener, never()).onMessage(anyInt());
+
+        mFakeExecutor.advanceClockToNext();
+        mFakeExecutor.runAllReady();
+        verify(mNoMdListener).onMessage(MESSAGE_A);
+    }
+
+    @Test
+    public void testSingleDelayedMessage_WithMetaData() {
+        mMR.subscribeTo(String.class, mStringListener);
+
+        mMR.sendMessageDelayed(METADATA_C, 1000);
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.advanceClockToNext();
+        mFakeExecutor.runAllReady();
+        verify(mStringListener).onMessage(METADATA_C);
+    }
+
+    @Test
+    public void testMultipleDelayedMessages() {
+        mMR.subscribeTo(String.class, mStringListener);
+
+        mMR.sendMessageDelayed(METADATA_A, 100);
+        mMR.sendMessageDelayed(METADATA_B, 1000);
+        mMR.sendMessageDelayed(METADATA_C, 500);
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.advanceClockToLast();
+        mFakeExecutor.runAllReady();
+
+        InOrder ordered = inOrder(mStringListener);
+        ordered.verify(mStringListener).onMessage(METADATA_A);
+        ordered.verify(mStringListener).onMessage(METADATA_C);
+        ordered.verify(mStringListener).onMessage(METADATA_B);
+    }
+
+    @Test
+    public void testCancelDelayedMessages() {
+        mMR.subscribeTo(String.class, mStringListener);
+
+        mMR.sendMessageDelayed(METADATA_A, 100);
+        mMR.sendMessageDelayed(METADATA_B, 1000);
+        mMR.sendMessageDelayed(METADATA_C, 500);
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mFakeExecutor.runAllReady();
+        verify(mStringListener, never()).onMessage(anyString());
+
+        mMR.cancelMessages(String.class);
+        mFakeExecutor.advanceClockToLast();
+        mFakeExecutor.runAllReady();
+
+        verify(mStringListener, never()).onMessage(anyString());
+    }
+
+    private static class Foobar {}
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
index bcc20c2..a2b016f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
@@ -17,124 +17,97 @@
 package com.android.systemui.util.leak;
 
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.content.Context;
-import android.os.Looper;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableLooper.RunWithLooper;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.concurrency.MessageRouterImpl;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper
 public class GarbageMonitorTest extends SysuiTestCase {
 
-    private LeakReporter mLeakReporter;
-    private TrackedGarbage mTrackedGarbage;
-    private TestableGarbageMonitor mGarbageMonitor;
+    @Mock private LeakReporter mLeakReporter;
+    @Mock private TrackedGarbage mTrackedGarbage;
+    @Mock private DumpManager mDumpManager;
+    private GarbageMonitor mGarbageMonitor;
+    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
 
     @Before
     public void setup() {
-        mTrackedGarbage = mock(TrackedGarbage.class);
-        mLeakReporter = mock(LeakReporter.class);
+        MockitoAnnotations.initMocks(this);
         mGarbageMonitor =
-                new TestableGarbageMonitor(
+                new GarbageMonitor(
                         mContext,
-                        TestableLooper.get(this).getLooper(),
-                        new LeakDetector(null, mTrackedGarbage, null),
-                        mLeakReporter);
-    }
-
-    @Test
-    public void testCallbacks_getScheduled() {
-        mGarbageMonitor.startLeakMonitor();
-        mGarbageMonitor.runCallbacksOnce();
-        mGarbageMonitor.runCallbacksOnce();
-        mGarbageMonitor.runCallbacksOnce();
-    }
-
-    @Test
-    public void testNoGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
-
-        mGarbageMonitor.startLeakMonitor();
-        mGarbageMonitor.runCallbacksOnce();
-        mGarbageMonitor.runCallbacksOnce();
-        mGarbageMonitor.runCallbacksOnce();
-
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
+                        mFakeExecutor,
+                        new MessageRouterImpl(mFakeExecutor),
+                        new LeakDetector(null, mTrackedGarbage, null, mDumpManager),
+                        mLeakReporter,
+                        mDumpManager);
     }
 
     @Test
     public void testALittleGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(4);
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE);
 
-        mGarbageMonitor.startLeakMonitor();
-        mGarbageMonitor.runCallbacksOnce();
-        mGarbageMonitor.runCallbacksOnce();
-        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.reinspectGarbageAfterGc();
 
         verify(mLeakReporter, never()).dumpLeak(anyInt());
     }
 
     @Test
     public void testTransientGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(100);
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
 
+        // Start the leak monitor. Nothing gets reported immediately.
         mGarbageMonitor.startLeakMonitor();
-        mGarbageMonitor.runInspectCallback();
+        mFakeExecutor.runAllReady();
+        verify(mLeakReporter, never()).dumpLeak(anyInt());
 
+        // Garbage gets reset to 0 before the leak reporte actually gets called.
         when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
+        mFakeExecutor.advanceClockToLast();
+        mFakeExecutor.runAllReady();
 
-        mGarbageMonitor.runReinspectCallback();
-
+        // Therefore nothing gets dumped.
         verify(mLeakReporter, never()).dumpLeak(anyInt());
     }
 
     @Test
     public void testLotsOfPersistentGarbage_dumps() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(100);
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
 
-        mGarbageMonitor.startLeakMonitor();
-        mGarbageMonitor.runCallbacksOnce();
+        mGarbageMonitor.reinspectGarbageAfterGc();
 
-        verify(mLeakReporter).dumpLeak(anyInt());
+        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
     }
 
-    private static class TestableGarbageMonitor extends GarbageMonitor {
-        public TestableGarbageMonitor(
-                Context context,
-                Looper looper,
-                LeakDetector leakDetector,
-                LeakReporter leakReporter) {
-            super(context, looper, leakDetector, leakReporter);
-        }
+    @Test
+    public void testLotsOfPersistentGarbage_dumpsAfterAtime() {
+        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
 
-        void runInspectCallback() {
-            startLeakMonitor();
-        }
+        // Start the leak monitor. Nothing gets reported immediately.
+        mGarbageMonitor.startLeakMonitor();
+        mFakeExecutor.runAllReady();
+        verify(mLeakReporter, never()).dumpLeak(anyInt());
 
-        void runReinspectCallback() {
-            reinspectGarbageAfterGc();
-        }
+        mFakeExecutor.advanceClockToLast();
+        mFakeExecutor.runAllReady();
 
-        void runCallbacksOnce() {
-            // Note that TestableLooper doesn't currently support delayed messages so we need to run
-            // callbacks explicitly.
-            runInspectCallback();
-            runReinspectCallback();
-        }
+        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java
index c68c920..6e42f0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java
@@ -21,12 +21,14 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.util.leak.ReferenceTestUtils.CollectionWaiter;
 
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 import java.io.FileOutputStream;
 import java.io.PrintWriter;
@@ -67,7 +69,7 @@
 
     @Before
     public void setup() {
-        mLeakDetector = LeakDetector.create();
+        mLeakDetector = LeakDetector.create(Mockito.mock(DumpManager.class));
 
         // Note: Do not try to factor out object / collection waiter creation. The optimizer will
         // try and cache accesses to fields and thus create a GC root for the duration of the test
@@ -112,7 +114,7 @@
 
     @Test
     public void testDisabled() throws Exception {
-        mLeakDetector = new LeakDetector(null, null, null);
+        mLeakDetector = new LeakDetector(null, null, null, Mockito.mock(DumpManager.class));
 
         Object o1 = new Object();
         Object o2 = new Object();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java
index 50947ab..22cf744 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java
@@ -19,14 +19,21 @@
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.FakeExecution;
 
-public class FakeProximitySensor extends ProximitySensor {
+public class FakeProximitySensor extends ProximitySensorImpl {
     private boolean mAvailable;
     private boolean mRegistered;
 
-    public FakeProximitySensor(ThresholdSensor primary, ThresholdSensor secondary,
-            DelayableExecutor delayableExecutor) {
-        super(primary, secondary == null ? new FakeThresholdSensor() : secondary,
-                delayableExecutor, new FakeExecution());
+    public FakeProximitySensor(
+            ThresholdSensor primary,
+            ThresholdSensor secondary,
+            DelayableExecutor delayableExecutor
+    ) {
+        super(
+                primary,
+                secondary == null ? new FakeThresholdSensor() : secondary,
+                delayableExecutor,
+                new FakeExecution()
+        );
         mAvailable = true;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeSensorManager.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeSensorManager.java
index 6e73827..197873f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeSensorManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeSensorManager.java
@@ -55,6 +55,7 @@
 
     private final FakeProximitySensor mFakeProximitySensor;
     private final FakeGenericSensor mFakeLightSensor;
+    private final FakeGenericSensor mFakeLightSensor2;
     private final FakeGenericSensor mFakeTapSensor;
     private final FakeGenericSensor[] mSensors;
 
@@ -70,7 +71,8 @@
         mSensors = new FakeGenericSensor[]{
                 mFakeProximitySensor = new FakeProximitySensor(proxSensor),
                 mFakeLightSensor = new FakeGenericSensor(createSensor(Sensor.TYPE_LIGHT, null)),
-                mFakeTapSensor = new FakeGenericSensor(createSensor(99, TAP_SENSOR_TYPE))
+                mFakeTapSensor = new FakeGenericSensor(createSensor(99, TAP_SENSOR_TYPE)),
+                mFakeLightSensor2 = new FakeGenericSensor(createSensor(Sensor.TYPE_LIGHT, null))
         };
     }
 
@@ -82,6 +84,10 @@
         return mFakeLightSensor;
     }
 
+    public FakeGenericSensor getFakeLightSensor2() {
+        return mFakeLightSensor2;
+    }
+
     public FakeGenericSensor getFakeTapSensor() {
         return mFakeTapSensor;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
index d9f9789..0d4a6c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
@@ -59,6 +59,16 @@
         mListeners.remove(listener);
     }
 
+    @Override
+    public String getName() {
+        return "FakeThresholdSensorName";
+    }
+
+    @Override
+    public String getType() {
+        return "FakeThresholdSensorType";
+    }
+
     public void setLoaded(boolean loaded) {
         mIsLoaded = loaded;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/PostureDependentProximitySensorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/PostureDependentProximitySensorTest.java
new file mode 100644
index 0000000..075f393
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/PostureDependentProximitySensorTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.FakeExecution;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class PostureDependentProximitySensorTest extends SysuiTestCase {
+    @Mock private Resources mResources;
+    @Mock private DevicePostureController mDevicePostureController;
+    @Mock private AsyncSensorManager mSensorManager;
+
+    @Captor private ArgumentCaptor<DevicePostureController.Callback> mPostureListenerCaptor =
+            ArgumentCaptor.forClass(DevicePostureController.Callback.class);
+    private DevicePostureController.Callback mPostureListener;
+
+    private PostureDependentProximitySensor mProximitySensor;
+    private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        allowTestableLooperAsMainThread();
+
+        mProximitySensor = new PostureDependentProximitySensor(
+                new ThresholdSensor[DevicePostureController.SUPPORTED_POSTURES_SIZE],
+                new ThresholdSensor[DevicePostureController.SUPPORTED_POSTURES_SIZE],
+                mFakeExecutor,
+                new FakeExecution(),
+                mDevicePostureController
+        );
+    }
+
+    @Test
+    public void testPostureChangeListenerAdded() {
+        capturePostureListener();
+    }
+
+    @Test
+    public void testPostureChangeListenerUpdatesPosture() {
+        // GIVEN posture listener is registered
+        capturePostureListener();
+
+        // WHEN the posture changes to DEVICE_POSTURE_OPENED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_OPENED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_OPENED,
+                mProximitySensor.mDevicePosture);
+
+        // WHEN the posture changes to DEVICE_POSTURE_CLOSED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_CLOSED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_CLOSED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_CLOSED,
+                mProximitySensor.mDevicePosture);
+
+        // WHEN the posture changes to DEVICE_POSTURE_FLIPPED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_FLIPPED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_FLIPPED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_FLIPPED,
+                mProximitySensor.mDevicePosture);
+
+        // WHEN the posture changes to DEVICE_POSTURE_HALF_OPENED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_HALF_OPENED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_HALF_OPENED,
+                mProximitySensor.mDevicePosture);
+    }
+
+    private void capturePostureListener() {
+        verify(mDevicePostureController).addCallback(mPostureListenerCaptor.capture());
+        mPostureListener = mPostureListenerCaptor.getValue();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java
index 242fe9f..19dbf9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java
@@ -49,7 +49,7 @@
 
     private TestableCallback mTestableCallback = new TestableCallback();
 
-    private ProximitySensor.ProximityCheck mProximityCheck;
+    private ProximityCheck mProximityCheck;
 
     @Before
     public void setUp() throws Exception {
@@ -58,7 +58,7 @@
         thresholdSensor.setLoaded(true);
         mFakeProximitySensor = new FakeProximitySensor(thresholdSensor, null, mFakeExecutor);
 
-        mProximityCheck = new ProximitySensor.ProximityCheck(mFakeProximitySensor, mFakeExecutor);
+        mProximityCheck = new ProximityCheck(mFakeProximitySensor, mFakeExecutor);
     }
 
     @Test
@@ -67,7 +67,7 @@
 
         assertNull(mTestableCallback.mLastResult);
 
-        mFakeProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 0));
+        mFakeProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 0));
         mFakeProximitySensor.alertListeners();
 
         assertTrue(mTestableCallback.mLastResult);
@@ -103,7 +103,7 @@
 
         mProximityCheck.check(100, mTestableCallback);
 
-        mFakeProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 0));
+        mFakeProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 0));
         mFakeProximitySensor.alertListeners();
 
         assertThat(mTestableCallback.mLastResult).isNotNull();
@@ -123,7 +123,7 @@
 
         assertNull(mTestableCallback.mLastResult);
 
-        mFakeProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 0));
+        mFakeProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 0));
         mFakeProximitySensor.alertListeners();
 
         assertTrue(mTestableCallback.mLastResult);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorDualTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplDualTest.java
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorDualTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplDualTest.java
index 0e9d96c..5e75578 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorDualTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplDualTest.java
@@ -42,7 +42,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class ProximitySensorDualTest extends SysuiTestCase {
+public class ProximitySensorImplDualTest extends SysuiTestCase {
     private ProximitySensor mProximitySensor;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeThresholdSensor mThresholdSensorPrimary;
@@ -57,7 +57,7 @@
         mThresholdSensorSecondary = new FakeThresholdSensor();
         mThresholdSensorSecondary.setLoaded(true);
 
-        mProximitySensor = new ProximitySensor(
+        mProximitySensor = new ProximitySensorImpl(
                 mThresholdSensorPrimary, mThresholdSensorSecondary, mFakeExecutor,
                 new FakeExecution());
     }
@@ -430,11 +430,11 @@
     }
 
     private static class TestableListener implements ThresholdSensor.Listener {
-        ThresholdSensor.ThresholdSensorEvent mLastEvent;
+        ThresholdSensorEvent mLastEvent;
         int mCallCount = 0;
 
         @Override
-        public void onThresholdCrossed(ThresholdSensor.ThresholdSensorEvent proximityEvent) {
+        public void onThresholdCrossed(ThresholdSensorEvent proximityEvent) {
             mLastEvent = proximityEvent;
             mCallCount++;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorSingleTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplSingleTest.java
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorSingleTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplSingleTest.java
index 6c6d355..752cd32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorSingleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplSingleTest.java
@@ -42,7 +42,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class ProximitySensorSingleTest extends SysuiTestCase {
+public class ProximitySensorImplSingleTest extends SysuiTestCase {
     private ProximitySensor mProximitySensor;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeThresholdSensor mThresholdSensor;
@@ -54,7 +54,7 @@
         mThresholdSensor = new FakeThresholdSensor();
         mThresholdSensor.setLoaded(true);
 
-        mProximitySensor = new ProximitySensor(
+        mProximitySensor = new ProximitySensorImpl(
                 mThresholdSensor, new FakeThresholdSensor(), mFakeExecutor, new FakeExecution());
     }
 
@@ -215,7 +215,7 @@
     public void testPreventRecursiveAlert() {
         TestableListener listenerA = new TestableListener() {
             @Override
-            public void onThresholdCrossed(ProximitySensor.ThresholdSensorEvent proximityEvent) {
+            public void onThresholdCrossed(ThresholdSensorEvent proximityEvent) {
                 super.onThresholdCrossed(proximityEvent);
                 if (mCallCount < 2) {
                     mProximitySensor.alertListeners();
@@ -231,11 +231,11 @@
     }
 
     private static class TestableListener implements ThresholdSensor.Listener {
-        ThresholdSensor.ThresholdSensorEvent mLastEvent;
+        ThresholdSensorEvent mLastEvent;
         int mCallCount = 0;
 
         @Override
-        public void onThresholdCrossed(ThresholdSensor.ThresholdSensorEvent proximityEvent) {
+        public void onThresholdCrossed(ThresholdSensorEvent proximityEvent) {
             mLastEvent = proximityEvent;
             mCallCount++;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java
index 125063a..b10f16c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java
@@ -380,7 +380,7 @@
         int mCallCount;
 
         @Override
-        public void onThresholdCrossed(ThresholdSensor.ThresholdSensorEvent event) {
+        public void onThresholdCrossed(ThresholdSensorEvent event) {
             mBelow = event.getBelow();
             mTimestampNs = event.getTimestampNs();
             mCallCount++;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettings.java
index 7bb2674..e66491e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettings.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettings.java
@@ -123,11 +123,11 @@
 
         Uri uri = getUriFor(name);
         for (ContentObserver observer : mContentObservers.getOrDefault(key, new ArrayList<>())) {
-            observer.dispatchChange(false, List.of(uri), userHandle);
+            observer.dispatchChange(false, List.of(uri), 0, userHandle);
         }
         for (ContentObserver observer :
                 mContentObserversAllUsers.getOrDefault(uri.toString(), new ArrayList<>())) {
-            observer.dispatchChange(false, List.of(uri), userHandle);
+            observer.dispatchChange(false, List.of(uri), 0, userHandle);
         }
         return true;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettingsTest.java
index 34cae58..f65caee2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettingsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettingsTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
@@ -86,7 +87,8 @@
 
         mFakeSettings.putString("cat", "hat");
 
-        verify(mContentObserver).dispatchChange(anyBoolean(), any(Collection.class), anyInt());
+        verify(mContentObserver).dispatchChange(anyBoolean(), any(Collection.class), anyInt(),
+                anyInt());
     }
 
     @Test
@@ -96,7 +98,8 @@
 
         mFakeSettings.putString("cat", "hat");
 
-        verify(mContentObserver).dispatchChange(anyBoolean(), any(Collection.class), anyInt());
+        verify(mContentObserver).dispatchChange(anyBoolean(), any(Collection.class), anyInt(),
+                anyInt());
     }
 
     @Test
@@ -119,6 +122,18 @@
         mFakeSettings.putString("cat", "hat");
 
         verify(mContentObserver, never()).dispatchChange(
-                anyBoolean(), any(Collection.class), anyInt());
+                anyBoolean(), any(Collection.class), anyInt(), anyInt());
+    }
+
+    @Test
+    public void testContentObserverDispatchCorrectUser() {
+        int user = 10;
+        mFakeSettings.registerContentObserverForUser(
+                mFakeSettings.getUriFor("cat"), false, mContentObserver, UserHandle.USER_ALL
+        );
+
+        mFakeSettings.putStringForUser("cat", "hat", user);
+        verify(mContentObserver).dispatchChange(anyBoolean(), any(Collection.class), anyInt(),
+                eq(user));
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java
index e7acfae..8ea9da6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java
@@ -18,9 +18,9 @@
 import android.testing.LeakCheck;
 
 import com.android.settingslib.net.DataUsageController;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.NetworkController.SignalCallback;
 import com.android.systemui.statusbar.policy.DataSaverController;
-import com.android.systemui.statusbar.policy.NetworkController;
-import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
 
 public class FakeNetworkController extends BaseLeakChecker<SignalCallback>
         implements NetworkController {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
index 6d1e6ce..d245c72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
@@ -22,7 +22,7 @@
 
 public class FakePluginManager implements PluginManager {
 
-    private final BaseLeakChecker<PluginListener> mLeakChecker;
+    private final BaseLeakChecker<PluginListener<?>> mLeakChecker;
 
     public FakePluginManager(LeakCheck test) {
         mLeakChecker = new BaseLeakChecker<>(test, "Plugin");
@@ -30,24 +30,24 @@
 
     @Override
     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            Class cls, boolean allowMultiple) {
+            Class<T> cls, boolean allowMultiple) {
         mLeakChecker.addCallback(listener);
     }
 
     @Override
-    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls) {
         mLeakChecker.addCallback(listener);
     }
 
     @Override
-    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
             boolean allowMultiple) {
         mLeakChecker.addCallback(listener);
     }
 
     @Override
     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            Class<?> cls) {
+            Class<T> cls) {
         mLeakChecker.addCallback(listener);
     }
 
@@ -62,17 +62,7 @@
     }
 
     @Override
-    public String[] getWhitelistedPlugins() {
+    public String[] getPrivilegedPlugins() {
         return new String[0];
     }
-
-    @Override
-    public <T extends Plugin> T getOneShotPlugin(Class<T> cls) {
-        return null;
-    }
-
-    @Override
-    public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
-        return null;
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
index 4f9cb35..be11024 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
@@ -51,11 +51,6 @@
     }
 
     @Override
-    public boolean isCameraRotationEnabled() {
-        return false;
-    }
-
-    @Override
     public void setRotationLockedAtAngle(boolean locked, int rotation) {
 
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
index fedc08d..dc6a8fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
@@ -19,6 +19,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -29,7 +30,6 @@
 import com.android.systemui.statusbar.policy.HotspotController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.LocationController;
-import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
 import com.android.systemui.statusbar.policy.RotationLockController;
 import com.android.systemui.statusbar.policy.SecurityController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index dd4830e..9493456 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -41,9 +41,13 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.VolumeDialogController.State;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -68,23 +72,34 @@
     View mDrawerNormal;
 
     @Mock
-    VolumeDialogController mController;
-
+    VolumeDialogController mVolumeDialogController;
     @Mock
     KeyguardManager mKeyguard;
-
     @Mock
     AccessibilityManagerWrapper mAccessibilityMgr;
+    @Mock
+    DeviceProvisionedController mDeviceProvisionedController;
+    @Mock
+    ConfigurationController mConfigurationController;
+    @Mock
+    MediaOutputDialogFactory mMediaOutputDialogFactory;
+    @Mock
+    ActivityStarter mActivityStarter;
 
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mController = mDependency.injectMockDependency(VolumeDialogController.class);
-        mAccessibilityMgr = mDependency.injectMockDependency(AccessibilityManagerWrapper.class);
         getContext().addMockSystemService(KeyguardManager.class, mKeyguard);
 
-        mDialog = new VolumeDialogImpl(getContext());
+        mDialog = new VolumeDialogImpl(
+                getContext(),
+                mVolumeDialogController,
+                mAccessibilityMgr,
+                mDeviceProvisionedController,
+                mConfigurationController,
+                mMediaOutputDialogFactory,
+                mActivityStarter);
         mDialog.init(0, null);
         State state = createShellState();
         mDialog.onStateChangedH(state);
@@ -170,7 +185,7 @@
         Mockito.reset(mAccessibilityMgr);
         ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture =
                 ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class);
-        verify(mController).addCallback(controllerCallbackCapture.capture(), any());
+        verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any());
         VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue();
         callbacks.onShowSafetyWarning(AudioManager.FLAG_SHOW_UI);
         verify(mAccessibilityMgr).getRecommendedTimeoutMillis(
@@ -201,13 +216,13 @@
         mDialog.onStateChangedH(initialSilentState);
 
         // expected: shouldn't call vibrate yet
-        verify(mController, never()).vibrate(any());
+        verify(mVolumeDialogController, never()).vibrate(any());
 
         // changed ringer to vibrate
         mDialog.onStateChangedH(vibrateState);
 
         // expected: vibrate device
-        verify(mController).vibrate(any());
+        verify(mVolumeDialogController).vibrate(any());
     }
 
     @Test
@@ -225,7 +240,7 @@
         mDialog.onStateChangedH(vibrateState);
 
         // shouldn't call vibrate
-        verify(mController, never()).vibrate(any());
+        verify(mVolumeDialogController, never()).vibrate(any());
     }
 
     @Test
@@ -238,7 +253,7 @@
         mDrawerVibrate.performClick();
 
         // Make sure we've actually changed the ringer mode.
-        verify(mController, times(1)).setRingerMode(
+        verify(mVolumeDialogController, times(1)).setRingerMode(
                 AudioManager.RINGER_MODE_VIBRATE, false);
     }
 
@@ -252,7 +267,7 @@
         mDrawerMute.performClick();
 
         // Make sure we've actually changed the ringer mode.
-        verify(mController, times(1)).setRingerMode(
+        verify(mVolumeDialogController, times(1)).setRingerMode(
                 AudioManager.RINGER_MODE_SILENT, false);
     }
 
@@ -266,7 +281,7 @@
         mDrawerNormal.performClick();
 
         // Make sure we've actually changed the ringer mode.
-        verify(mController, times(1)).setRingerMode(
+        verify(mVolumeDialogController, times(1)).setRingerMode(
                 AudioManager.RINGER_MODE_NORMAL, false);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index f243077..1159e09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -24,6 +24,7 @@
 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NOTIF_CANCEL;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -75,11 +76,11 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
 import com.android.systemui.statusbar.RankingBuilder;
@@ -120,6 +121,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 
 import com.google.common.collect.ImmutableList;
@@ -177,6 +179,7 @@
 
     private SysUiState mSysUiState;
     private boolean mSysUiStateBubblesExpanded;
+    private boolean mSysUiStateBubblesManageMenuExpanded;
 
     @Captor
     private ArgumentCaptor<NotificationEntryListener> mEntryListenerCaptor;
@@ -293,9 +296,13 @@
         when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
 
         mSysUiState = new SysUiState();
-        mSysUiState.addCallback(sysUiFlags ->
-                mSysUiStateBubblesExpanded =
-                        (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED) != 0);
+        mSysUiState.addCallback(sysUiFlags -> {
+            mSysUiStateBubblesManageMenuExpanded =
+                    (sysUiFlags
+                            & QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0;
+            mSysUiStateBubblesExpanded =
+                    (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED) != 0;
+        });
 
         // TODO: Fix
         mPositioner = new TestableBubblePositioner(mContext, mWindowManager);
@@ -331,7 +338,8 @@
                 mPositioner,
                 mock(DisplayController.class),
                 syncExecutor,
-                mock(Handler.class));
+                mock(Handler.class),
+                mock(SyncTransactionQueue.class));
         mBubbleController.setExpandListener(mBubbleExpandListener);
         spyOn(mBubbleController);
 
@@ -369,8 +377,7 @@
     public void testAddBubble() {
         mBubbleController.updateBubble(mBubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
-
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -378,7 +385,7 @@
         assertFalse(mBubbleController.hasBubbles());
         mBubbleController.updateBubble(mBubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -393,7 +400,7 @@
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
         verify(mNotificationEntryManager, times(2)).updateNotifications(anyString());
 
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -439,7 +446,7 @@
                 mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
 
         mBubbleController.removeBubble(
-                mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
+                mRow.getKey(), DISMISS_NOTIF_CANCEL);
         verify(mNotificationEntryManager, times(1)).performRemoveNotification(
                 eq(mRow.getSbn()), any(), anyInt());
         assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
@@ -456,7 +463,7 @@
         verify(mNotificationEntryManager, never()).performRemoveNotification(
                 eq(mRow.getSbn()), any(), anyInt());
         assertFalse(mBubbleController.hasBubbles());
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
         assertTrue(mRow.isBubble());
     }
 
@@ -475,7 +482,7 @@
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey()));
 
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -495,8 +502,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
-
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Make sure the notif is suppressed
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -505,8 +511,7 @@
         mBubbleController.collapseStack();
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
         assertStackCollapsed();
-
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -529,8 +534,7 @@
         assertStackExpanded();
         verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
                 true, mRow2.getKey());
-
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Last added is the one that is expanded
         assertEquals(mRow2.getKey(), mBubbleData.getSelectedBubble().getKey());
@@ -554,8 +558,7 @@
         // Collapse
         mBubbleController.collapseStack();
         assertStackCollapsed();
-
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -575,8 +578,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
-
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Notif is suppressed after expansion
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -601,8 +603,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
-
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Notif is suppressed after expansion
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -631,7 +632,7 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
 
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getKey());
@@ -663,7 +664,7 @@
         assertEquals(mBubbleData.getSelectedBubble().getKey(), BubbleOverflow.KEY);
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, BubbleOverflow.KEY);
         assertTrue(mBubbleController.hasBubbles());
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -676,7 +677,7 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
 
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
 
@@ -691,7 +692,7 @@
         // We should be collapsed
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
         assertFalse(mBubbleController.hasBubbles());
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -708,8 +709,7 @@
         verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */,
                 mRow.getKey());
         assertStackCollapsed();
-
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -725,8 +725,7 @@
         verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */,
                 mRow.getKey());
         assertStackExpanded();
-
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -743,8 +742,7 @@
         // Dot + flyout is hidden because notif is suppressed
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
-
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -766,8 +764,7 @@
         // Dot + flyout is hidden because notif is suppressed
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
-
-        assertFalse(mSysUiStateBubblesExpanded);
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -781,7 +778,7 @@
 
         mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
 
-        assertTrue(mSysUiStateBubblesExpanded);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -1144,6 +1141,28 @@
         // Verify these are in the overflow
         assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntryUser11.getKey())).isNotNull();
         assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2User11.getKey())).isNotNull();
+
+        // Would have loaded bubbles twice because of user switch
+        verify(mDataRepository, times(2)).loadBubbles(anyInt(), any());
+    }
+
+    /**
+     * Verifies we only load the overflow data once.
+     */
+    @Test
+    public void testOverflowLoadedOnce() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleController.updateBubble(mBubbleEntry2);
+        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbles().isEmpty()).isFalse();
+
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleController.updateBubble(mBubbleEntry2);
+        mBubbleController.removeBubble(mBubbleEntry.getKey(), DISMISS_NOTIF_CANCEL);
+        mBubbleController.removeBubble(mBubbleEntry2.getKey(), DISMISS_NOTIF_CANCEL);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+
+        verify(mDataRepository, times(1)).loadBubbles(anyInt(), any());
     }
 
 
@@ -1181,6 +1200,63 @@
         assertNotNull(info);
     }
 
+    @Test
+    public void testShowManageMenuChangesSysuiState() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        // Expand the stack
+        BubbleStackView stackView = mBubbleController.getStackView();
+        mBubbleData.setExpanded(true);
+        assertStackExpanded();
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+
+        // Show the menu
+        stackView.showManageMenu(true);
+        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+    }
+
+    @Test
+    public void testHideManageMenuChangesSysuiState() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        // Expand the stack
+        BubbleStackView stackView = mBubbleController.getStackView();
+        mBubbleData.setExpanded(true);
+        assertStackExpanded();
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+
+        // Show the menu
+        stackView.showManageMenu(true);
+        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+
+        // Hide the menu
+        stackView.showManageMenu(false);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+    }
+
+    @Test
+    public void testCollapseBubbleManageMenuChangesSysuiState() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        // Expand the stack
+        BubbleStackView stackView = mBubbleController.getStackView();
+        mBubbleData.setExpanded(true);
+        assertStackExpanded();
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+
+        // Show the menu
+        stackView.showManageMenu(true);
+        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+
+        // Collapse the stack
+        mBubbleData.setExpanded(false);
+
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+    }
+
     /** Creates a bubble using the userId and package. */
     private Bubble createBubble(int userId, String pkg) {
         final UserHandle userHandle = new UserHandle(userId);
@@ -1278,4 +1354,12 @@
         assertFalse(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                 entry.getKey(), entry.getGroupKey()));
     }
+
+    /**
+     * Asserts that the system ui states associated to bubbles are in the correct state.
+     */
+    private void assertSysuiStates(boolean stackExpanded, boolean manageMenuExpanded) {
+        assertThat(mSysUiStateBubblesExpanded).isEqualTo(stackExpanded);
+        assertThat(mSysUiStateBubblesManageMenuExpanded).isEqualTo(manageMenuExpanded);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
index e4c7800..05c4822 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.wmshell;
 
 import static android.app.Notification.FLAG_BUBBLE;
+import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -47,6 +48,7 @@
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Handler;
 import android.os.PowerManager;
+import android.os.UserHandle;
 import android.service.dreams.IDreamManager;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.ZenModeConfig;
@@ -62,10 +64,11 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.FeatureFlags;
+import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -100,6 +103,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 
 import org.junit.Before;
@@ -157,7 +161,9 @@
     @Mock
     private AuthController mAuthController;
 
-    private SysUiState mSysUiState = new SysUiState();
+    private SysUiState mSysUiState;
+    private boolean mSysUiStateBubblesExpanded;
+    private boolean mSysUiStateBubblesManageMenuExpanded;
 
     @Captor
     private ArgumentCaptor<NotifCollectionListener> mNotifListenerCaptor;
@@ -171,6 +177,10 @@
     private ExpandableNotificationRow mNonBubbleNotifRow;
     private BubbleEntry mBubbleEntry;
     private BubbleEntry mBubbleEntry2;
+
+    private BubbleEntry mBubbleEntryUser11;
+    private BubbleEntry mBubbleEntry2User11;
+
     @Mock
     private Bubbles.BubbleExpandListener mBubbleExpandListener;
     @Mock
@@ -240,9 +250,25 @@
         mBubbleEntry = BubblesManager.notifToBubbleEntry(mRow);
         mBubbleEntry2 = BubblesManager.notifToBubbleEntry(mRow2);
 
+        UserHandle handle = mock(UserHandle.class);
+        when(handle.getIdentifier()).thenReturn(11);
+        mBubbleEntryUser11 = BubblesManager.notifToBubbleEntry(
+                mNotificationTestHelper.createBubble(handle));
+        mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry(
+                mNotificationTestHelper.createBubble(handle));
+
         mZenModeConfig.suppressedVisualEffects = 0;
         when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
 
+        mSysUiState = new SysUiState();
+        mSysUiState.addCallback(sysUiFlags -> {
+            mSysUiStateBubblesManageMenuExpanded =
+                    (sysUiFlags
+                            & QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0;
+            mSysUiStateBubblesExpanded =
+                    (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED) != 0;
+        });
+
         mPositioner = new TestableBubblePositioner(mContext, mWindowManager);
         mPositioner.setMaxBubbles(5);
         mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor);
@@ -275,7 +301,8 @@
                 mPositioner,
                 mock(DisplayController.class),
                 syncExecutor,
-                mock(Handler.class));
+                mock(Handler.class),
+                mock(SyncTransactionQueue.class));
         mBubbleController.setExpandListener(mBubbleExpandListener);
         spyOn(mBubbleController);
 
@@ -310,6 +337,7 @@
     public void testAddBubble() {
         mBubbleController.updateBubble(mBubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -317,6 +345,7 @@
         assertFalse(mBubbleController.hasBubbles());
         mBubbleController.updateBubble(mBubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -330,6 +359,8 @@
                 mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
         verify(mNotifCallback, times(2)).invalidateNotifications(anyString());
+
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -392,6 +423,8 @@
         verify(mNotifCallback, times(3)).invalidateNotifications(anyString());
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey()));
+
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -410,6 +443,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Make sure the notif is suppressed
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -418,6 +452,7 @@
         mBubbleController.collapseStack();
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
         assertStackCollapsed();
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -440,6 +475,7 @@
         assertStackExpanded();
         verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
                 true, mRow2.getKey());
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Last added is the one that is expanded
         assertEquals(mRow2.getKey(), mBubbleData.getSelectedBubble().getKey());
@@ -464,6 +500,7 @@
         // Collapse
         mBubbleController.collapseStack();
         assertStackCollapsed();
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -483,6 +520,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Notif is suppressed after expansion
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -507,6 +545,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
 
         // Notif is suppressed after expansion
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -535,6 +574,8 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
 
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getKey());
 
@@ -565,6 +606,7 @@
         assertEquals(mBubbleData.getSelectedBubble().getKey(), BubbleOverflow.KEY);
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, BubbleOverflow.KEY);
         assertTrue(mBubbleController.hasBubbles());
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -577,6 +619,7 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
 
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
 
@@ -591,6 +634,7 @@
         // We should be collapsed
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
         assertFalse(mBubbleController.hasBubbles());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
 
@@ -608,6 +652,7 @@
         verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */,
                 mRow.getKey());
         assertStackCollapsed();
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -623,6 +668,7 @@
         verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */,
                 mRow.getKey());
         assertStackExpanded();
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -639,6 +685,7 @@
         // Dot + flyout is hidden because notif is suppressed
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -660,6 +707,7 @@
         // Dot + flyout is hidden because notif is suppressed
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
@@ -906,6 +954,122 @@
                 groupSummary.getEntry().getSbn().getGroupKey()));
     }
 
+
+    /**
+     * Verifies that when the user changes, the bubbles in the overflow list is cleared. Doesn't
+     * test the loading from the repository which would be a nice thing to add.
+     */
+    @Test
+    public void testOnUserChanged_overflowState() {
+        int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier();
+        int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier();
+
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleController.updateBubble(mBubbleEntry2);
+        assertTrue(mBubbleController.hasBubbles());
+        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
+
+        // Verify these are in the overflow
+        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey())).isNotNull();
+        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2.getKey())).isNotNull();
+
+        // Switch users
+        mBubbleController.onUserChanged(secondUserId);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+
+        // Give this user some bubbles
+        mBubbleController.updateBubble(mBubbleEntryUser11);
+        mBubbleController.updateBubble(mBubbleEntry2User11);
+        assertTrue(mBubbleController.hasBubbles());
+        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
+
+        // Verify these are in the overflow
+        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntryUser11.getKey())).isNotNull();
+        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2User11.getKey())).isNotNull();
+
+        // Would have loaded bubbles twice because of user switch
+        verify(mDataRepository, times(2)).loadBubbles(anyInt(), any());
+    }
+
+    /**
+     * Verifies we only load the overflow data once.
+     */
+    @Test
+    public void testOverflowLoadedOnce() {
+        when(mNotificationEntryManager.getPendingOrActiveNotif(mRow.getKey()))
+                .thenReturn(mRow);
+        when(mNotificationEntryManager.getPendingOrActiveNotif(mRow2.getKey()))
+                .thenReturn(mRow2);
+
+        mEntryListener.onEntryAdded(mRow);
+        mEntryListener.onEntryAdded(mRow2);
+        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+
+        mEntryListener.onEntryRemoved(mRow, REASON_APP_CANCEL);
+        mEntryListener.onEntryRemoved(mRow2, REASON_APP_CANCEL);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+
+        verify(mDataRepository, times(1)).loadBubbles(anyInt(), any());
+    }
+
+    @Test
+    public void testShowManageMenuChangesSysuiState() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        // Expand the stack
+        BubbleStackView stackView = mBubbleController.getStackView();
+        mBubbleData.setExpanded(true);
+        assertStackExpanded();
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+
+        // Show the menu
+        stackView.showManageMenu(true);
+        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+    }
+
+    @Test
+    public void testHideManageMenuChangesSysuiState() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        // Expand the stack
+        BubbleStackView stackView = mBubbleController.getStackView();
+        mBubbleData.setExpanded(true);
+        assertStackExpanded();
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+
+        // Show the menu
+        stackView.showManageMenu(true);
+        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+
+        // Hide the menu
+        stackView.showManageMenu(false);
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+    }
+
+    @Test
+    public void testCollapseBubbleManageMenuChangesSysuiState() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        // Expand the stack
+        BubbleStackView stackView = mBubbleController.getStackView();
+        mBubbleData.setExpanded(true);
+        assertStackExpanded();
+        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+
+        // Show the menu
+        stackView.showManageMenu(true);
+        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+
+        // Collapse the stack
+        mBubbleData.setExpanded(false);
+
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+    }
+
     /**
      * Sets the bubble metadata flags for this entry. These flags are normally set by
      * NotificationManagerService when the notification is sent, however, these tests do not
@@ -960,4 +1124,12 @@
         assertFalse(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                 entry.getKey(), entry.getGroupKey()));
     }
+
+    /**
+     * Asserts that the system ui states associated to bubbles are in the correct state.
+     */
+    private void assertSysuiStates(boolean stackExpanded, boolean manageMenuExpanded) {
+        assertThat(mSysUiStateBubblesExpanded).isEqualTo(stackExpanded);
+        assertThat(mSysUiStateBubblesManageMenuExpanded).isEqualTo(manageMenuExpanded);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index cd5aa9a..7b77cb0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -32,6 +32,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 
 /**
@@ -54,11 +55,12 @@
             BubblePositioner positioner,
             DisplayController displayController,
             ShellExecutor shellMainExecutor,
-            Handler shellMainHandler) {
+            Handler shellMainHandler,
+            SyncTransactionQueue syncQueue) {
         super(context, data, Runnable::run, floatingContentCoordinator, dataRepository,
                 statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
                 bubbleLogger, taskStackListener, shellTaskOrganizer, positioner, displayController,
-                shellMainExecutor, shellMainHandler);
+                shellMainExecutor, shellMainHandler, syncQueue);
         setInflateSynchronously(true);
         initialize();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 5691660..8480702 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -41,6 +41,7 @@
 import com.android.wm.shell.onehanded.OneHandedEventCallback;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
 import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.splitscreen.SplitScreen;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -69,6 +70,7 @@
     @Mock SysUiState mSysUiState;
     @Mock Pip mPip;
     @Mock LegacySplitScreen mLegacySplitScreen;
+    @Mock SplitScreen mSplitScreen;
     @Mock OneHanded mOneHanded;
     @Mock HideDisplayCutout mHideDisplayCutout;
     @Mock WakefulnessLifecycle mWakefulnessLifecycle;
@@ -81,7 +83,7 @@
         MockitoAnnotations.initMocks(this);
 
         mWMShell = new WMShell(mContext, Optional.of(mPip), Optional.of(mLegacySplitScreen),
-                Optional.of(mOneHanded), Optional.of(mHideDisplayCutout),
+                Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mHideDisplayCutout),
                 Optional.of(mShellCommandHandler), mCommandQueue, mConfigurationController,
                 mKeyguardUpdateMonitor, mNavigationModeController,
                 mScreenLifecycle, mSysUiState, mProtoTracer, mWakefulnessLifecycle,
@@ -96,8 +98,15 @@
     }
 
     @Test
+    public void initLegacySplitScreen_registersCallbacks() {
+        mWMShell.initLegacySplitScreen(mLegacySplitScreen);
+
+        verify(mKeyguardUpdateMonitor).registerCallback(any(KeyguardUpdateMonitorCallback.class));
+    }
+
+    @Test
     public void initSplitScreen_registersCallbacks() {
-        mWMShell.initSplitScreen(mLegacySplitScreen);
+        mWMShell.initSplitScreen(mSplitScreen);
 
         verify(mKeyguardUpdateMonitor).registerCallback(any(KeyguardUpdateMonitorCallback.class));
     }
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index b0893cc..ed37d7e 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.graphics.Camera;
 import android.graphics.GraphicBuffer;
 import android.graphics.Rect;
 import android.hardware.camera2.CameraAccessException;
@@ -135,6 +136,7 @@
 
     private HashMap<String, CameraCharacteristics> mCharacteristicsHashMap = new HashMap<>();
     private HashMap<String, Long> mMetadataVendorIdMap = new HashMap<>();
+    private CameraManager mCameraManager;
 
     private static boolean checkForAdvancedAPI() {
         if (EXTENSIONS_PRESENT && EXTENSIONS_VERSION.startsWith(ADVANCED_VERSION_PREFIX)) {
@@ -460,12 +462,12 @@
         // This will setup the camera vendor tag descriptor in the service process
         // along with all camera characteristics.
         try {
-            CameraManager manager = getSystemService(CameraManager.class);
+            mCameraManager = getSystemService(CameraManager.class);
 
-            String [] cameraIds = manager.getCameraIdListNoLazy();
+            String [] cameraIds = mCameraManager.getCameraIdListNoLazy();
             if (cameraIds != null) {
                 for (String cameraId : cameraIds) {
-                    CameraCharacteristics chars = manager.getCameraCharacteristics(cameraId);
+                    CameraCharacteristics chars = mCameraManager.getCameraCharacteristics(cameraId);
                     mCharacteristicsHashMap.put(cameraId, chars);
                     Object thisClass = CameraCharacteristics.Key.class;
                     Class<CameraCharacteristics.Key<?>> keyClass =
@@ -1174,8 +1176,9 @@
         @Override
         public void onInit(String cameraId, CameraMetadataNative cameraCharacteristics) {
             mCameraId = cameraId;
-            mPreviewExtender.onInit(cameraId, new CameraCharacteristics(cameraCharacteristics),
-                    CameraExtensionsProxyService.this);
+            CameraCharacteristics chars = new CameraCharacteristics(cameraCharacteristics);
+            mCameraManager.registerDeviceStateListener(chars);
+            mPreviewExtender.onInit(cameraId, chars, CameraExtensionsProxyService.this);
         }
 
         @Override
@@ -1200,13 +1203,16 @@
 
         @Override
         public void init(String cameraId, CameraMetadataNative chars) {
-            mPreviewExtender.init(cameraId, new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            mPreviewExtender.init(cameraId, c);
         }
 
         @Override
         public boolean isExtensionAvailable(String cameraId, CameraMetadataNative chars) {
-            return mPreviewExtender.isExtensionAvailable(cameraId,
-                    new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            return mPreviewExtender.isExtensionAvailable(cameraId, c);
         }
 
         @Override
@@ -1283,8 +1289,9 @@
 
         @Override
         public void onInit(String cameraId, CameraMetadataNative cameraCharacteristics) {
-            mImageExtender.onInit(cameraId, new CameraCharacteristics(cameraCharacteristics),
-                    CameraExtensionsProxyService.this);
+            CameraCharacteristics chars = new CameraCharacteristics(cameraCharacteristics);
+            mCameraManager.registerDeviceStateListener(chars);
+            mImageExtender.onInit(cameraId, chars, CameraExtensionsProxyService.this);
             mCameraId = cameraId;
         }
 
@@ -1310,13 +1317,16 @@
 
         @Override
         public void init(String cameraId, CameraMetadataNative chars) {
-            mImageExtender.init(cameraId, new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            mImageExtender.init(cameraId, c);
         }
 
         @Override
         public boolean isExtensionAvailable(String cameraId, CameraMetadataNative chars) {
-            return mImageExtender.isExtensionAvailable(cameraId,
-                    new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            return mImageExtender.isExtensionAvailable(cameraId, c);
         }
 
         @Override
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index e9c9899..ee80dae 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -21,8 +21,13 @@
 import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_STATUS;
 import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP;
 import static android.accessibilityservice.AccessibilityServiceInfo.DEFAULT;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CONNECTION;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 import static android.view.accessibility.AccessibilityInteractionClient.CALL_STACK;
+import static android.view.accessibility.AccessibilityInteractionClient.IGNORE_CALL_STACK;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
@@ -31,6 +36,7 @@
 import android.accessibilityservice.AccessibilityGestureEvent;
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.accessibilityservice.IAccessibilityServiceConnection;
 import android.annotation.NonNull;
@@ -103,10 +109,9 @@
         FingerprintGestureDispatcher.FingerprintGestureClient {
     private static final boolean DEBUG = false;
     private static final String LOG_TAG = "AbstractAccessibilityServiceConnection";
-    private static final String TRACE_A11Y_SERVICE_CONNECTION =
-            LOG_TAG + ".IAccessibilityServiceConnection";
-    private static final String TRACE_A11Y_SERVICE_CLIENT =
-            LOG_TAG + ".IAccessibilityServiceClient";
+    private static final String TRACE_SVC_CONN = LOG_TAG + ".IAccessibilityServiceConnection";
+    private static final String TRACE_SVC_CLIENT = LOG_TAG + ".IAccessibilityServiceClient";
+    private static final String TRACE_WM = "WindowManagerInternal";
     private static final int WAIT_WINDOWS_TIMEOUT_MILLIS = 5000;
 
     protected static final String TAKE_SCREENSHOT = "takeScreenshot";
@@ -298,9 +303,8 @@
             return false;
         }
         try {
-            if (mTrace.isA11yTracingEnabled()) {
-                mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onKeyEvent",
-                        keyEvent + ", " + sequenceNumber);
+            if (svcClientTracingEnabled()) {
+                logTraceSvcClient("onKeyEvent", keyEvent + ", " + sequenceNumber);
             }
             mServiceInterface.onKeyEvent(keyEvent, sequenceNumber);
         } catch (RemoteException e) {
@@ -365,17 +369,16 @@
 
     @Override
     public void setOnKeyEventResult(boolean handled, int sequence) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setOnKeyEventResult",
-                    "handled=" + handled + ";sequence=" + sequence);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setOnKeyEventResult", "handled=" + handled + ";sequence=" + sequence);
         }
         mSystemSupport.getKeyEventDispatcher().setOnKeyEventResult(this, handled, sequence);
     }
 
     @Override
     public AccessibilityServiceInfo getServiceInfo() {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getServiceInfo");
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getServiceInfo", "");
         }
         synchronized (mLock) {
             return mAccessibilityServiceInfo;
@@ -393,8 +396,8 @@
 
     @Override
     public void setServiceInfo(AccessibilityServiceInfo info) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setServiceInfo", "info=" + info);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setServiceInfo", "info=" + info);
         }
         final long identity = Binder.clearCallingIdentity();
         try {
@@ -421,8 +424,8 @@
     @Nullable
     @Override
     public AccessibilityWindowInfo.WindowListSparseArray getWindows() {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getWindows");
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getWindows", "");
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -458,8 +461,8 @@
 
     @Override
     public AccessibilityWindowInfo getWindow(int windowId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getWindow", "windowId=" + windowId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getWindow", "windowId=" + windowId);
         }
         synchronized (mLock) {
             int displayId = Display.INVALID_DISPLAY;
@@ -496,8 +499,8 @@
             long accessibilityNodeId, String viewIdResName, int interactionId,
             IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
             throws RemoteException {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".findAccessibilityNodeInfosByViewId",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("findAccessibilityNodeInfosByViewId",
                     "accessibilityWindowId=" + accessibilityWindowId + ";accessibilityNodeId="
                     + accessibilityNodeId + ";viewIdResName=" + viewIdResName + ";interactionId="
                     + interactionId + ";callback=" + callback + ";interrogatingTid="
@@ -539,6 +542,12 @@
         callback = replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
                 interrogatingPid, interrogatingTid);
         final long identityToken = Binder.clearCallingIdentity();
+        if (intConnTracingEnabled()) {
+            logTraceIntConn("findAccessibilityNodeInfosByViewId",
+                    accessibilityNodeId + ";" + viewIdResName + ";" + partialInteractiveRegion + ";"
+                    + interactionId + ";" + callback + ";" + mFetchFlags + ";" + interrogatingPid
+                    + ";" + interrogatingTid + ";" + spec);
+        }
         try {
             connection.getRemote().findAccessibilityNodeInfosByViewId(accessibilityNodeId,
                     viewIdResName, partialInteractiveRegion, interactionId, callback, mFetchFlags,
@@ -564,8 +573,8 @@
             long accessibilityNodeId, String text, int interactionId,
             IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
             throws RemoteException {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".findAccessibilityNodeInfosByText",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("findAccessibilityNodeInfosByText",
                     "accessibilityWindowId=" + accessibilityWindowId + ";accessibilityNodeId="
                     + accessibilityNodeId + ";text=" + text + ";interactionId=" + interactionId
                     + ";callback=" + callback + ";interrogatingTid=" + interrogatingTid);
@@ -606,6 +615,12 @@
         callback = replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
                 interrogatingPid, interrogatingTid);
         final long identityToken = Binder.clearCallingIdentity();
+        if (intConnTracingEnabled()) {
+            logTraceIntConn("findAccessibilityNodeInfosByText",
+                    accessibilityNodeId + ";" + text + ";" + partialInteractiveRegion + ";"
+                    + interactionId + ";" + callback + ";" + mFetchFlags + ";" + interrogatingPid
+                    + ";" + interrogatingTid + ";" + spec);
+        }
         try {
             connection.getRemote().findAccessibilityNodeInfosByText(accessibilityNodeId,
                     text, partialInteractiveRegion, interactionId, callback, mFetchFlags,
@@ -631,13 +646,12 @@
             int accessibilityWindowId, long accessibilityNodeId, int interactionId,
             IAccessibilityInteractionConnectionCallback callback, int flags,
             long interrogatingTid, Bundle arguments) throws RemoteException {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(
-                    TRACE_A11Y_SERVICE_CONNECTION + ".findAccessibilityNodeInfoByAccessibilityId",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("findAccessibilityNodeInfoByAccessibilityId",
                     "accessibilityWindowId=" + accessibilityWindowId + ";accessibilityNodeId="
-                            + accessibilityNodeId + ";interactionId=" + interactionId + ";callback="
-                            + callback + ";flags=" + flags + ";interrogatingTid=" + interrogatingTid
-                            + ";arguments=" + arguments);
+                    + accessibilityNodeId + ";interactionId=" + interactionId + ";callback="
+                    + callback + ";flags=" + flags + ";interrogatingTid=" + interrogatingTid
+                    + ";arguments=" + arguments);
         }
         final int resolvedWindowId;
         RemoteAccessibilityConnection connection;
@@ -675,6 +689,12 @@
         callback = replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
                 interrogatingPid, interrogatingTid);
         final long identityToken = Binder.clearCallingIdentity();
+        if (intConnTracingEnabled()) {
+            logTraceIntConn("findAccessibilityNodeInfoByAccessibilityId",
+                    accessibilityNodeId + ";" + partialInteractiveRegion + ";" + interactionId + ";"
+                    + callback + ";" + (mFetchFlags | flags) + ";" + interrogatingPid + ";"
+                    + interrogatingTid + ";" + spec + ";" + arguments);
+        }
         try {
             connection.getRemote().findAccessibilityNodeInfoByAccessibilityId(
                     accessibilityNodeId, partialInteractiveRegion, interactionId, callback,
@@ -700,12 +720,12 @@
             int focusType, int interactionId,
             IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
             throws RemoteException {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".findFocus",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("findFocus",
                     "accessibilityWindowId=" + accessibilityWindowId + ";accessibilityNodeId="
-                            + accessibilityNodeId + ";focusType=" + focusType + ";interactionId="
-                            + interactionId + ";callback=" + callback + ";interrogatingTid="
-                            + interrogatingTid);
+                    + accessibilityNodeId + ";focusType=" + focusType + ";interactionId="
+                    + interactionId + ";callback=" + callback + ";interrogatingTid="
+                    + interrogatingTid);
         }
         final int resolvedWindowId;
         RemoteAccessibilityConnection connection;
@@ -743,6 +763,12 @@
         callback = replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
                 interrogatingPid, interrogatingTid);
         final long identityToken = Binder.clearCallingIdentity();
+        if (intConnTracingEnabled()) {
+            logTraceIntConn("findFocus",
+                    accessibilityNodeId + ";" + focusType + ";" + partialInteractiveRegion + ";"
+                    + interactionId + ";" + callback + ";" + mFetchFlags + ";" + interrogatingPid
+                    + ";" + interrogatingTid + ";" + spec);
+        }
         try {
             connection.getRemote().findFocus(accessibilityNodeId, focusType,
                     partialInteractiveRegion, interactionId, callback, mFetchFlags,
@@ -768,12 +794,12 @@
             int direction, int interactionId,
             IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
             throws RemoteException {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".focusSearch",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("focusSearch",
                     "accessibilityWindowId=" + accessibilityWindowId + ";accessibilityNodeId="
-                            + accessibilityNodeId + ";direction=" + direction + ";interactionId="
-                            + interactionId + ";callback=" + callback + ";interrogatingTid="
-                            + interrogatingTid);
+                    + accessibilityNodeId + ";direction=" + direction + ";interactionId="
+                    + interactionId + ";callback=" + callback + ";interrogatingTid="
+                    + interrogatingTid);
         }
         final int resolvedWindowId;
         RemoteAccessibilityConnection connection;
@@ -810,6 +836,12 @@
         callback = replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
                 interrogatingPid, interrogatingTid);
         final long identityToken = Binder.clearCallingIdentity();
+        if (intConnTracingEnabled()) {
+            logTraceIntConn("focusSearch",
+                    accessibilityNodeId + ";" + direction + ";" + partialInteractiveRegion
+                    + ";" + interactionId + ";" + callback + ";" + mFetchFlags + ";"
+                    + interrogatingPid + ";" + interrogatingTid + ";" + spec);
+        }
         try {
             connection.getRemote().focusSearch(accessibilityNodeId, direction,
                     partialInteractiveRegion, interactionId, callback, mFetchFlags,
@@ -832,17 +864,17 @@
 
     @Override
     public void sendGesture(int sequence, ParceledListSlice gestureSteps) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".sendGesture",
-                    "sequence=" + sequence + ";gestureSteps=" + gestureSteps);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn(
+                    "sendGesture", "sequence=" + sequence + ";gestureSteps=" + gestureSteps);
         }
     }
 
     @Override
     public void dispatchGesture(int sequence, ParceledListSlice gestureSteps, int displayId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".dispatchGesture", "sequence="
-                    + sequence + ";gestureSteps=" + gestureSteps + ";displayId=" + displayId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("dispatchGesture", "sequence=" + sequence + ";gestureSteps="
+                    + gestureSteps + ";displayId=" + displayId);
         }
     }
 
@@ -851,12 +883,12 @@
             long accessibilityNodeId, int action, Bundle arguments, int interactionId,
             IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
             throws RemoteException {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".performAccessibilityAction",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("performAccessibilityAction",
                     "accessibilityWindowId=" + accessibilityWindowId + ";accessibilityNodeId="
-                            + accessibilityNodeId + ";action=" + action + ";arguments=" + arguments
-                            + ";interactionId=" + interactionId + ";callback=" + callback
-                            + ";interrogatingTid=" + interrogatingTid);
+                    + accessibilityNodeId + ";action=" + action + ";arguments=" + arguments
+                    + ";interactionId=" + interactionId + ";callback=" + callback
+                    + ";interrogatingTid=" + interrogatingTid);
         }
         final int resolvedWindowId;
         synchronized (mLock) {
@@ -879,9 +911,8 @@
 
     @Override
     public boolean performGlobalAction(int action) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".performGlobalAction",
-                    "action=" + action);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("performGlobalAction", "action=" + action);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -893,8 +924,8 @@
 
     @Override
     public @NonNull List<AccessibilityNodeInfo.AccessibilityAction> getSystemActions() {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getSystemActions");
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getSystemActions", "");
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -906,9 +937,8 @@
 
     @Override
     public boolean isFingerprintGestureDetectionAvailable() {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(
-                    TRACE_A11Y_SERVICE_CONNECTION + ".isFingerprintGestureDetectionAvailable");
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("isFingerprintGestureDetectionAvailable", "");
         }
         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
             return false;
@@ -923,9 +953,8 @@
 
     @Override
     public float getMagnificationScale(int displayId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getMagnificationScale",
-                    "displayId=" + displayId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getMagnificationScale", "displayId=" + displayId);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -942,9 +971,8 @@
 
     @Override
     public Region getMagnificationRegion(int displayId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getMagnificationRegion",
-                    "displayId=" + displayId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getMagnificationRegion", "displayId=" + displayId);
         }
         synchronized (mLock) {
             final Region region = Region.obtain();
@@ -970,9 +998,8 @@
 
     @Override
     public float getMagnificationCenterX(int displayId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getMagnificationCenterX",
-                    "displayId=" + displayId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getMagnificationCenterX", "displayId=" + displayId);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -996,9 +1023,8 @@
 
     @Override
     public float getMagnificationCenterY(int displayId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getMagnificationCenterY",
-                    "displayId=" + displayId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getMagnificationCenterY", "displayId=" + displayId);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -1032,9 +1058,8 @@
 
     @Override
     public boolean resetMagnification(int displayId, boolean animate) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".resetMagnification",
-                    "displayId=" + displayId + ";animate=" + animate);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("resetMagnification", "displayId=" + displayId + ";animate=" + animate);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -1058,10 +1083,10 @@
     @Override
     public boolean setMagnificationScaleAndCenter(int displayId, float scale, float centerX,
             float centerY, boolean animate) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setMagnificationScaleAndCenter",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setMagnificationScaleAndCenter",
                     "displayId=" + displayId + ";scale=" + scale + ";centerX=" + centerX
-                            + ";centerY=" + centerY + ";animate=" + animate);
+                    + ";centerY=" + centerY + ";animate=" + animate);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -1087,8 +1112,8 @@
 
     @Override
     public void setMagnificationCallbackEnabled(int displayId, boolean enabled) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setMagnificationCallbackEnabled",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setMagnificationCallbackEnabled",
                     "displayId=" + displayId + ";enabled=" + enabled);
         }
         mInvocationHandler.setMagnificationCallbackEnabled(displayId, enabled);
@@ -1100,18 +1125,16 @@
 
     @Override
     public void setSoftKeyboardCallbackEnabled(boolean enabled) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setSoftKeyboardCallbackEnabled",
-                    "enabled=" + enabled);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setSoftKeyboardCallbackEnabled", "enabled=" + enabled);
         }
         mInvocationHandler.setSoftKeyboardCallbackEnabled(enabled);
     }
 
     @Override
     public void takeScreenshot(int displayId, RemoteCallback callback) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".takeScreenshot",
-                    "displayId=" + displayId + ";callback=" + callback);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("takeScreenshot", "displayId=" + displayId + ";callback=" + callback);
         }
         final long currentTimestamp = SystemClock.uptimeMillis();
         if (mRequestTakeScreenshotTimestampMs != 0
@@ -1237,6 +1260,10 @@
         final long identity = Binder.clearCallingIdentity();
         try {
             final IBinder overlayWindowToken = new Binder();
+            if (wmTracingEnabled()) {
+                logTraceWM("addWindowToken",
+                        overlayWindowToken + ";TYPE_ACCESSIBILITY_OVERLAY;" + displayId + ";null");
+            }
             mWindowManagerService.addWindowToken(overlayWindowToken, TYPE_ACCESSIBILITY_OVERLAY,
                     displayId, null /* options */);
             synchronized (mLock) {
@@ -1263,6 +1290,10 @@
      */
     public void onDisplayRemoved(int displayId) {
         final long identity = Binder.clearCallingIdentity();
+        if (wmTracingEnabled()) {
+            logTraceWM(
+                    "addWindowToken", mOverlayWindowTokens.get(displayId) + ";true;" + displayId);
+        }
         try {
             mWindowManagerService.removeWindowToken(mOverlayWindowTokens.get(displayId), true,
                     displayId);
@@ -1282,9 +1313,8 @@
      */
     @Override
     public IBinder getOverlayWindowToken(int displayId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getOverlayWindowToken",
-                    "displayId=" + displayId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getOverlayWindowToken", "displayId=" + displayId);
         }
         synchronized (mLock) {
             return mOverlayWindowTokens.get(displayId);
@@ -1299,9 +1329,8 @@
      */
     @Override
     public int getWindowIdForLeashToken(@NonNull IBinder token) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getWindowIdForLeashToken",
-                    "token=" + token);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getWindowIdForLeashToken", "token=" + token);
         }
         synchronized (mLock) {
             return mA11yWindowManager.getWindowIdLocked(token);
@@ -1314,8 +1343,8 @@
             // Clear the proxy in the other process so this
             // IAccessibilityServiceConnection can be garbage collected.
             if (mServiceInterface != null) {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".init", "null, " + mId + ", null");
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("init", "null, " + mId + ", null");
                 }
                 mServiceInterface.init(null, mId, null);
             }
@@ -1465,9 +1494,8 @@
         }
 
         try {
-            if (mTrace.isA11yTracingEnabled()) {
-                mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onAccessibilityEvent",
-                        event + ";" + serviceWantsEvent);
+            if (svcClientTracingEnabled()) {
+                logTraceSvcClient("onAccessibilityEvent", event + ";" + serviceWantsEvent);
             }
             listener.onAccessibilityEvent(event, serviceWantsEvent);
             if (DEBUG) {
@@ -1522,9 +1550,9 @@
         final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
         if (listener != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onMagnificationChanged", displayId
-                            + ", " + region + ", " + scale + ", " + centerX + ", " + centerY);
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onMagnificationChanged", displayId + ", " + region + ", "
+                            + scale + ", " + centerX + ", " + centerY);
                 }
                 listener.onMagnificationChanged(displayId, region, scale, centerX, centerY);
             } catch (RemoteException re) {
@@ -1541,9 +1569,8 @@
         final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
         if (listener != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onSoftKeyboardShowModeChanged",
-                            String.valueOf(showState));
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onSoftKeyboardShowModeChanged", String.valueOf(showState));
                 }
                 listener.onSoftKeyboardShowModeChanged(showState);
             } catch (RemoteException re) {
@@ -1557,9 +1584,8 @@
         final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
         if (listener != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onAccessibilityButtonClicked",
-                            String.valueOf(displayId));
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onAccessibilityButtonClicked", String.valueOf(displayId));
                 }
                 listener.onAccessibilityButtonClicked(displayId);
             } catch (RemoteException re) {
@@ -1579,9 +1605,8 @@
         final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
         if (listener != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(
-                            TRACE_A11Y_SERVICE_CLIENT + ".onAccessibilityButtonAvailabilityChanged",
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onAccessibilityButtonAvailabilityChanged",
                             String.valueOf(available));
                 }
                 listener.onAccessibilityButtonAvailabilityChanged(available);
@@ -1597,9 +1622,8 @@
         final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
         if (listener != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onGesture",
-                            gestureInfo.toString());
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onGesture", gestureInfo.toString());
                 }
                 listener.onGesture(gestureInfo);
             } catch (RemoteException re) {
@@ -1613,8 +1637,8 @@
         final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
         if (listener != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onSystemActionsChanged");
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onSystemActionsChanged", "");
                 }
                 listener.onSystemActionsChanged();
             } catch (RemoteException re) {
@@ -1628,8 +1652,8 @@
         final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
         if (listener != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".clearAccessibilityCache");
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("clearAccessibilityCache", "");
                 }
                 listener.clearAccessibilityCache();
             } catch (RemoteException re) {
@@ -1747,6 +1771,12 @@
                 LocalServices.getService(ActivityTaskManagerInternal.class)
                         .setFocusedActivity(activityToken);
             }
+            if (intConnTracingEnabled()) {
+                logTraceIntConn("performAccessibilityAction",
+                        accessibilityNodeId + ";" + action + ";" + arguments + ";" + interactionId
+                        + ";" + callback + ";" + mFetchFlags + ";" + interrogatingPid + ";"
+                        + interrogatingTid);
+            }
             connection.getRemote().performAccessibilityAction(accessibilityNodeId, action,
                     arguments, interactionId, callback, fetchFlags, interrogatingPid,
                     interrogatingTid);
@@ -1957,8 +1987,8 @@
 
     @Override
     public void setGestureDetectionPassthroughRegion(int displayId, Region region) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setGestureDetectionPassthroughRegion",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setGestureDetectionPassthroughRegion",
                     "displayId=" + displayId + ";region=" + region);
         }
         mSystemSupport.setGestureDetectionPassthroughRegion(displayId, region);
@@ -1966,8 +1996,8 @@
 
     @Override
     public void setTouchExplorationPassthroughRegion(int displayId, Region region) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setTouchExplorationPassthroughRegion",
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setTouchExplorationPassthroughRegion",
                     "displayId=" + displayId + ";region=" + region);
         }
         mSystemSupport.setTouchExplorationPassthroughRegion(displayId, region);
@@ -1975,20 +2005,56 @@
 
     @Override
     public void setFocusAppearance(int strokeWidth, int color) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setFocusAppearance",
-                    "strokeWidth=" + strokeWidth + ";color=" + color);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setFocusAppearance", "strokeWidth=" + strokeWidth + ";color=" + color);
         }
     }
 
     @Override
-    public void logTrace(long timestamp, String where, String callingParams, int processId,
-            long threadId, int callingUid, Bundle callingStack) {
-        if (mTrace.isA11yTracingEnabled()) {
+    public void logTrace(long timestamp, String where, long loggingTypes, String callingParams,
+            int processId, long threadId, int callingUid, Bundle callingStack) {
+        if (mTrace.isA11yTracingEnabledForTypes(loggingTypes)) {
             ArrayList<StackTraceElement> list =
                     (ArrayList<StackTraceElement>) callingStack.getSerializable(CALL_STACK);
-            mTrace.logTrace(timestamp, where, callingParams, processId, threadId, callingUid,
-                    list.toArray(new StackTraceElement[list.size()]));
+            HashSet<String> ignoreList =
+                    (HashSet<String>) callingStack.getSerializable(IGNORE_CALL_STACK);
+            mTrace.logTrace(timestamp, where, loggingTypes, callingParams, processId, threadId,
+                    callingUid, list.toArray(new StackTraceElement[list.size()]), ignoreList);
         }
     }
+
+    protected boolean svcClientTracingEnabled() {
+        return mTrace.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_SERVICE_CLIENT);
+    }
+
+    protected void logTraceSvcClient(String methodName, String params) {
+        mTrace.logTrace(TRACE_SVC_CLIENT + "." + methodName,
+                    FLAGS_ACCESSIBILITY_SERVICE_CLIENT, params);
+    }
+
+    protected boolean svcConnTracingEnabled() {
+        return mTrace.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_SERVICE_CONNECTION);
+    }
+
+    protected void logTraceSvcConn(String methodName, String params) {
+        mTrace.logTrace(TRACE_SVC_CONN + "." + methodName,
+                FLAGS_ACCESSIBILITY_SERVICE_CONNECTION, params);
+    }
+
+    protected boolean intConnTracingEnabled() {
+        return mTrace.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION);
+    }
+
+    protected void logTraceIntConn(String methodName, String params) {
+        mTrace.logTrace(LOG_TAG + "." + methodName,
+                FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION, params);
+    }
+
+    protected boolean wmTracingEnabled() {
+        return mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MANAGER_INTERNAL);
+    }
+
+    protected void logTraceWM(String methodName, String params) {
+        mTrace.logTrace(TRACE_WM + "." + methodName, FLAGS_WINDOW_MANAGER_INTERNAL, params);
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 7403af7..7d2b71f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -19,7 +19,9 @@
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY;
 
+import android.accessibilityservice.AccessibilityTrace;
 import android.annotation.MainThread;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.graphics.Region;
 import android.os.PowerManager;
@@ -43,7 +45,10 @@
 import com.android.server.accessibility.magnification.WindowMagnificationPromptController;
 import com.android.server.policy.WindowManagerPolicy;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.StringJoiner;
 
 /**
  * This class is an input filter for implementing accessibility features such
@@ -171,9 +176,9 @@
 
     private int mEnabledFeatures;
 
-    private EventStreamState mMouseStreamState;
+    private final SparseArray<EventStreamState> mMouseStreamStates = new SparseArray<>(0);
 
-    private EventStreamState mTouchScreenStreamState;
+    private final SparseArray<EventStreamState> mTouchScreenStreamStates = new SparseArray<>(0);
 
     private EventStreamState mKeyboardStreamState;
 
@@ -211,10 +216,17 @@
         super.onUninstalled();
     }
 
-    void onDisplayChanged() {
+    void onDisplayAdded(@NonNull Display display) {
         if (mInstalled) {
-            disableFeatures();
-            enableFeatures();
+            resetStreamStateForDisplay(display.getDisplayId());
+            enableFeaturesForDisplay(display);
+        }
+    }
+
+    void onDisplayRemoved(int displayId) {
+        if (mInstalled) {
+            disableFeaturesForDisplay(displayId);
+            resetStreamStateForDisplay(displayId);
         }
     }
 
@@ -224,7 +236,12 @@
             Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
                     + Integer.toHexString(policyFlags));
         }
-
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(
+                AccessibilityTrace.FLAGS_INPUT_FILTER)) {
+            mAms.getTraceManager().logTrace(TAG + ".onInputEvent",
+                    AccessibilityTrace.FLAGS_INPUT_FILTER,
+                    "event=" + event + ";policyFlags=" + policyFlags);
+        }
         if (mEventHandler.size() == 0) {
             if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
             super.onInputEvent(event, policyFlags);
@@ -237,16 +254,17 @@
             return;
         }
 
-        int eventSource = event.getSource();
+        final int eventSource = event.getSource();
+        final int displayId = event.getDisplayId();
         if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
             state.reset();
-            clearEventsForAllEventHandlers(eventSource);
+            clearEventStreamHandler(displayId, eventSource);
             super.onInputEvent(event, policyFlags);
             return;
         }
 
         if (state.updateInputSource(event.getSource())) {
-            clearEventsForAllEventHandlers(eventSource);
+            clearEventStreamHandler(displayId, eventSource);
         }
 
         if (!state.inputSourceValid()) {
@@ -275,35 +293,39 @@
      */
     private EventStreamState getEventStreamState(InputEvent event) {
         if (event instanceof MotionEvent) {
-          if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
-              if (mTouchScreenStreamState == null) {
-                  mTouchScreenStreamState = new TouchScreenEventStreamState();
-              }
-              return mTouchScreenStreamState;
-          }
-          if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
-              if (mMouseStreamState == null) {
-                  mMouseStreamState = new MouseEventStreamState();
-              }
-              return mMouseStreamState;
-          }
+            final int displayId = event.getDisplayId();
+
+            if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
+                EventStreamState touchScreenStreamState = mTouchScreenStreamStates.get(displayId);
+                if (touchScreenStreamState == null) {
+                    touchScreenStreamState = new TouchScreenEventStreamState();
+                    mTouchScreenStreamStates.put(displayId, touchScreenStreamState);
+                }
+                return touchScreenStreamState;
+            }
+            if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+                EventStreamState mouseStreamState = mMouseStreamStates.get(displayId);
+                if (mouseStreamState == null) {
+                    mouseStreamState = new MouseEventStreamState();
+                    mMouseStreamStates.put(displayId, mouseStreamState);
+                }
+                return mouseStreamState;
+            }
         } else if (event instanceof KeyEvent) {
-          if (event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
-              if (mKeyboardStreamState == null) {
-                  mKeyboardStreamState = new KeyboardEventStreamState();
-              }
-              return mKeyboardStreamState;
-          }
+            if (event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
+                if (mKeyboardStreamState == null) {
+                    mKeyboardStreamState = new KeyboardEventStreamState();
+                }
+                return mKeyboardStreamState;
+            }
         }
         return null;
     }
 
-    private void clearEventsForAllEventHandlers(int eventSource) {
-        for (int i = 0; i < mEventHandler.size(); i++) {
-            final EventStreamTransformation eventHandler = mEventHandler.valueAt(i);
-            if (eventHandler != null) {
-                eventHandler.clearEvents(eventSource);
-            }
+    private void clearEventStreamHandler(int displayId, int eventSource) {
+        final EventStreamTransformation eventHandler = mEventHandler.get(displayId);
+        if (eventHandler != null) {
+            eventHandler.clearEvents(eventSource);
         }
     }
 
@@ -419,55 +441,69 @@
     private void enableFeatures() {
         if (DEBUG) Slog.i(TAG, "enableFeatures()");
 
-        resetStreamState();
+        resetAllStreamState();
 
         final ArrayList<Display> displaysList = mAms.getValidDisplayList();
 
-        if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
-            mAutoclickController = new AutoclickController(mContext, mUserId);
-            addFirstEventHandlerForAllDisplays(displaysList, mAutoclickController);
-        }
-
         for (int i = displaysList.size() - 1; i >= 0; i--) {
-            final int displayId = displaysList.get(i).getDisplayId();
-            final Context displayContext = mContext.createDisplayContext(displaysList.get(i));
+            enableFeaturesForDisplay(displaysList.get(i));
+        }
+        enableDisplayIndependentFeatures();
+    }
 
-            if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
-                TouchExplorer explorer = new TouchExplorer(displayContext, mAms);
-                if ((mEnabledFeatures & FLAG_SERVICE_HANDLES_DOUBLE_TAP) != 0) {
-                    explorer.setServiceHandlesDoubleTap(true);
-                }
-                if ((mEnabledFeatures & FLAG_REQUEST_MULTI_FINGER_GESTURES) != 0) {
-                    explorer.setMultiFingerGesturesEnabled(true);
-                }
-                if ((mEnabledFeatures & FLAG_REQUEST_2_FINGER_PASSTHROUGH) != 0) {
-                    explorer.setTwoFingerPassthroughEnabled(true);
-                }
-                if ((mEnabledFeatures & FLAG_SEND_MOTION_EVENTS) != 0) {
-                    explorer.setSendMotionEventsEnabled(true);
-                }
-                addFirstEventHandler(displayId, explorer);
-                mTouchExplorer.put(displayId, explorer);
-            }
-
-            if ((mEnabledFeatures & FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER) != 0
-                    || ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0)
-                    || ((mEnabledFeatures & FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0)) {
-                final MagnificationGestureHandler magnificationGestureHandler =
-                        createMagnificationGestureHandler(displayId,
-                                displayContext);
-                addFirstEventHandler(displayId, magnificationGestureHandler);
-                mMagnificationGestureHandler.put(displayId, magnificationGestureHandler);
-            }
-
-            if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
-                MotionEventInjector injector = new MotionEventInjector(
-                        mContext.getMainLooper());
-                addFirstEventHandler(displayId, injector);
-                mMotionEventInjectors.put(displayId, injector);
-            }
+    private void enableFeaturesForDisplay(Display display) {
+        if (DEBUG) {
+            Slog.i(TAG, "enableFeaturesForDisplay() : display Id = " + display.getDisplayId());
         }
 
+        final Context displayContext = mContext.createDisplayContext(display);
+        final int displayId = display.getDisplayId();
+
+        if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
+            if (mAutoclickController == null) {
+                mAutoclickController = new AutoclickController(
+                        mContext, mUserId, mAms.getTraceManager());
+            }
+            addFirstEventHandler(displayId, mAutoclickController);
+        }
+
+        if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
+            TouchExplorer explorer = new TouchExplorer(displayContext, mAms);
+            if ((mEnabledFeatures & FLAG_SERVICE_HANDLES_DOUBLE_TAP) != 0) {
+                explorer.setServiceHandlesDoubleTap(true);
+            }
+            if ((mEnabledFeatures & FLAG_REQUEST_MULTI_FINGER_GESTURES) != 0) {
+                explorer.setMultiFingerGesturesEnabled(true);
+            }
+            if ((mEnabledFeatures & FLAG_REQUEST_2_FINGER_PASSTHROUGH) != 0) {
+                explorer.setTwoFingerPassthroughEnabled(true);
+            }
+            if ((mEnabledFeatures & FLAG_SEND_MOTION_EVENTS) != 0) {
+                explorer.setSendMotionEventsEnabled(true);
+            }
+            addFirstEventHandler(displayId, explorer);
+            mTouchExplorer.put(displayId, explorer);
+        }
+
+        if ((mEnabledFeatures & FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER) != 0
+                || ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0)
+                || ((mEnabledFeatures & FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0)) {
+            final MagnificationGestureHandler magnificationGestureHandler =
+                    createMagnificationGestureHandler(displayId,
+                            displayContext);
+            addFirstEventHandler(displayId, magnificationGestureHandler);
+            mMagnificationGestureHandler.put(displayId, magnificationGestureHandler);
+        }
+
+        if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
+            MotionEventInjector injector = new MotionEventInjector(
+                    mContext.getMainLooper(), mAms.getTraceManager());
+            addFirstEventHandler(displayId, injector);
+            mMotionEventInjectors.put(displayId, injector);
+        }
+    }
+
+    private void enableDisplayIndependentFeatures() {
         if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
             mAms.setMotionEventInjectors(mMotionEventInjectors);
         }
@@ -500,55 +536,57 @@
         mEventHandler.put(displayId, eventHandler);
     }
 
-    /**
-     * Adds an event handler to the event handler chain for all displays. The handler is added at
-     * the beginning of the chain.
-     *
-     * @param displayList The list of displays
-     * @param handler The handler to be added to the event handlers list.
-     */
-    private void addFirstEventHandlerForAllDisplays(ArrayList<Display> displayList,
-            EventStreamTransformation handler) {
-        for (int i = 0; i < displayList.size(); i++) {
-            final int displayId = displayList.get(i).getDisplayId();
-            addFirstEventHandler(displayId, handler);
+    private void disableFeatures() {
+        final ArrayList<Display> displaysList = mAms.getValidDisplayList();
+
+        for (int i = displaysList.size() - 1; i >= 0; i--) {
+            disableFeaturesForDisplay(displaysList.get(i).getDisplayId());
+        }
+        mAms.setMotionEventInjectors(null);
+        disableDisplayIndependentFeatures();
+
+        resetAllStreamState();
+    }
+
+    private void disableFeaturesForDisplay(int displayId) {
+        if (DEBUG) {
+            Slog.i(TAG, "disableFeaturesForDisplay() : display Id = " + displayId);
+        }
+
+        final MotionEventInjector injector = mMotionEventInjectors.get(displayId);
+        if (injector != null) {
+            injector.onDestroy();
+            mMotionEventInjectors.remove(displayId);
+        }
+
+        final TouchExplorer explorer = mTouchExplorer.get(displayId);
+        if (explorer != null) {
+            explorer.onDestroy();
+            mTouchExplorer.remove(displayId);
+        }
+
+        final MagnificationGestureHandler handler = mMagnificationGestureHandler.get(displayId);
+        if (handler != null) {
+            handler.onDestroy();
+            mMagnificationGestureHandler.remove(displayId);
+        }
+
+        final EventStreamTransformation eventStreamTransformation = mEventHandler.get(displayId);
+        if (eventStreamTransformation != null) {
+            mEventHandler.remove(displayId);
         }
     }
 
-    private void disableFeatures() {
-        for (int i = mMotionEventInjectors.size() - 1; i >= 0; i--) {
-            final MotionEventInjector injector = mMotionEventInjectors.valueAt(i);
-            if (injector != null) {
-                injector.onDestroy();
-            }
-        }
-        mAms.setMotionEventInjectors(null);
-        mMotionEventInjectors.clear();
+    private void disableDisplayIndependentFeatures() {
         if (mAutoclickController != null) {
             mAutoclickController.onDestroy();
             mAutoclickController = null;
         }
-        for (int i = mTouchExplorer.size() - 1; i >= 0; i--) {
-            final TouchExplorer explorer = mTouchExplorer.valueAt(i);
-            if (explorer != null) {
-                explorer.onDestroy();
-            }
-        }
-        mTouchExplorer.clear();
-        for (int i = mMagnificationGestureHandler.size() - 1; i >= 0; i--) {
-            final MagnificationGestureHandler handler = mMagnificationGestureHandler.valueAt(i);
-            if (handler != null) {
-                handler.onDestroy();
-            }
-        }
-        mMagnificationGestureHandler.clear();
+
         if (mKeyboardInterceptor != null) {
             mKeyboardInterceptor.onDestroy();
             mKeyboardInterceptor = null;
         }
-
-        mEventHandler.clear();
-        resetStreamState();
     }
 
     private MagnificationGestureHandler createMagnificationGestureHandler(
@@ -563,32 +601,46 @@
             final Context uiContext = displayContext.createWindowContext(
                     TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, null /* options */);
             magnificationGestureHandler = new WindowMagnificationGestureHandler(uiContext,
-                    mAms.getWindowMagnificationMgr(), mAms.getMagnificationController(),
-                    detectControlGestures, triggerable,
+                    mAms.getWindowMagnificationMgr(), mAms.getTraceManager(),
+                    mAms.getMagnificationController(), detectControlGestures, triggerable,
                     displayId);
         } else {
             final Context uiContext = displayContext.createWindowContext(
                     TYPE_MAGNIFICATION_OVERLAY, null /* options */);
             magnificationGestureHandler = new FullScreenMagnificationGestureHandler(uiContext,
-                    mAms.getFullScreenMagnificationController(), mAms.getMagnificationController(),
-                    detectControlGestures, triggerable,
+                    mAms.getFullScreenMagnificationController(), mAms.getTraceManager(),
+                    mAms.getMagnificationController(), detectControlGestures, triggerable,
                     new WindowMagnificationPromptController(displayContext, mUserId), displayId);
         }
         return magnificationGestureHandler;
     }
 
-    void resetStreamState() {
-        if (mTouchScreenStreamState != null) {
-            mTouchScreenStreamState.reset();
+    void resetAllStreamState() {
+        final ArrayList<Display> displaysList = mAms.getValidDisplayList();
+
+        for (int i = displaysList.size() - 1; i >= 0; i--) {
+            resetStreamStateForDisplay(displaysList.get(i).getDisplayId());
         }
-        if (mMouseStreamState != null) {
-            mMouseStreamState.reset();
-        }
+
         if (mKeyboardStreamState != null) {
             mKeyboardStreamState.reset();
         }
     }
 
+    void resetStreamStateForDisplay(int displayId) {
+        final EventStreamState touchScreenStreamState = mTouchScreenStreamStates.get(displayId);
+        if (touchScreenStreamState != null) {
+            touchScreenStreamState.reset();
+            mTouchScreenStreamStates.remove(displayId);
+        }
+
+        final EventStreamState mouseStreamState = mMouseStreamStates.get(displayId);
+        if (mouseStreamState != null) {
+            mouseStreamState.reset();
+            mMouseStreamStates.remove(displayId);
+        }
+    }
+
     @Override
     public void onDestroy() {
         /* ignore */
@@ -839,4 +891,45 @@
             mTouchExplorer.get(displayId).setTouchExplorationPassthroughRegion(region);
         }
     }
+
+    /**
+     * Dumps all {@link AccessibilityInputFilter}s here.
+     */
+    public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
+        if (mEventHandler == null) {
+            return;
+        }
+        pw.append("A11yInputFilter Info : ");
+        pw.println();
+
+        final ArrayList<Display> displaysList = mAms.getValidDisplayList();
+        for (int i = 0; i < displaysList.size(); i++) {
+            final int displayId = displaysList.get(i).getDisplayId();
+            EventStreamTransformation next = mEventHandler.get(displayId);
+            if (next != null) {
+                pw.append("Enabled features of Display [");
+                pw.append(Integer.toString(displayId));
+                pw.append("] = ");
+
+                final StringJoiner joiner = new StringJoiner(",", "[", "]");
+
+                while (next != null) {
+                    if (next instanceof MagnificationGestureHandler) {
+                        joiner.add("MagnificationGesture");
+                    } else if (next instanceof KeyboardInterceptor) {
+                        joiner.add("KeyboardInterceptor");
+                    } else if (next instanceof TouchExplorer) {
+                        joiner.add("TouchExplorer");
+                    } else if (next instanceof AutoclickController) {
+                        joiner.add("AutoclickController");
+                    } else if (next instanceof MotionEventInjector) {
+                        joiner.add("MotionEventInjector");
+                    }
+                    next = next.getNext();
+                }
+                pw.append(joiner.toString());
+            }
+            pw.println();
+        }
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index f631988..04ef101 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -16,6 +16,15 @@
 
 package com.android.server.accessibility;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_FINGERPRINT;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_INPUT_FILTER;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_PACKAGE_BROADCAST_RECEIVER;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_USER_BROADCAST_RECEIVER;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL;
 import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
@@ -289,8 +298,8 @@
         mContext = context;
         mPowerManager =  (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         mWindowManagerService = LocalServices.getService(WindowManagerInternal.class);
-        mTraceManager = new AccessibilityTraceManager(
-                mWindowManagerService.getAccessibilityController(), this);
+        mTraceManager = AccessibilityTraceManager.getInstance(
+                mWindowManagerService.getAccessibilityController(), this, mLock);
         mMainHandler = new MainHandler(mContext.getMainLooper());
         mActivityTaskManagerService = LocalServices.getService(ActivityTaskManagerInternal.class);
         mPackageManager = packageManager;
@@ -311,8 +320,8 @@
         mContext = context;
         mPowerManager = context.getSystemService(PowerManager.class);
         mWindowManagerService = LocalServices.getService(WindowManagerInternal.class);
-        mTraceManager = new AccessibilityTraceManager(
-                mWindowManagerService.getAccessibilityController(), this);
+        mTraceManager = AccessibilityTraceManager.getInstance(
+                mWindowManagerService.getAccessibilityController(), this, mLock);
         mMainHandler = new MainHandler(mContext.getMainLooper());
         mActivityTaskManagerService = LocalServices.getService(ActivityTaskManagerInternal.class);
         mPackageManager = mContext.getPackageManager();
@@ -324,7 +333,7 @@
         mSecurityPolicy = new AccessibilitySecurityPolicy(policyWarningUIController, mContext,
                 this);
         mA11yWindowManager = new AccessibilityWindowManager(mLock, mMainHandler,
-                mWindowManagerService, this, mSecurityPolicy, this);
+                mWindowManagerService, this, mSecurityPolicy, this, mTraceManager);
         mA11yDisplayListener = new AccessibilityDisplayListener(mContext, mMainHandler);
         mMagnificationController = new MagnificationController(this, mLock, mContext);
         init();
@@ -339,26 +348,16 @@
 
     @Override
     public int getCurrentUserIdLocked() {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".getCurrentUserIdLocked");
-        }
         return mCurrentUserId;
     }
 
     @Override
     public boolean isAccessibilityButtonShown() {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".isAccessibilityButtonShown");
-        }
         return mIsAccessibilityButtonShown;
     }
 
     @Override
     public void onServiceInfoChangedLocked(AccessibilityUserState userState) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(
-                    LOG_TAG + ".onServiceInfoChangedLocked", "userState=" + userState);
-        }
         mSecurityPolicy.onBoundServicesChangedLocked(userState.mUserId,
                 userState.mBoundServices);
         scheduleNotifyClientsOfServicesStateChangeLocked(userState);
@@ -424,8 +423,9 @@
         PackageMonitor monitor = new PackageMonitor() {
             @Override
             public void onSomePackagesChanged() {
-                if (mTraceManager.isA11yTracingEnabled()) {
-                    mTraceManager.logTrace(LOG_TAG + ".PM.onSomePackagesChanged");
+                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_PACKAGE_BROADCAST_RECEIVER)) {
+                    mTraceManager.logTrace(LOG_TAG + ".PM.onSomePackagesChanged",
+                            FLAGS_PACKAGE_BROADCAST_RECEIVER);
                 }
 
                 synchronized (mLock) {
@@ -452,8 +452,9 @@
                 // mBindingServices in binderDied() during updating. Remove services from  this
                 // package from mBindingServices, and then update the user state to re-bind new
                 // versions of them.
-                if (mTraceManager.isA11yTracingEnabled()) {
+                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_PACKAGE_BROADCAST_RECEIVER)) {
                     mTraceManager.logTrace(LOG_TAG + ".PM.onPackageUpdateFinished",
+                            FLAGS_PACKAGE_BROADCAST_RECEIVER,
                             "packageName=" + packageName + ";uid=" + uid);
                 }
                 synchronized (mLock) {
@@ -485,8 +486,9 @@
 
             @Override
             public void onPackageRemoved(String packageName, int uid) {
-                if (mTraceManager.isA11yTracingEnabled()) {
+                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_PACKAGE_BROADCAST_RECEIVER)) {
                     mTraceManager.logTrace(LOG_TAG + ".PM.onPackageRemoved",
+                            FLAGS_PACKAGE_BROADCAST_RECEIVER,
                             "packageName=" + packageName + ";uid=" + uid);
                 }
 
@@ -529,8 +531,9 @@
             @Override
             public boolean onHandleForceStop(Intent intent, String[] packages,
                     int uid, boolean doit) {
-                if (mTraceManager.isA11yTracingEnabled()) {
+                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_PACKAGE_BROADCAST_RECEIVER)) {
                     mTraceManager.logTrace(LOG_TAG + ".PM.onHandleForceStop",
+                            FLAGS_PACKAGE_BROADCAST_RECEIVER,
                             "intent=" + intent + ";packages=" + packages + ";uid=" + uid
                             + ";doit=" + doit);
                 }
@@ -580,8 +583,8 @@
         mContext.registerReceiverAsUser(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
-                if (mTraceManager.isA11yTracingEnabled()) {
-                    mTraceManager.logTrace(LOG_TAG + ".BR.onReceive",
+                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_USER_BROADCAST_RECEIVER)) {
+                    mTraceManager.logTrace(LOG_TAG + ".BR.onReceive", FLAGS_USER_BROADCAST_RECEIVER,
                             "context=" + context + ";intent=" + intent);
                 }
 
@@ -668,8 +671,8 @@
 
     @Override
     public long addClient(IAccessibilityManagerClient callback, int userId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".addClient",
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".addClient", FLAGS_ACCESSIBILITY_MANAGER,
                     "callback=" + callback + ";userId=" + userId);
         }
 
@@ -739,8 +742,8 @@
 
     @Override
     public void sendAccessibilityEvent(AccessibilityEvent event, int userId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".sendAccessibilityEvent",
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".sendAccessibilityEvent", FLAGS_ACCESSIBILITY_MANAGER,
                     "event=" + event + ";userId=" + userId);
         }
         boolean dispatchEvent = false;
@@ -803,6 +806,10 @@
                 }
             }
             if (shouldComputeWindows) {
+                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MANAGER_INTERNAL)) {
+                    mTraceManager.logTrace("WindowManagerInternal.computeWindowsForAccessibility",
+                            FLAGS_WINDOW_MANAGER_INTERNAL, "display=" + displayId);
+                }
                 final WindowManagerInternal wm = LocalServices.getService(
                         WindowManagerInternal.class);
                 wm.computeWindowsForAccessibility(displayId);
@@ -835,9 +842,9 @@
      */
     @Override
     public void registerSystemAction(RemoteAction action, int actionId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".registerSystemAction",
-                    "action=" + action + ";actionId=" + actionId);
+                    FLAGS_ACCESSIBILITY_MANAGER, "action=" + action + ";actionId=" + actionId);
         }
         mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY);
         getSystemActionPerformer().registerSystemAction(actionId, action);
@@ -850,8 +857,9 @@
      */
     @Override
     public void unregisterSystemAction(int actionId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".unregisterSystemAction", "actionId=" + actionId);
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".unregisterSystemAction",
+                    FLAGS_ACCESSIBILITY_MANAGER, "actionId=" + actionId);
         }
         mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY);
         getSystemActionPerformer().unregisterSystemAction(actionId);
@@ -867,9 +875,9 @@
 
     @Override
     public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(int userId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".getInstalledAccessibilityServiceList",
-                    "userId=" + userId);
+                    FLAGS_ACCESSIBILITY_MANAGER, "userId=" + userId);
         }
 
         final int resolvedUserId;
@@ -903,8 +911,9 @@
     @Override
     public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType,
             int userId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".getEnabledAccessibilityServiceList",
+                    FLAGS_ACCESSIBILITY_MANAGER,
                     "feedbackType=" + feedbackType + ";userId=" + userId);
         }
 
@@ -936,8 +945,9 @@
 
     @Override
     public void interrupt(int userId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".interrupt", "userId=" + userId);
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".interrupt",
+                    FLAGS_ACCESSIBILITY_MANAGER, "userId=" + userId);
         }
 
         List<IAccessibilityServiceClient> interfacesToInterrupt;
@@ -966,8 +976,10 @@
         }
         for (int i = 0, count = interfacesToInterrupt.size(); i < count; i++) {
             try {
-                if (mTraceManager.isA11yTracingEnabled()) {
-                    mTraceManager.logTrace(LOG_TAG + ".IAccessibilityServiceClient.onInterrupt");
+                if (mTraceManager.isA11yTracingEnabledForTypes(
+                        FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
+                    mTraceManager.logTrace(LOG_TAG + ".IAccessibilityServiceClient.onInterrupt",
+                            FLAGS_ACCESSIBILITY_SERVICE_CLIENT);
                 }
                 interfacesToInterrupt.get(i).onInterrupt();
             } catch (RemoteException re) {
@@ -981,8 +993,9 @@
     public int addAccessibilityInteractionConnection(IWindow windowToken, IBinder leashToken,
             IAccessibilityInteractionConnection connection, String packageName,
             int userId) throws RemoteException {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".addAccessibilityInteractionConnection",
+                    FLAGS_ACCESSIBILITY_MANAGER,
                     "windowToken=" + windowToken + "leashToken=" + leashToken + ";connection="
                             + connection + "; packageName=" + packageName + ";userId=" + userId);
         }
@@ -993,9 +1006,9 @@
 
     @Override
     public void removeAccessibilityInteractionConnection(IWindow window) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".removeAccessibilityInteractionConnection",
-                    "window=" + window);
+                    FLAGS_ACCESSIBILITY_MANAGER, "window=" + window);
         }
         mA11yWindowManager.removeAccessibilityInteractionConnection(window);
     }
@@ -1003,9 +1016,9 @@
     @Override
     public void setPictureInPictureActionReplacingConnection(
             IAccessibilityInteractionConnection connection) throws RemoteException {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".setPictureInPictureActionReplacingConnection",
-                    "connection=" + connection);
+                    FLAGS_ACCESSIBILITY_MANAGER, "connection=" + connection);
         }
         mSecurityPolicy.enforceCallingPermission(Manifest.permission.MODIFY_ACCESSIBILITY_DATA,
                 SET_PIP_ACTION_REPLACEMENT);
@@ -1017,10 +1030,11 @@
             IAccessibilityServiceClient serviceClient,
             AccessibilityServiceInfo accessibilityServiceInfo,
             int flags) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".registerUiTestAutomationService", "owner=" + owner
-                    + ";serviceClient=" + serviceClient + ";accessibilityServiceInfo="
-                    + accessibilityServiceInfo + ";flags=" + flags);
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".registerUiTestAutomationService",
+                    FLAGS_ACCESSIBILITY_MANAGER,
+                    "owner=" + owner + ";serviceClient=" + serviceClient
+                    + ";accessibilityServiceInfo=" + accessibilityServiceInfo + ";flags=" + flags);
         }
 
         mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT,
@@ -1037,9 +1051,9 @@
 
     @Override
     public void unregisterUiTestAutomationService(IAccessibilityServiceClient serviceClient) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".unregisterUiTestAutomationService",
-                    "serviceClient=" + serviceClient);
+                    FLAGS_ACCESSIBILITY_MANAGER, "serviceClient=" + serviceClient);
         }
         synchronized (mLock) {
             mUiAutomationManager.unregisterUiTestAutomationServiceLocked(serviceClient);
@@ -1049,15 +1063,20 @@
     @Override
     public void temporaryEnableAccessibilityStateUntilKeyguardRemoved(
             ComponentName service, boolean touchExplorationEnabled) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(
                     LOG_TAG + ".temporaryEnableAccessibilityStateUntilKeyguardRemoved",
+                    FLAGS_ACCESSIBILITY_MANAGER,
                     "service=" + service + ";touchExplorationEnabled=" + touchExplorationEnabled);
         }
 
         mSecurityPolicy.enforceCallingPermission(
                 Manifest.permission.TEMPORARY_ENABLE_ACCESSIBILITY,
                 TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED);
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MANAGER_INTERNAL)) {
+            mTraceManager.logTrace("WindowManagerInternal.isKeyguardLocked",
+                    FLAGS_WINDOW_MANAGER_INTERNAL);
+        }
         if (!mWindowManagerService.isKeyguardLocked()) {
             return;
         }
@@ -1083,9 +1102,9 @@
 
     @Override
     public IBinder getWindowToken(int windowId, int userId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".getWindowToken",
-                    "windowId=" + windowId + ";userId=" + userId);
+                    FLAGS_ACCESSIBILITY_MANAGER, "windowId=" + windowId + ";userId=" + userId);
         }
 
         mSecurityPolicy.enforceCallingPermission(
@@ -1127,8 +1146,9 @@
      */
     @Override
     public void notifyAccessibilityButtonClicked(int displayId, String targetName) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonClicked",
+                    FLAGS_ACCESSIBILITY_MANAGER,
                     "displayId=" + displayId + ";targetName=" + targetName);
         }
 
@@ -1157,9 +1177,9 @@
      */
     @Override
     public void notifyAccessibilityButtonVisibilityChanged(boolean shown) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonVisibilityChanged",
-                    "shown=" + shown);
+                    FLAGS_ACCESSIBILITY_MANAGER, "shown=" + shown);
         }
 
         mSecurityPolicy.enforceCallingOrSelfPermission(
@@ -1190,10 +1210,6 @@
      */
     @Override
     public void onSystemActionsChanged() {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".onSystemActionsChanged");
-        }
-
         synchronized (mLock) {
             AccessibilityUserState state = getCurrentUserStateLocked();
             notifySystemActionsChangedLocked(state);
@@ -1256,11 +1272,6 @@
 
     @Override
     public @Nullable MotionEventInjector getMotionEventInjectorForDisplayLocked(int displayId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".getMotionEventInjectorForDisplayLocked",
-                    "displayId=" + displayId);
-        }
-
         final long endMillis = SystemClock.uptimeMillis() + WAIT_MOTION_INJECTOR_TIMEOUT_MILLIS;
         MotionEventInjector motionEventInjector = null;
         while ((mMotionEventInjectors == null) && (SystemClock.uptimeMillis() < endMillis)) {
@@ -1323,6 +1334,10 @@
         synchronized (mLock) {
             token = getWindowToken(windowId, mCurrentUserId);
         }
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MANAGER_INTERNAL)) {
+            mTraceManager.logTrace("WindowManagerInternal.getWindowFrame",
+                    FLAGS_WINDOW_MANAGER_INTERNAL, "token=" + token + ";outBounds=" + outBounds);
+        }
         mWindowManagerService.getWindowFrame(token, outBounds);
         if (!outBounds.isEmpty()) {
             return true;
@@ -1471,7 +1486,7 @@
     private int getClientStateLocked(AccessibilityUserState userState) {
         return userState.getClientStateLocked(
             mUiAutomationManager.isUiAutomationRunningLocked(),
-            mTraceManager.isA11yTracingEnabled());
+            mTraceManager.getTraceStateForAccessibilityManagerClientState());
     }
 
     private InteractionBridge getInteractionBridge() {
@@ -1681,6 +1696,10 @@
     }
 
     private void updateRelevantEventsLocked(AccessibilityUserState userState) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
+            mTraceManager.logTrace(LOG_TAG + ".updateRelevantEventsLocked",
+                    FLAGS_ACCESSIBILITY_SERVICE_CLIENT, "userState=" + userState);
+        }
         mMainHandler.post(() -> {
             broadcastToClients(userState, ignoreRemoteException(client -> {
                 int relevantEventTypes;
@@ -1830,12 +1849,6 @@
     @Override
     public void persistComponentNamesToSettingLocked(String settingName,
             Set<ComponentName> componentNames, int userId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".persistComponentNamesToSettingLocked",
-                    "settingName=" + settingName + ";componentNames=" + componentNames + ";userId="
-                            + userId);
-        }
-
         persistColonDelimitedSetToSettingLocked(settingName, userId, componentNames,
                 componentName -> componentName.flattenToShortString());
     }
@@ -1960,7 +1973,7 @@
         }
     }
 
-    private void scheduleUpdateClientsIfNeededLocked(AccessibilityUserState userState) {
+    void scheduleUpdateClientsIfNeededLocked(AccessibilityUserState userState) {
         final int clientState = getClientStateLocked(userState);
         if (userState.getLastSentClientStateLocked() != clientState
                 && (mGlobalClients.getRegisteredCallbackCount() > 0
@@ -1983,6 +1996,10 @@
 
     private void sendStateToClients(int clientState,
             RemoteCallbackList<IAccessibilityManagerClient> clients) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER_CLIENT)) {
+            mTraceManager.logTrace(LOG_TAG + ".sendStateToClients",
+                    FLAGS_ACCESSIBILITY_MANAGER_CLIENT, "clientState=" + clientState);
+        }
         clients.broadcast(ignoreRemoteException(
                 client -> client.setState(clientState)));
     }
@@ -2003,6 +2020,10 @@
 
     private void notifyClientsOfServicesStateChange(
             RemoteCallbackList<IAccessibilityManagerClient> clients, long uiTimeout) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER_CLIENT)) {
+            mTraceManager.logTrace(LOG_TAG + ".notifyClientsOfServicesStateChange",
+                    FLAGS_ACCESSIBILITY_MANAGER_CLIENT, "uiTimeout=" + uiTimeout);
+        }
         clients.broadcast(ignoreRemoteException(
                 client -> client.notifyServicesStateChanged(uiTimeout)));
     }
@@ -2082,6 +2103,12 @@
             }
         }
         if (setInputFilter) {
+            if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MANAGER_INTERNAL
+                    | FLAGS_INPUT_FILTER)) {
+                mTraceManager.logTrace("WindowManagerInternal.setInputFilter",
+                        FLAGS_WINDOW_MANAGER_INTERNAL | FLAGS_INPUT_FILTER,
+                        "inputFilter=" + inputFilter);
+            }
             mWindowManagerService.setInputFilter(inputFilter);
         }
     }
@@ -2805,26 +2832,21 @@
     @GuardedBy("mLock")
     @Override
     public MagnificationSpec getCompatibleMagnificationSpecLocked(int windowId) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".getCompatibleMagnificationSpecLocked",
-                    "windowId=" + windowId);
-        }
-
         IBinder windowToken = mA11yWindowManager.getWindowTokenForUserAndWindowIdLocked(
                 mCurrentUserId, windowId);
         if (windowToken != null) {
-            return mWindowManagerService.getCompatibleMagnificationSpecForWindow(
-                    windowToken);
+            if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MANAGER_INTERNAL)) {
+                mTraceManager.logTrace(LOG_TAG + ".getCompatibleMagnificationSpecForWindow",
+                        FLAGS_WINDOW_MANAGER_INTERNAL, "windowToken=" + windowToken);
+            }
+
+            return mWindowManagerService.getCompatibleMagnificationSpecForWindow(windowToken);
         }
         return null;
     }
 
     @Override
     public KeyEventDispatcher getKeyEventDispatcher() {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".getKeyEventDispatcher");
-        }
-
         if (mKeyEventDispatcher == null) {
             mKeyEventDispatcher = new KeyEventDispatcher(
                     mMainHandler, MainHandler.MSG_SEND_KEY_EVENT_TO_INPUT_FILTER, mLock,
@@ -2837,13 +2859,6 @@
     @SuppressWarnings("AndroidFrameworkPendingIntentMutability")
     public PendingIntent getPendingIntentActivity(Context context, int requestCode, Intent intent,
             int flags) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".getPendingIntentActivity",
-                    "context=" + context + ";requestCode=" + requestCode + ";intent=" + intent
-                            + ";flags=" + flags);
-        }
-
-
         return PendingIntent.getActivity(context, requestCode, intent, flags);
     }
 
@@ -2858,9 +2873,9 @@
      */
     @Override
     public void performAccessibilityShortcut(String targetName) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".performAccessibilityShortcut",
-                    "targetName=" + targetName);
+                    FLAGS_ACCESSIBILITY_MANAGER, "targetName=" + targetName);
         }
 
         if ((UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)
@@ -3048,9 +3063,9 @@
 
     @Override
     public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".getAccessibilityShortcutTargets",
-                    "shortcutType=" + shortcutType);
+                    FLAGS_ACCESSIBILITY_MANAGER, "shortcutType=" + shortcutType);
         }
 
         if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
@@ -3122,11 +3137,6 @@
 
     @Override
     public void sendAccessibilityEventForCurrentUserLocked(AccessibilityEvent event) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".sendAccessibilityEventForCurrentUserLocked",
-                    "event=" + event);
-        }
-
         sendAccessibilityEventLocked(event, mCurrentUserId);
     }
 
@@ -3148,8 +3158,10 @@
      */
     @Override
     public boolean sendFingerprintGesture(int gestureKeyCode) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(
+                FLAGS_ACCESSIBILITY_MANAGER | FLAGS_FINGERPRINT)) {
             mTraceManager.logTrace(LOG_TAG + ".sendFingerprintGesture",
+                    FLAGS_ACCESSIBILITY_MANAGER | FLAGS_FINGERPRINT,
                     "gestureKeyCode=" + gestureKeyCode);
         }
 
@@ -3174,9 +3186,9 @@
      */
     @Override
     public int getAccessibilityWindowId(@Nullable IBinder windowToken) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".getAccessibilityWindowId",
-                    "windowToken=" + windowToken);
+                    FLAGS_ACCESSIBILITY_MANAGER, "windowToken=" + windowToken);
         }
 
         synchronized (mLock) {
@@ -3196,8 +3208,9 @@
      */
     @Override
     public long getRecommendedTimeoutMillis() {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".getRecommendedTimeoutMillis");
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(
+                    LOG_TAG + ".getRecommendedTimeoutMillis", FLAGS_ACCESSIBILITY_MANAGER);
         }
 
         synchronized(mLock) {
@@ -3214,8 +3227,10 @@
     @Override
     public void setWindowMagnificationConnection(
             IWindowMagnificationConnection connection) throws RemoteException {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(
+                FLAGS_ACCESSIBILITY_MANAGER | FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
             mTraceManager.logTrace(LOG_TAG + ".setWindowMagnificationConnection",
+                    FLAGS_ACCESSIBILITY_MANAGER | FLAGS_WINDOW_MAGNIFICATION_CONNECTION,
                     "connection=" + connection);
         }
 
@@ -3249,9 +3264,9 @@
 
     @Override
     public void associateEmbeddedHierarchy(@NonNull IBinder host, @NonNull IBinder embedded) {
-        if (mTraceManager.isA11yTracingEnabled()) {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".associateEmbeddedHierarchy",
-                    "host=" + host + ";embedded=" + embedded);
+                    FLAGS_ACCESSIBILITY_MANAGER, "host=" + host + ";embedded=" + embedded);
         }
 
         synchronized (mLock) {
@@ -3261,8 +3276,9 @@
 
     @Override
     public void disassociateEmbeddedHierarchy(@NonNull IBinder token) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".disassociateEmbeddedHierarchy", "token=" + token);
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".disassociateEmbeddedHierarchy",
+                    FLAGS_ACCESSIBILITY_MANAGER, "token=" + token);
         }
 
         synchronized (mLock) {
@@ -3274,7 +3290,11 @@
      * Gets the stroke width of the focus rectangle.
      * @return The stroke width.
      */
+    @Override
     public int getFocusStrokeWidth() {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".getFocusStrokeWidth", FLAGS_ACCESSIBILITY_MANAGER);
+        }
         synchronized (mLock) {
             final AccessibilityUserState userState = getCurrentUserStateLocked();
 
@@ -3286,7 +3306,11 @@
      * Gets the color of the focus rectangle.
      * @return The color.
      */
+    @Override
     public int getFocusColor() {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".getFocusColor", FLAGS_ACCESSIBILITY_MANAGER);
+        }
         synchronized (mLock) {
             final AccessibilityUserState userState = getCurrentUserStateLocked();
 
@@ -3314,6 +3338,9 @@
                 pw.println();
             }
             mA11yWindowManager.dump(fd, pw, args);
+            if (mHasInputFilter && mInputFilter != null) {
+                mInputFilter.dump(fd, pw, args);
+            }
             pw.println("Global client list info:{");
             mGlobalClients.dump(pw, "    Client list ");
             pw.println("    Registered clients:{");
@@ -3350,9 +3377,6 @@
 
     @Override
     public FullScreenMagnificationController getFullScreenMagnificationController() {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".getFullScreenMagnificationController");
-        }
         synchronized (mLock) {
             return mMagnificationController.getFullScreenMagnificationController();
         }
@@ -3360,11 +3384,6 @@
 
     @Override
     public void onClientChangeLocked(boolean serviceInfoChanged) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".onClientChangeLocked",
-                    "serviceInfoChanged=" + serviceInfoChanged);
-        }
-
         AccessibilityUserState userState = getUserStateLocked(mCurrentUserId);
         onUserStateChangedLocked(userState);
         if (serviceInfoChanged) {
@@ -3569,7 +3588,7 @@
             synchronized (mLock) {
                 mDisplaysList.add(display);
                 if (mInputFilter != null) {
-                    mInputFilter.onDisplayChanged();
+                    mInputFilter.onDisplayAdded(display);
                 }
                 AccessibilityUserState userState = getCurrentUserStateLocked();
                 if (displayId != Display.DEFAULT_DISPLAY) {
@@ -3591,7 +3610,7 @@
                     return;
                 }
                 if (mInputFilter != null) {
-                    mInputFilter.onDisplayChanged();
+                    mInputFilter.onDisplayRemoved(displayId);
                 }
                 AccessibilityUserState userState = getCurrentUserStateLocked();
                 if (displayId != Display.DEFAULT_DISPLAY) {
@@ -3891,11 +3910,6 @@
 
     @Override
     public void setGestureDetectionPassthroughRegion(int displayId, Region region) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".setGestureDetectionPassthroughRegion",
-                    "displayId=" + displayId + ";region=" + region);
-        }
-
         mMainHandler.sendMessage(
                 obtainMessage(
                         AccessibilityManagerService::setGestureDetectionPassthroughRegionInternal,
@@ -3906,11 +3920,6 @@
 
     @Override
     public void setTouchExplorationPassthroughRegion(int displayId, Region region) {
-        if (mTraceManager.isA11yTracingEnabled()) {
-            mTraceManager.logTrace(LOG_TAG + ".setTouchExplorationPassthroughRegion",
-                    "displayId=" + displayId + ";region=" + region);
-        }
-
         mMainHandler.sendMessage(
                 obtainMessage(
                         AccessibilityManagerService::setTouchExplorationPassthroughRegionInternal,
@@ -3939,7 +3948,10 @@
         if (userState.mUserId != mCurrentUserId) {
             return;
         }
-
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
+            mTraceManager.logTrace(LOG_TAG + ".updateFocusAppearanceDataLocked",
+                    FLAGS_ACCESSIBILITY_SERVICE_CLIENT, "userState=" + userState);
+        }
         mMainHandler.post(() -> {
             broadcastToClients(userState, ignoreRemoteException(client -> {
                 client.mCallback.setFocusAppearance(userState.getFocusStrokeWidthLocked(),
@@ -3949,7 +3961,7 @@
 
     }
 
-    AccessibilityTraceManager getTraceManager() {
+    public AccessibilityTraceManager getTraceManager() {
         return mTraceManager;
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 7d75b73..467cab5 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -20,6 +20,7 @@
 
 import android.Manifest;
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -53,10 +54,7 @@
  */
 class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnection {
     private static final String LOG_TAG = "AccessibilityServiceConnection";
-    private static final String TRACE_A11Y_SERVICE_CONNECTION =
-            LOG_TAG + ".IAccessibilityServiceConnection";
-    private static final String TRACE_A11Y_SERVICE_CLIENT =
-            LOG_TAG + ".IAccessibilityServiceClient";
+
     /*
      Holding a weak reference so there isn't a loop of references. AccessibilityUserState keeps
      lists of bound and binding services. These are freed on user changes, but just in case it
@@ -137,8 +135,8 @@
 
     @Override
     public void disableSelf() {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".disableSelf");
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("disableSelf", "");
         }
         synchronized (mLock) {
             AccessibilityUserState userState = mUserStateWeakReference.get();
@@ -218,9 +216,9 @@
             return;
         }
         try {
-            if (mTrace.isA11yTracingEnabled()) {
-                mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".init", this + ", " + mId + ", "
-                        + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
+            if (svcClientTracingEnabled()) {
+                logTraceSvcClient("init",
+                        this + "," + mId + "," + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
             }
             serviceInterface.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
         } catch (RemoteException re) {
@@ -264,9 +262,8 @@
 
     @Override
     public boolean setSoftKeyboardShowMode(int showMode) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".setSoftKeyboardShowMode",
-                    "showMode=" + showMode);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("setSoftKeyboardShowMode", "showMode=" + showMode);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -280,8 +277,8 @@
 
     @Override
     public int getSoftKeyboardShowMode() {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".getSoftKeyboardShowMode");
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("getSoftKeyboardShowMode", "");
         }
         final AccessibilityUserState userState = mUserStateWeakReference.get();
         return (userState != null) ? userState.getSoftKeyboardShowModeLocked() : 0;
@@ -289,9 +286,8 @@
 
     @Override
     public boolean switchToInputMethod(String imeId) {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".switchToInputMethod",
-                    "imeId=" + imeId);
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("switchToInputMethod", "imeId=" + imeId);
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -311,8 +307,8 @@
 
     @Override
     public boolean isAccessibilityButtonAvailable() {
-        if (mTrace.isA11yTracingEnabled()) {
-            mTrace.logTrace(TRACE_A11Y_SERVICE_CONNECTION + ".isAccessibilityButtonAvailable");
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("isAccessibilityButtonAvailable", "");
         }
         synchronized (mLock) {
             if (!hasRightsToCurrentUserLocked()) {
@@ -373,9 +369,9 @@
         }
         if (serviceInterface != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT
-                            + ".onFingerprintCapturingGesturesChanged", String.valueOf(active));
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient(
+                            "onFingerprintCapturingGesturesChanged", String.valueOf(active));
                 }
                 mServiceInterface.onFingerprintCapturingGesturesChanged(active);
             } catch (RemoteException e) {
@@ -394,9 +390,8 @@
         }
         if (serviceInterface != null) {
             try {
-                if (mTrace.isA11yTracingEnabled()) {
-                    mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onFingerprintGesture",
-                            String.valueOf(gesture));
+                if (svcClientTracingEnabled()) {
+                    logTraceSvcClient("onFingerprintGesture", String.valueOf(gesture));
                 }
                 mServiceInterface.onFingerprintGesture(gesture);
             } catch (RemoteException e) {
@@ -410,15 +405,17 @@
             if (mSecurityPolicy.canPerformGestures(this)) {
                 MotionEventInjector motionEventInjector =
                         mSystemSupport.getMotionEventInjectorForDisplayLocked(displayId);
+                if (wmTracingEnabled()) {
+                    logTraceWM("isTouchOrFaketouchDevice", "");
+                }
                 if (motionEventInjector != null
                         && mWindowManagerService.isTouchOrFaketouchDevice()) {
                     motionEventInjector.injectEvents(
                             gestureSteps.getList(), mServiceInterface, sequence, displayId);
                 } else {
                     try {
-                        if (mTrace.isA11yTracingEnabled()) {
-                            mTrace.logTrace(TRACE_A11Y_SERVICE_CLIENT + ".onPerformGestureResult",
-                                    sequence + ", false");
+                        if (svcClientTracingEnabled()) {
+                            logTraceSvcClient("onPerformGestureResult", sequence + ", false");
                         }
                         mServiceInterface.onPerformGestureResult(sequence, false);
                     } catch (RemoteException re) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
index 6396960..8cf5547 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
@@ -60,7 +60,7 @@
             }
             case "start-trace":
             case "stop-trace":
-                return mService.getTraceManager().onShellCommand(cmd);
+                return mService.getTraceManager().onShellCommand(cmd, this);
         }
         return -1;
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityTrace.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityTrace.java
deleted file mode 100644
index 0391413..0000000
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityTrace.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.accessibility;
-
-/**
- * Interface to log accessibility trace.
- */
-public interface AccessibilityTrace {
-    /**
-     * Whether the trace is enabled.
-     */
-    boolean isA11yTracingEnabled();
-
-    /**
-     * Start tracing.
-     */
-    void startTrace();
-
-    /**
-     * Stop tracing.
-     */
-    void stopTrace();
-
-    /**
-     * Log one trace entry.
-     * @param where A string to identify this log entry, which can be used to filter/search
-     *        through the tracing file.
-     */
-    void logTrace(String where);
-
-    /**
-     * Log one trace entry.
-     * @param where A string to identify this log entry, which can be used to filter/search
-     *        through the tracing file.
-     * @param callingParams The parameters for the method to be logged.
-     */
-    void logTrace(String where, String callingParams);
-
-    /**
-     * Log one trace entry. Accessibility services using AccessibilityInteractionClient to
-     * make screen content related requests use this API to log entry when receive callback.
-     * @param timestamp The timestamp when a callback is received.
-     * @param where A string to identify this log entry, which can be used to filter/search
-     *        through the tracing file.
-     * @param callingParams The parameters for the callback.
-     * @param processId The process id of the calling component.
-     * @param threadId The threadId of the calling component.
-     * @param callingUid The calling uid of the callback.
-     * @param callStack The call stack of the callback.
-     */
-    void logTrace(long timestamp, String where, String callingParams, int processId,
-            long threadId, int callingUid, StackTraceElement[] callStack);
-}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityTraceManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityTraceManager.java
index 6105e8a..51e01ea 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityTraceManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityTraceManager.java
@@ -15,72 +15,197 @@
  */
 package com.android.server.accessibility;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CLIENT;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT_STATES;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_LOGGING_ALL;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_LOGGING_NONE;
+import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TRACE_A11Y_INTERACTION_CLIENT_ENABLED;
+import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_CB_ENABLED;
+import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_ENABLED;
+import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TRACE_A11Y_SERVICE_ENABLED;
+
+import android.accessibilityservice.AccessibilityTrace;
+import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.os.Binder;
+import android.os.ShellCommand;
 
 import com.android.server.wm.WindowManagerInternal;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 /**
  * Manager of accessibility trace.
  */
-class AccessibilityTraceManager implements AccessibilityTrace {
+public class AccessibilityTraceManager implements AccessibilityTrace {
     private final WindowManagerInternal.AccessibilityControllerInternal mA11yController;
     private final AccessibilityManagerService mService;
+    private final Object mA11yMSLock;
 
-    AccessibilityTraceManager(
+    private long mEnabledLoggingFlags;
+
+    private static AccessibilityTraceManager sInstance = null;
+
+    @MainThread
+    static AccessibilityTraceManager getInstance(
             @NonNull WindowManagerInternal.AccessibilityControllerInternal a11yController,
-            @NonNull AccessibilityManagerService service) {
+            @NonNull AccessibilityManagerService service,
+            @NonNull Object lock) {
+        if (sInstance == null) {
+            sInstance = new AccessibilityTraceManager(a11yController, service, lock);
+        }
+        return sInstance;
+    }
+
+    private AccessibilityTraceManager(
+            @NonNull WindowManagerInternal.AccessibilityControllerInternal a11yController,
+            @NonNull AccessibilityManagerService service,
+            @NonNull Object lock) {
         mA11yController = a11yController;
         mService = service;
+        mA11yMSLock = lock;
+        mEnabledLoggingFlags = FLAGS_LOGGING_NONE;
     }
 
     @Override
     public boolean isA11yTracingEnabled() {
-        return mA11yController.isAccessibilityTracingEnabled();
+        synchronized (mA11yMSLock) {
+            return mEnabledLoggingFlags != FLAGS_LOGGING_NONE;
+        }
     }
 
     @Override
-    public void startTrace() {
-        if (!mA11yController.isAccessibilityTracingEnabled()) {
-            mA11yController.startTrace();
-            mService.scheduleUpdateClientsIfNeeded(mService.getCurrentUserState());
+    public boolean isA11yTracingEnabledForTypes(long typeIdFlags) {
+        synchronized (mA11yMSLock) {
+            return ((typeIdFlags & mEnabledLoggingFlags) != FLAGS_LOGGING_NONE);
         }
     }
 
     @Override
+    public int getTraceStateForAccessibilityManagerClientState() {
+        int state = 0x0;
+        synchronized (mA11yMSLock) {
+            if (isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION)) {
+                state |= STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_ENABLED;
+            }
+            if (isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK)) {
+                state |= STATE_FLAG_TRACE_A11Y_INTERACTION_CONNECTION_CB_ENABLED;
+            }
+            if (isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_INTERACTION_CLIENT)) {
+                state |= STATE_FLAG_TRACE_A11Y_INTERACTION_CLIENT_ENABLED;
+            }
+            if (isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_SERVICE)) {
+                state |= STATE_FLAG_TRACE_A11Y_SERVICE_ENABLED;
+            }
+        }
+        return state;
+    }
+
+    @Override
+    public void startTrace(long loggingTypes) {
+        if (loggingTypes == FLAGS_LOGGING_NONE) {
+            // Ignore start none request
+            return;
+        }
+
+        synchronized (mA11yMSLock) {
+            long oldEnabled = mEnabledLoggingFlags;
+            mEnabledLoggingFlags = loggingTypes;
+
+            if (needToNotifyClients(oldEnabled)) {
+                mService.scheduleUpdateClientsIfNeededLocked(mService.getCurrentUserState());
+            }
+        }
+
+        mA11yController.startTrace(loggingTypes);
+    }
+
+    @Override
     public void stopTrace() {
-        if (mA11yController.isAccessibilityTracingEnabled()) {
+        boolean stop = false;
+        synchronized (mA11yMSLock) {
+            stop = isA11yTracingEnabled();
+
+            long oldEnabled = mEnabledLoggingFlags;
+            mEnabledLoggingFlags = FLAGS_LOGGING_NONE;
+
+            if (needToNotifyClients(oldEnabled)) {
+                mService.scheduleUpdateClientsIfNeededLocked(mService.getCurrentUserState());
+            }
+        }
+        if (stop) {
             mA11yController.stopTrace();
-            mService.scheduleUpdateClientsIfNeeded(mService.getCurrentUserState());
         }
     }
 
     @Override
-    public void logTrace(String where) {
-        logTrace(where, "");
+    public void logTrace(String where, long loggingTypes) {
+        logTrace(where, loggingTypes, "");
     }
 
     @Override
-    public void logTrace(String where, String callingParams) {
-        mA11yController.logTrace(where, callingParams, "".getBytes(),
-                Binder.getCallingUid(), Thread.currentThread().getStackTrace());
-    }
-
-    @Override
-    public void logTrace(long timestamp, String where, String callingParams, int processId,
-            long threadId, int callingUid, StackTraceElement[] callStack) {
-        if (mA11yController.isAccessibilityTracingEnabled()) {
-            mA11yController.logTrace(where, callingParams, "".getBytes(), callingUid, callStack,
-                    timestamp, processId, threadId);
+    public void logTrace(String where, long loggingTypes, String callingParams) {
+        if (isA11yTracingEnabledForTypes(loggingTypes)) {
+            mA11yController.logTrace(where, loggingTypes, callingParams, "".getBytes(),
+                    Binder.getCallingUid(), Thread.currentThread().getStackTrace(),
+                    new HashSet<String>(Arrays.asList("logTrace")));
         }
     }
 
-    int onShellCommand(String cmd) {
+    @Override
+    public void logTrace(long timestamp, String where, long loggingTypes, String callingParams,
+            int processId, long threadId, int callingUid, StackTraceElement[] callStack,
+            Set<String> ignoreElementList) {
+        if (isA11yTracingEnabledForTypes(loggingTypes)) {
+            mA11yController.logTrace(where, loggingTypes, callingParams, "".getBytes(), callingUid,
+                    callStack, timestamp, processId, threadId,
+                    ((ignoreElementList == null) ? new HashSet<String>() : ignoreElementList));
+        }
+    }
+
+    private boolean needToNotifyClients(long otherTypesEnabled) {
+        return (mEnabledLoggingFlags & FLAGS_ACCESSIBILITY_MANAGER_CLIENT_STATES)
+                != (otherTypesEnabled & FLAGS_ACCESSIBILITY_MANAGER_CLIENT_STATES);
+    }
+
+    int onShellCommand(String cmd, ShellCommand shell) {
         switch (cmd) {
             case "start-trace": {
-                startTrace();
+                String opt = shell.getNextOption();
+                if (opt == null) {
+                    startTrace(FLAGS_LOGGING_ALL);
+                    return 0;
+                }
+                List<String> types = new ArrayList<String>();
+                while (opt != null) {
+                    switch (opt) {
+                        case "-t": {
+                            String type = shell.getNextArg();
+                            while (type != null) {
+                                types.add(type);
+                                type = shell.getNextArg();
+                            }
+                            break;
+                        }
+                        default: {
+                            shell.getErrPrintWriter().println(
+                                    "Error: option not recognized " + opt);
+                            stopTrace();
+                            return -1;
+                        }
+                    }
+                    opt = shell.getNextOption();
+                }
+                long enabledTypes = AccessibilityTrace.getLoggingFlagsFromNames(types);
+                startTrace(enabledTypes);
                 return 0;
             }
             case "stop-trace": {
@@ -92,8 +217,29 @@
     }
 
     void onHelp(PrintWriter pw) {
-        pw.println("  start-trace");
-        pw.println("    Start the debug tracing.");
+        pw.println("  start-trace [-t LOGGING_TYPE [LOGGING_TYPE...]]");
+        pw.println("    Start the debug tracing. If no option is present, full trace will be");
+        pw.println("    generated. Options are:");
+        pw.println("      -t: Only generate tracing for the logging type(s) specified here.");
+        pw.println("          LOGGING_TYPE can be any one of below:");
+        pw.println("            IAccessibilityServiceConnection");
+        pw.println("            IAccessibilityServiceClient");
+        pw.println("            IAccessibilityManager");
+        pw.println("            IAccessibilityManagerClient");
+        pw.println("            IAccessibilityInteractionConnection");
+        pw.println("            IAccessibilityInteractionConnectionCallback");
+        pw.println("            IRemoteMagnificationAnimationCallback");
+        pw.println("            IWindowMagnificationConnection");
+        pw.println("            IWindowMagnificationConnectionCallback");
+        pw.println("            WindowManagerInternal");
+        pw.println("            WindowsForAccessibilityCallback");
+        pw.println("            MagnificationCallbacks");
+        pw.println("            InputFilter");
+        pw.println("            Gesture");
+        pw.println("            AccessibilityService");
+        pw.println("            PMBroadcastReceiver");
+        pw.println("            UserBroadcastReceiver");
+        pw.println("            FingerprintGesture");
         pw.println("  stop-trace");
         pw.println("    Stop the debug tracing.");
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 0fde0de..c70bf73 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -392,7 +392,7 @@
         return mBoundServices;
     }
 
-    int getClientStateLocked(boolean isUiAutomationRunning, boolean isTracingEnabled) {
+    int getClientStateLocked(boolean isUiAutomationRunning, int traceClientState) {
         int clientState = 0;
         final boolean a11yEnabled = isUiAutomationRunning
                 || isHandlingAccessibilityEventsLocked();
@@ -408,9 +408,9 @@
         if (mIsTextHighContrastEnabled) {
             clientState |= AccessibilityManager.STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED;
         }
-        if (isTracingEnabled) {
-            clientState |= AccessibilityManager.STATE_FLAG_ACCESSIBILITY_TRACING_ENABLED;
-        }
+
+        clientState |= traceClientState;
+
         return clientState;
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index ff79469..244f357 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED;
 
@@ -69,6 +71,7 @@
     private final AccessibilityEventSender mAccessibilityEventSender;
     private final AccessibilitySecurityPolicy mSecurityPolicy;
     private final AccessibilityUserManager mAccessibilityUserManager;
+    private final AccessibilityTraceManager mTraceManager;
 
     // Connections and window tokens for cross-user windows
     private final SparseArray<RemoteAccessibilityConnection>
@@ -151,6 +154,10 @@
                 // In some cases, onWindowsForAccessibilityChanged will be called immediately in
                 // setWindowsForAccessibilityCallback. We'll lost windows if flag is false.
                 mTrackingWindows = true;
+                if (traceWMEnabled()) {
+                    logTraceWM("setWindowsForAccessibilityCallback",
+                            "displayId=" + mDisplayId + ";callback=" + this);
+                }
                 result = mWindowManagerInternal.setWindowsForAccessibilityCallback(
                         mDisplayId, this);
                 if (!result) {
@@ -167,6 +174,10 @@
          */
         void stopTrackingWindowsLocked() {
             if (mTrackingWindows) {
+                if (traceWMEnabled()) {
+                    logTraceWM("setWindowsForAccessibilityCallback",
+                            "displayId=" + mDisplayId + ";callback=null");
+                }
                 mWindowManagerInternal.setWindowsForAccessibilityCallback(
                         mDisplayId, null);
                 mTrackingWindows = false;
@@ -373,6 +384,20 @@
             }
         }
 
+        /**
+         * Called when the display is reparented and becomes an embedded
+         * display.
+         *
+         * @param embeddedDisplayId The embedded display Id.
+         */
+        @Override
+        public void onDisplayReparented(int embeddedDisplayId) {
+            // Removes the un-used window observer for the embedded display.
+            synchronized (mLock) {
+                mDisplayWindowsObservers.remove(embeddedDisplayId);
+            }
+        }
+
         private boolean shouldUpdateWindowsLocked(boolean forceSend,
                 @NonNull List<WindowInfo> windows) {
             if (forceSend) {
@@ -474,6 +499,9 @@
             if (oldWindow.displayId != newWindow.displayId) {
                 return true;
             }
+            if (oldWindow.taskId != newWindow.taskId) {
+                return true;
+            }
             return false;
         }
 
@@ -674,6 +702,7 @@
             reportedWindow.setAnchorId(window.accessibilityIdOfAnchor);
             reportedWindow.setPictureInPicture(window.inPictureInPicture);
             reportedWindow.setDisplayId(window.displayId);
+            reportedWindow.setTaskId(window.taskId);
 
             final int parentId = findWindowIdLocked(userId, window.parentToken);
             if (parentId >= 0) {
@@ -844,19 +873,21 @@
     }
 
     /**
-     * Constructor for AccessibilityManagerService.
+     * Constructor for AccessibilityWindowManager.
      */
     public AccessibilityWindowManager(@NonNull Object lock, @NonNull Handler handler,
             @NonNull WindowManagerInternal windowManagerInternal,
             @NonNull AccessibilityEventSender accessibilityEventSender,
             @NonNull AccessibilitySecurityPolicy securityPolicy,
-            @NonNull AccessibilityUserManager accessibilityUserManager) {
+            @NonNull AccessibilityUserManager accessibilityUserManager,
+            @NonNull AccessibilityTraceManager traceManager) {
         mLock = lock;
         mHandler = handler;
         mWindowManagerInternal = windowManagerInternal;
         mAccessibilityEventSender = accessibilityEventSender;
         mSecurityPolicy = securityPolicy;
         mAccessibilityUserManager = accessibilityUserManager;
+        mTraceManager = traceManager;
     }
 
     /**
@@ -957,6 +988,9 @@
         final int windowId;
         boolean shouldComputeWindows = false;
         final IBinder token = window.asBinder();
+        if (traceWMEnabled()) {
+            logTraceWM("getDisplayIdForWindow", "token=" + token);
+        }
         final int displayId = mWindowManagerInternal.getDisplayIdForWindow(token);
         synchronized (mLock) {
             // We treat calls from a profile as if made by its parent as profiles
@@ -1003,9 +1037,15 @@
             registerIdLocked(leashToken, windowId);
         }
         if (shouldComputeWindows) {
+            if (traceWMEnabled()) {
+                logTraceWM("computeWindowsForAccessibility", "displayId=" + displayId);
+            }
             mWindowManagerInternal.computeWindowsForAccessibility(displayId);
         }
-
+        if (traceWMEnabled()) {
+            logTraceWM("setAccessibilityIdToSurfaceMetadata",
+                    "token=" + token + ";windowId=" + windowId);
+        }
         mWindowManagerInternal.setAccessibilityIdToSurfaceMetadata(token, windowId);
         return windowId;
     }
@@ -1139,6 +1179,10 @@
             mActiveWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
         }
         if (binder != null) {
+            if (traceWMEnabled()) {
+                logTraceWM("setAccessibilityIdToSurfaceMetadata", "token=" + binder
+                        + ";windowId=AccessibilityWindowInfo.UNDEFINED_WINDOW_ID");
+            }
             mWindowManagerInternal.setAccessibilityIdToSurfaceMetadata(
                     binder, AccessibilityWindowInfo.UNDEFINED_WINDOW_ID);
         }
@@ -1169,6 +1213,9 @@
      * @return The userId
      */
     public int getWindowOwnerUserId(@NonNull IBinder windowToken) {
+        if (traceWMEnabled()) {
+            logTraceWM("getWindowOwnerUserId", "token=" + windowToken);
+        }
         return mWindowManagerInternal.getWindowOwnerUserId(windowToken);
     }
 
@@ -1547,6 +1594,10 @@
         for (int i = 0; i < connectionList.size(); i++) {
             final RemoteAccessibilityConnection connection = connectionList.get(i);
             if (connection != null) {
+                if (traceIntConnEnabled()) {
+                    logTraceIntConn("notifyOutsideTouch");
+                }
+
                 try {
                     connection.getRemote().notifyOutsideTouch();
                 } catch (RemoteException re) {
@@ -1567,6 +1618,9 @@
      */
     public int getDisplayIdByUserIdAndWindowIdLocked(int userId, int windowId) {
         final IBinder windowToken = getWindowTokenForUserAndWindowIdLocked(userId, windowId);
+        if (traceWMEnabled()) {
+            logTraceWM("getDisplayIdForWindow", "token=" + windowToken);
+        }
         final int displayId = mWindowManagerInternal.getDisplayIdForWindow(windowToken);
         return displayId;
     }
@@ -1595,6 +1649,9 @@
      * @return The input focused windowId, or -1 if not found
      */
     private int findFocusedWindowId(int userId) {
+        if (traceWMEnabled()) {
+            logTraceWM("getFocusedWindowToken", "");
+        }
         final IBinder token = mWindowManagerInternal.getFocusedWindowToken();
         synchronized (mLock) {
             return findWindowIdLocked(userId, token);
@@ -1644,6 +1701,9 @@
                 return;
             }
         }
+        if (traceIntConnEnabled()) {
+            logTraceIntConn("notifyOutsideTouch");
+        }
         try {
             connection.getRemote().clearAccessibilityFocus();
         } catch (RemoteException re) {
@@ -1666,6 +1726,25 @@
         return null;
     }
 
+    private boolean traceWMEnabled() {
+        return mTraceManager.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MANAGER_INTERNAL);
+    }
+
+    private void logTraceWM(String methodName, String params) {
+        mTraceManager.logTrace("WindowManagerInternal." + methodName,
+                    FLAGS_WINDOW_MANAGER_INTERNAL, params);
+    }
+
+    private boolean traceIntConnEnabled() {
+        return mTraceManager.isA11yTracingEnabledForTypes(
+                FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION);
+    }
+
+    private void logTraceIntConn(String methodName) {
+        mTraceManager.logTrace(
+                    LOG_TAG + "." + methodName, FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION);
+    }
+
     /**
      * Associate the token of the embedded view hierarchy to the host view hierarchy.
      *
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
index f5b0eb1..95f3560 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility;
 
+import android.accessibilityservice.AccessibilityTrace;
 import android.annotation.NonNull;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -56,6 +57,7 @@
 
     private static final String LOG_TAG = AutoclickController.class.getSimpleName();
 
+    private final AccessibilityTraceManager mTrace;
     private final Context mContext;
     private final int mUserId;
 
@@ -63,13 +65,18 @@
     private ClickScheduler mClickScheduler;
     private ClickDelayObserver mClickDelayObserver;
 
-    public AutoclickController(Context context, int userId) {
+    public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
+        mTrace = trace;
         mContext = context;
         mUserId = userId;
     }
 
     @Override
     public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (mTrace.isA11yTracingEnabledForTypes(AccessibilityTrace.FLAGS_INPUT_FILTER)) {
+            mTrace.logTrace(LOG_TAG + ".onMotionEvent", AccessibilityTrace.FLAGS_INPUT_FILTER,
+                    "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
+        }
         if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
             if (mClickScheduler == null) {
                 Handler handler = new Handler(mContext.getMainLooper());
@@ -89,6 +96,10 @@
 
     @Override
     public void onKeyEvent(KeyEvent event, int policyFlags) {
+        if (mTrace.isA11yTracingEnabledForTypes(AccessibilityTrace.FLAGS_INPUT_FILTER)) {
+            mTrace.logTrace(LOG_TAG + ".onKeyEvent", AccessibilityTrace.FLAGS_INPUT_FILTER,
+                    "event=" + event + ";policyFlags=" + policyFlags);
+        }
         if (mClickScheduler != null) {
             if (KeyEvent.isModifierKey(event.getKeyCode())) {
                 mClickScheduler.updateMetaState(event.getMetaState());
diff --git a/services/accessibility/java/com/android/server/accessibility/KeyboardInterceptor.java b/services/accessibility/java/com/android/server/accessibility/KeyboardInterceptor.java
index bc379c2..b8250c0 100644
--- a/services/accessibility/java/com/android/server/accessibility/KeyboardInterceptor.java
+++ b/services/accessibility/java/com/android/server/accessibility/KeyboardInterceptor.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_INPUT_FILTER;
+
 import android.os.Handler;
 import android.os.Message;
 import android.os.SystemClock;
@@ -64,6 +66,10 @@
 
     @Override
     public void onKeyEvent(KeyEvent event, int policyFlags) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(FLAGS_INPUT_FILTER)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onKeyEvent",
+                    FLAGS_INPUT_FILTER, "event=" + event + ";policyFlags=" + policyFlags);
+        }
         /*
          * Certain keys have system-level behavior that affects accessibility services.
          * Let that behavior settle before handling the keys
diff --git a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
index 2673cd1..5cbd1a2 100644
--- a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
+++ b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility;
 
+import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.GestureDescription;
 import android.accessibilityservice.GestureDescription.GestureStep;
 import android.accessibilityservice.GestureDescription.TouchPoint;
@@ -68,6 +69,7 @@
     private final Handler mHandler;
     private final SparseArray<Boolean> mOpenGesturesInProgress = new SparseArray<>();
 
+    private final AccessibilityTraceManager mTrace;
     private IAccessibilityServiceClient mServiceInterfaceForCurrentGesture;
     private IntArray mSequencesInProgress = new IntArray(5);
     private boolean mIsDestroyed = false;
@@ -80,15 +82,17 @@
     /**
      * @param looper A looper on the main thread to use for dispatching new events
      */
-    public MotionEventInjector(Looper looper) {
+    public MotionEventInjector(Looper looper, AccessibilityTraceManager trace) {
         mHandler = new Handler(looper, this);
+        mTrace = trace;
     }
 
     /**
      * @param handler A handler to post messages. Exposes internal state for testing only.
      */
-    public MotionEventInjector(Handler handler) {
+    public MotionEventInjector(Handler handler, AccessibilityTraceManager trace) {
         mHandler = handler;
+        mTrace = trace;
     }
 
     /**
@@ -112,6 +116,12 @@
 
     @Override
     public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (mTrace.isA11yTracingEnabledForTypes(
+                AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE)) {
+            mTrace.logTrace(LOG_TAG + ".onMotionEvent",
+                    AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE,
+                    "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
+        }
         // MotionEventInjector would cancel any injected gesture when any MotionEvent arrives.
         // For user using an external device to control the pointer movement, it's almost
         // impossible to perform the gestures. Any slightly unintended movement results in the
diff --git a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
index 8ccdb7a..7ee0690 100644
--- a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
@@ -17,6 +17,7 @@
 package com.android.server.accessibility;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.annotation.Nullable;
 import android.app.UiAutomation;
@@ -270,6 +271,14 @@
                     // If the serviceInterface is null, the UiAutomation has been shut down on
                     // another thread.
                     if (serviceInterface != null) {
+                        if (mTrace.isA11yTracingEnabledForTypes(
+                                AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
+                            mTrace.logTrace("UiAutomationService.connectServiceUnknownThread",
+                                    AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT,
+                                    "serviceConnection=" + this + ";connectionId=" + mId
+                                    + "windowToken="
+                                    + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
+                        }
                         serviceInterface.init(this, mId,
                                 mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
                     }
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index d7bc040..74f0bcb 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility.gestures;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_GESTURE;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_INPUT_FILTER;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_HOVER_ENTER;
@@ -82,6 +84,7 @@
         implements GestureManifold.Listener {
 
     static final boolean DEBUG = false;
+    private static final long LOGGING_FLAGS = FLAGS_GESTURE | FLAGS_INPUT_FILTER;
 
     // Tag for logging received events.
     private static final String LOG_TAG = "TouchExplorer";
@@ -254,6 +257,10 @@
 
     @Override
     public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onMotionEvent", LOGGING_FLAGS,
+                    "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
+        }
         if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
             super.onMotionEvent(event, rawEvent, policyFlags);
             return;
@@ -308,6 +315,10 @@
 
     @Override
     public void onAccessibilityEvent(AccessibilityEvent event) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onAccessibilityEvent",
+                    LOGGING_FLAGS, "event=" + event);
+        }
         final int eventType = event.getEventType();
 
         if (eventType == TYPE_VIEW_HOVER_EXIT) {
@@ -346,6 +357,10 @@
 
     @Override
     public void onDoubleTapAndHold(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onDoubleTapAndHold", LOGGING_FLAGS,
+                    "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
+        }
         if (mDispatcher.longPressWithTouchEvents(event, policyFlags)) {
             sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
             if (isSendMotionEventsEnabled()) {
@@ -362,6 +377,10 @@
 
     @Override
     public boolean onDoubleTap(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onDoubleTap", LOGGING_FLAGS,
+                    "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
+        }
         mAms.onTouchInteractionEnd();
         // Remove pending event deliveries.
         mSendHoverEnterAndMoveDelayed.cancel();
@@ -394,6 +413,9 @@
 
     @Override
     public boolean onGestureStarted() {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onGestureStarted", LOGGING_FLAGS);
+        }
         // We have to perform gesture detection, so
         // clear the current state and try to detect.
         mSendHoverEnterAndMoveDelayed.cancel();
@@ -407,6 +429,10 @@
 
     @Override
     public boolean onGestureCompleted(AccessibilityGestureEvent gestureEvent) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onGestureCompleted",
+                    LOGGING_FLAGS, "event=" + gestureEvent);
+        }
         endGestureDetection(true);
         mSendTouchInteractionEndDelayed.cancel();
         dispatchGesture(gestureEvent);
@@ -415,6 +441,10 @@
 
     @Override
     public boolean onGestureCancelled(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(LOGGING_FLAGS)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onGestureCancelled", LOGGING_FLAGS,
+                    "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
+        }
         if (mState.isGestureDetecting()) {
             endGestureDetection(event.getActionMasked() == ACTION_UP);
             return true;
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index 1f66bfd..718da9e 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -16,6 +16,8 @@
 
 package com.android.server.accessibility.magnification;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL;
+
 import android.animation.Animator;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
@@ -46,6 +48,7 @@
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
 import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.wm.WindowManagerInternal;
 
 import java.util.Locale;
@@ -135,6 +138,10 @@
          */
         @GuardedBy("mLock")
         boolean register() {
+            if (traceEnabled()) {
+                logTrace("setMagnificationCallbacks",
+                        "displayID=" + mDisplayId + ";callback=" + this);
+            }
             mRegistered = mControllerCtx.getWindowManager().setMagnificationCallbacks(
                     mDisplayId, this);
             if (!mRegistered) {
@@ -142,6 +149,10 @@
                 return false;
             }
             mSpecAnimationBridge.setEnabled(true);
+            if (traceEnabled()) {
+                logTrace("getMagnificationRegion",
+                        "displayID=" + mDisplayId + ";region=" + mMagnificationRegion);
+            }
             // Obtain initial state.
             mControllerCtx.getWindowManager().getMagnificationRegion(
                     mDisplayId, mMagnificationRegion);
@@ -162,6 +173,10 @@
         void unregister(boolean delete) {
             if (mRegistered) {
                 mSpecAnimationBridge.setEnabled(false);
+                if (traceEnabled()) {
+                    logTrace("setMagnificationCallbacks",
+                            "displayID=" + mDisplayId + ";callback=null");
+                }
                 mControllerCtx.getWindowManager().setMagnificationCallbacks(
                         mDisplayId, null);
                 mMagnificationRegion.setEmpty();
@@ -268,7 +283,7 @@
         }
 
         @Override
-        public void onRotationChanged(int rotation) {
+        public void onDisplaySizeChanged() {
             // Treat as context change and reset
             final Message m = PooledLambda.obtainMessage(
                     FullScreenMagnificationController::resetIfNeeded,
@@ -431,6 +446,10 @@
         void setForceShowMagnifiableBounds(boolean show) {
             if (mRegistered) {
                 mForceShowMagnifiableBounds = show;
+                if (traceEnabled()) {
+                    logTrace("setForceShowMagnifiableBounds",
+                            "displayID=" + mDisplayId + ";show=" + show);
+                }
                 mControllerCtx.getWindowManager().setForceShowMagnifiableBounds(
                         mDisplayId, show);
             }
@@ -1255,6 +1274,16 @@
         }
     }
 
+    private boolean traceEnabled() {
+        return mControllerCtx.getTraceManager().isA11yTracingEnabledForTypes(
+                FLAGS_WINDOW_MANAGER_INTERNAL);
+    }
+
+    private void logTrace(String methodName, String params) {
+        mControllerCtx.getTraceManager().logTrace(
+                "WindowManagerInternal." + methodName, FLAGS_WINDOW_MANAGER_INTERNAL, params);
+    }
+
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder();
@@ -1320,6 +1349,13 @@
                     mEnabled = enabled;
                     if (!mEnabled) {
                         mSentMagnificationSpec.clear();
+                        if (mControllerCtx.getTraceManager().isA11yTracingEnabledForTypes(
+                                FLAGS_WINDOW_MANAGER_INTERNAL)) {
+                            mControllerCtx.getTraceManager().logTrace(
+                                    "WindowManagerInternal.setMagnificationSpec",
+                                    FLAGS_WINDOW_MANAGER_INTERNAL,
+                                    "displayID=" + mDisplayId + ";spec=" + mSentMagnificationSpec);
+                        }
                         mControllerCtx.getWindowManager().setMagnificationSpec(
                                 mDisplayId, mSentMagnificationSpec);
                     }
@@ -1367,6 +1403,13 @@
                 }
 
                 mSentMagnificationSpec.setTo(spec);
+                if (mControllerCtx.getTraceManager().isA11yTracingEnabledForTypes(
+                        FLAGS_WINDOW_MANAGER_INTERNAL)) {
+                    mControllerCtx.getTraceManager().logTrace(
+                            "WindowManagerInternal.setMagnificationSpec",
+                            FLAGS_WINDOW_MANAGER_INTERNAL,
+                            "displayID=" + mDisplayId + ";spec=" + mSentMagnificationSpec);
+                }
                 mControllerCtx.getWindowManager().setMagnificationSpec(
                         mDisplayId, mSentMagnificationSpec);
             }
@@ -1455,6 +1498,7 @@
     public static class ControllerContext {
         private final Context mContext;
         private final AccessibilityManagerService mAms;
+        private final AccessibilityTraceManager mTrace;
         private final WindowManagerInternal mWindowManager;
         private final Handler mHandler;
         private final Long mAnimationDuration;
@@ -1469,6 +1513,7 @@
                 long animationDuration) {
             mContext = context;
             mAms = ams;
+            mTrace = ams.getTraceManager();
             mWindowManager = windowManager;
             mHandler = handler;
             mAnimationDuration = animationDuration;
@@ -1491,6 +1536,14 @@
         }
 
         /**
+         * @return AccessibilityTraceManager
+         */
+        @NonNull
+        public AccessibilityTraceManager getTraceManager() {
+            return mTrace;
+        }
+
+        /**
          * @return WindowManagerInternal
          */
         @NonNull
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index f7d1b9a..c3d8d4c 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -61,6 +61,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.gestures.GestureUtils;
 
 /**
@@ -142,12 +143,13 @@
 
     public FullScreenMagnificationGestureHandler(@UiContext Context context,
             FullScreenMagnificationController fullScreenMagnificationController,
+            AccessibilityTraceManager trace,
             Callback callback,
             boolean detectTripleTap,
             boolean detectShortcutTrigger,
             @NonNull WindowMagnificationPromptController promptController,
             int displayId) {
-        super(displayId, detectTripleTap, detectShortcutTrigger, callback);
+        super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
         if (DEBUG_ALL) {
             Log.i(mLogTag,
                     "FullScreenMagnificationGestureHandler(detectTripleTap = " + detectTripleTap
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index f9aecd7..8aacafb 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -411,8 +411,7 @@
         synchronized (mLock) {
             if (mWindowMagnificationMgr == null) {
                 mWindowMagnificationMgr = new WindowMagnificationManager(mContext,
-                        mAms.getCurrentUserIdLocked(),
-                        this);
+                        mAms.getCurrentUserIdLocked(), this, mAms.getTraceManager());
             }
             return mWindowMagnificationMgr;
         }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
index bbe40b6..19b3396 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
@@ -20,11 +20,13 @@
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_UP;
 
+import android.accessibilityservice.AccessibilityTrace;
 import android.annotation.NonNull;
 import android.util.Log;
 import android.util.Slog;
 import android.view.MotionEvent;
 
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.BaseEventStreamTransformation;
 
 import java.util.ArrayDeque;
@@ -99,14 +101,17 @@
         void onTripleTapped(int displayId, int mode);
     }
 
+    private final AccessibilityTraceManager mTrace;
     protected final Callback mCallback;
 
     protected MagnificationGestureHandler(int displayId, boolean detectTripleTap,
             boolean detectShortcutTrigger,
+            AccessibilityTraceManager trace,
             @NonNull Callback callback) {
         mDisplayId = displayId;
         mDetectTripleTap = detectTripleTap;
         mDetectShortcutTrigger = detectShortcutTrigger;
+        mTrace = trace;
         mCallback = callback;
 
         mDebugInputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null;
@@ -118,6 +123,12 @@
         if (DEBUG_ALL) {
             Slog.i(mLogTag, "onMotionEvent(" + event + ")");
         }
+        if (mTrace.isA11yTracingEnabledForTypes(
+                AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE)) {
+            mTrace.logTrace("MagnificationGestureHandler.onMotionEvent",
+                    AccessibilityTrace.FLAGS_INPUT_FILTER | AccessibilityTrace.FLAGS_GESTURE,
+                    "event=" + event + ";rawEvent=" + rawEvent + ";policyFlags=" + policyFlags);
+        }
         if (DEBUG_EVENT_STREAM) {
             storeEventInto(mDebugInputEventHistory, event);
         }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
index 993027d..5277425 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
@@ -16,6 +16,9 @@
 
 package com.android.server.accessibility.magnification;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK;
 import static android.os.IBinder.DeathRecipient;
 
 import android.annotation.NonNull;
@@ -27,6 +30,8 @@
 import android.view.accessibility.IWindowMagnificationConnectionCallback;
 import android.view.accessibility.MagnificationAnimationCallback;
 
+import com.android.server.accessibility.AccessibilityTraceManager;
+
 /**
  * A wrapper of {@link IWindowMagnificationConnection}.
  */
@@ -36,9 +41,12 @@
     private static final String TAG = "WindowMagnificationConnectionWrapper";
 
     private final @NonNull IWindowMagnificationConnection mConnection;
+    private final @NonNull AccessibilityTraceManager mTrace;
 
-    WindowMagnificationConnectionWrapper(@NonNull IWindowMagnificationConnection connection) {
+    WindowMagnificationConnectionWrapper(@NonNull IWindowMagnificationConnection connection,
+            @NonNull AccessibilityTraceManager trace) {
         mConnection = connection;
+        mTrace = trace;
     }
 
     //Should not use this instance anymore after calling it.
@@ -52,9 +60,15 @@
 
     boolean enableWindowMagnification(int displayId, float scale, float centerX, float centerY,
             @Nullable MagnificationAnimationCallback callback) {
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".enableWindowMagnification",
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION,
+                    "displayId=" + displayId + ";scale=" + scale + ";centerX=" + centerX
+                    + ";centerY=" + centerY + ";callback=" + callback);
+        }
         try {
             mConnection.enableWindowMagnification(displayId, scale, centerX, centerY,
-                    transformToRemoteCallback(callback));
+                    transformToRemoteCallback(callback, mTrace));
         } catch (RemoteException e) {
             if (DBG) {
                 Slog.e(TAG, "Error calling enableWindowMagnification()", e);
@@ -65,6 +79,10 @@
     }
 
     boolean setScale(int displayId, float scale) {
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".setScale", FLAGS_WINDOW_MAGNIFICATION_CONNECTION,
+                    "displayId=" + displayId + ";scale=" + scale);
+        }
         try {
             mConnection.setScale(displayId, scale);
         } catch (RemoteException e) {
@@ -78,8 +96,14 @@
 
     boolean disableWindowMagnification(int displayId,
             @Nullable MagnificationAnimationCallback callback) {
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".disableWindowMagnification",
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION,
+                    "displayId=" + displayId + ";callback=" + callback);
+        }
         try {
-            mConnection.disableWindowMagnification(displayId, transformToRemoteCallback(callback));
+            mConnection.disableWindowMagnification(displayId,
+                    transformToRemoteCallback(callback, mTrace));
         } catch (RemoteException e) {
             if (DBG) {
                 Slog.e(TAG, "Error calling disableWindowMagnification()", e);
@@ -90,6 +114,10 @@
     }
 
     boolean moveWindowMagnifier(int displayId, float offsetX, float offsetY) {
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".moveWindowMagnifier", FLAGS_WINDOW_MAGNIFICATION_CONNECTION,
+                    "displayId=" + displayId + ";offsetX=" + offsetX + ";offsetY=" + offsetY);
+        }
         try {
             mConnection.moveWindowMagnifier(displayId, offsetX, offsetY);
         } catch (RemoteException e) {
@@ -102,6 +130,11 @@
     }
 
     boolean showMagnificationButton(int displayId, int magnificationMode) {
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".showMagnificationButton",
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION,
+                    "displayId=" + displayId + ";mode=" + magnificationMode);
+        }
         try {
             mConnection.showMagnificationButton(displayId, magnificationMode);
         } catch (RemoteException e) {
@@ -114,6 +147,10 @@
     }
 
     boolean removeMagnificationButton(int displayId) {
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".removeMagnificationButton",
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION, "displayId=" + displayId);
+        }
         try {
             mConnection.removeMagnificationButton(displayId);
         } catch (RemoteException e) {
@@ -126,6 +163,14 @@
     }
 
     boolean setConnectionCallback(IWindowMagnificationConnectionCallback connectionCallback) {
+        if (mTrace.isA11yTracingEnabledForTypes(
+                FLAGS_WINDOW_MAGNIFICATION_CONNECTION
+                | FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK)) {
+            mTrace.logTrace(TAG + ".setConnectionCallback",
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION
+                    | FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK,
+                    "callback=" + connectionCallback);
+        }
         try {
             mConnection.setConnectionCallback(connectionCallback);
         } catch (RemoteException e) {
@@ -139,25 +184,38 @@
 
     private static @Nullable
             IRemoteMagnificationAnimationCallback transformToRemoteCallback(
-            MagnificationAnimationCallback callback) {
+            MagnificationAnimationCallback callback, AccessibilityTraceManager trace) {
         if (callback == null) {
             return null;
         }
-        return new RemoteAnimationCallback(callback);
+        return new RemoteAnimationCallback(callback, trace);
     }
 
     private static class RemoteAnimationCallback extends
             IRemoteMagnificationAnimationCallback.Stub {
-
         private final MagnificationAnimationCallback mCallback;
+        private final AccessibilityTraceManager mTrace;
 
-        RemoteAnimationCallback(@NonNull MagnificationAnimationCallback callback) {
+        RemoteAnimationCallback(@NonNull MagnificationAnimationCallback callback,
+                               @NonNull AccessibilityTraceManager trace) {
             mCallback = callback;
+            mTrace = trace;
+            if (mTrace.isA11yTracingEnabledForTypes(
+                    FLAGS_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK)) {
+                mTrace.logTrace("RemoteAnimationCallback.constructor",
+                        FLAGS_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK, "callback=" + callback);
+            }
         }
 
         @Override
         public void onResult(boolean success) throws RemoteException {
             mCallback.onResult(success);
+            if (mTrace.isA11yTracingEnabledForTypes(
+                    FLAGS_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK)) {
+                mTrace.logTrace("RemoteAnimationCallback.onResult",
+                        FLAGS_REMOTE_MAGNIFICATION_ANIMATION_CALLBACK, "success=" + success);
+            }
+
         }
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
index 4fb9a03..b26d364 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
@@ -34,6 +34,7 @@
 import android.view.MotionEvent;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.EventStreamTransformation;
 import com.android.server.accessibility.gestures.MultiTap;
 import com.android.server.accessibility.gestures.MultiTapAndHold;
@@ -89,9 +90,10 @@
 
     public WindowMagnificationGestureHandler(@UiContext Context context,
             WindowMagnificationManager windowMagnificationMgr,
+            AccessibilityTraceManager trace,
             Callback callback,
             boolean detectTripleTap, boolean detectShortcutTrigger, int displayId) {
-        super(displayId, detectTripleTap, detectShortcutTrigger, callback);
+        super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
         if (DEBUG_ALL) {
             Slog.i(mLogTag,
                     "WindowMagnificationGestureHandler() , displayId = " + displayId + ")");
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
index 938cb73..7a111d8 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
@@ -16,6 +16,9 @@
 
 package com.android.server.accessibility.magnification;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
@@ -39,6 +42,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.server.LocalServices;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.statusbar.StatusBarManagerInternal;
 
 /**
@@ -111,11 +115,14 @@
     }
 
     private final Callback mCallback;
+    private final AccessibilityTraceManager mTrace;
 
-    public WindowMagnificationManager(Context context, int userId, @NonNull Callback callback) {
+    public WindowMagnificationManager(Context context, int userId, @NonNull Callback callback,
+            AccessibilityTraceManager trace) {
         mContext = context;
         mUserId = userId;
         mCallback = callback;
+        mTrace = trace;
     }
 
     /**
@@ -135,7 +142,7 @@
                 mConnectionWrapper = null;
             }
             if (connection != null) {
-                mConnectionWrapper = new WindowMagnificationConnectionWrapper(connection);
+                mConnectionWrapper = new WindowMagnificationConnectionWrapper(connection, mTrace);
             }
 
             if (mConnectionWrapper != null) {
@@ -197,7 +204,10 @@
                 }
             }
         }
-
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".requestWindowMagnificationConnection",
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION, "connect=" + connect);
+        }
         final long identity = Binder.clearCallingIdentity();
         try {
             final StatusBarManagerInternal service = LocalServices.getService(
@@ -511,6 +521,12 @@
 
         @Override
         public void onWindowMagnifierBoundsChanged(int displayId, Rect bounds) {
+            if (mTrace.isA11yTracingEnabledForTypes(
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK)) {
+                mTrace.logTrace(TAG + "ConnectionCallback.onWindowMagnifierBoundsChanged",
+                        FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK,
+                        "displayId=" + displayId + ";bounds=" + bounds);
+            }
             synchronized (mLock) {
                 WindowMagnifier magnifier = mWindowMagnifiers.get(displayId);
                 if (magnifier == null) {
@@ -527,11 +543,23 @@
         @Override
         public void onChangeMagnificationMode(int displayId, int magnificationMode)
                 throws RemoteException {
+            if (mTrace.isA11yTracingEnabledForTypes(
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK)) {
+                mTrace.logTrace(TAG + "ConnectionCallback.onChangeMagnificationMode",
+                        FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK,
+                        "displayId=" + displayId + ";mode=" + magnificationMode);
+            }
             //TODO: Uses this method to change the magnification mode on non-default display.
         }
 
         @Override
         public void onSourceBoundsChanged(int displayId, Rect sourceBounds) {
+            if (mTrace.isA11yTracingEnabledForTypes(
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK)) {
+                mTrace.logTrace(TAG + "ConnectionCallback.onSourceBoundsChanged",
+                        FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK,
+                        "displayId=" + displayId + ";source=" + sourceBounds);
+            }
             synchronized (mLock) {
                 WindowMagnifier magnifier = mWindowMagnifiers.get(displayId);
                 if (magnifier == null) {
@@ -543,11 +571,23 @@
 
         @Override
         public void onPerformScaleAction(int displayId, float scale) {
+            if (mTrace.isA11yTracingEnabledForTypes(
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK)) {
+                mTrace.logTrace(TAG + "ConnectionCallback.onPerformScaleAction",
+                        FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK,
+                        "displayId=" + displayId + ";scale=" + scale);
+            }
             mCallback.onPerformScaleAction(displayId, scale);
         }
 
         @Override
         public void onAccessibilityActionPerformed(int displayId) {
+            if (mTrace.isA11yTracingEnabledForTypes(
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK)) {
+                mTrace.logTrace(TAG + "ConnectionCallback.onAccessibilityActionPerformed",
+                        FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK,
+                        "displayId=" + displayId);
+            }
             mCallback.onAccessibilityActionPerformed(displayId);
         }
 
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 30de4b4..4748a86 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -566,7 +566,7 @@
                 return PendingIntent.getActivityAsUser(getContext(),
                         0 /* request code */,
                         NotificationAccessConfirmationActivityContract.launcherIntent(
-                                userId, component, packageTitle),
+                                getContext(), userId, component, packageTitle),
                         PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
                                 | PendingIntent.FLAG_CANCEL_CURRENT,
                         null /* options */,
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 904def0..6bd1fa6 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -190,6 +190,9 @@
         Slog.w(TAG, "remote service died: " + service);
         synchronized (mLock) {
             mZombie = true;
+            writeServiceEvent(
+                    FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_REMOTE_SERVICE_DIED,
+                    getServiceComponentName());
         }
     }
 
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
index 9f3045e..3a90a95 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
@@ -87,7 +87,7 @@
         mId = sessionId;
         mUid = uid;
         mContentCaptureContext = new ContentCaptureContext(/* clientContext= */ null,
-                activityId, appComponentName, displayId, flags);
+                activityId, appComponentName, displayId, activityToken, flags);
         mSessionStateReceiver = sessionStateReceiver;
         try {
             sessionStateReceiver.asBinder().linkToDeath(() -> onClientDeath(), 0);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 55b982b..e5716ee 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -149,7 +149,7 @@
         "android.hardware.biometrics.fingerprint-V2.3-java",
         "android.hardware.biometrics.fingerprint-V1-java",
         "android.hardware.oemlock-V1.0-java",
-        "android.hardware.configstore-V1.0-java",
+        "android.hardware.configstore-V1.1-java",
         "android.hardware.contexthub-V1.0-java",
         "android.hardware.rebootescrow-V1-java",
         "android.hardware.soundtrigger-V2.3-java",
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 6d43d4b..fab58e8 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1029,7 +1029,6 @@
             return;
         }
 
-        int phoneId = getPhoneIdFromSubId(subId);
         synchronized (mRecords) {
             // register
             IBinder b = callback.asBinder();
@@ -1052,21 +1051,24 @@
             // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID,
             // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID
             if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+                if (DBG) {
+                    log("invalid subscription id, use default id");
+                }
                 r.subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
             } else {//APP specify subID
                 r.subId = subId;
             }
-            r.phoneId = phoneId;
+            r.phoneId = getPhoneIdFromSubId(r.subId);
             r.eventList = events;
 
             if (DBG) {
-                log("listen:  Register r=" + r + " r.subId=" + r.subId + " phoneId=" + phoneId);
+                log("listen:  Register r=" + r + " r.subId=" + r.subId + " r.phoneId=" + r.phoneId);
             }
-            if (notifyNow && validatePhoneId(phoneId)) {
+            if (notifyNow && validatePhoneId(r.phoneId)) {
                 if (events.contains(TelephonyCallback.EVENT_SERVICE_STATE_CHANGED)){
                     try {
-                        if (VDBG) log("listen: call onSSC state=" + mServiceState[phoneId]);
-                        ServiceState rawSs = new ServiceState(mServiceState[phoneId]);
+                        if (VDBG) log("listen: call onSSC state=" + mServiceState[r.phoneId]);
+                        ServiceState rawSs = new ServiceState(mServiceState[r.phoneId]);
                         if (checkFineLocationAccess(r, Build.VERSION_CODES.Q)) {
                             r.callback.onServiceStateChanged(rawSs);
                         } else if (checkCoarseLocationAccess(r, Build.VERSION_CODES.Q)) {
@@ -1082,8 +1084,8 @@
                 }
                 if (events.contains(TelephonyCallback.EVENT_SIGNAL_STRENGTH_CHANGED)) {
                     try {
-                        if (mSignalStrength[phoneId] != null) {
-                            int gsmSignalStrength = mSignalStrength[phoneId]
+                        if (mSignalStrength[r.phoneId] != null) {
+                            int gsmSignalStrength = mSignalStrength[r.phoneId]
                                     .getGsmSignalStrength();
                             r.callback.onSignalStrengthChanged((gsmSignalStrength == 99 ? -1
                                     : gsmSignalStrength));
@@ -1096,7 +1098,7 @@
                         TelephonyCallback.EVENT_MESSAGE_WAITING_INDICATOR_CHANGED)) {
                     try {
                         r.callback.onMessageWaitingIndicatorChanged(
-                                mMessageWaiting[phoneId]);
+                                mMessageWaiting[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
@@ -1105,7 +1107,7 @@
                         TelephonyCallback.EVENT_CALL_FORWARDING_INDICATOR_CHANGED)) {
                     try {
                         r.callback.onCallForwardingIndicatorChanged(
-                                mCallForwarding[phoneId]);
+                                mCallForwarding[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
@@ -1113,11 +1115,11 @@
                 if (validateEventAndUserLocked(
                         r, TelephonyCallback.EVENT_CELL_LOCATION_CHANGED)) {
                     try {
-                        if (DBG_LOC) log("listen: mCellIdentity = " + mCellIdentity[phoneId]);
+                        if (DBG_LOC) log("listen: mCellIdentity = " + mCellIdentity[r.phoneId]);
                         if (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE)
                                 && checkFineLocationAccess(r, Build.VERSION_CODES.Q)) {
                             // null will be translated to empty CellLocation object in client.
-                            r.callback.onCellLocationChanged(mCellIdentity[phoneId]);
+                            r.callback.onCellLocationChanged(mCellIdentity[r.phoneId]);
                         }
                     } catch (RemoteException ex) {
                         remove(r.binder);
@@ -1125,38 +1127,38 @@
                 }
                 if (events.contains(TelephonyCallback.EVENT_LEGACY_CALL_STATE_CHANGED)) {
                     try {
-                        r.callback.onLegacyCallStateChanged(mCallState[phoneId],
-                                getCallIncomingNumber(r, phoneId));
+                        r.callback.onLegacyCallStateChanged(mCallState[r.phoneId],
+                                getCallIncomingNumber(r, r.phoneId));
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_CALL_STATE_CHANGED)) {
                     try {
-                        r.callback.onCallStateChanged(mCallState[phoneId]);
+                        r.callback.onCallStateChanged(mCallState[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_DATA_CONNECTION_STATE_CHANGED)) {
                     try {
-                        r.callback.onDataConnectionStateChanged(mDataConnectionState[phoneId],
-                                mDataConnectionNetworkType[phoneId]);
+                        r.callback.onDataConnectionStateChanged(mDataConnectionState[r.phoneId],
+                                mDataConnectionNetworkType[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_DATA_ACTIVITY_CHANGED)) {
                     try {
-                        r.callback.onDataActivity(mDataActivity[phoneId]);
+                        r.callback.onDataActivity(mDataActivity[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_SIGNAL_STRENGTHS_CHANGED)) {
                     try {
-                        if (mSignalStrength[phoneId] != null) {
-                            r.callback.onSignalStrengthsChanged(mSignalStrength[phoneId]);
+                        if (mSignalStrength[r.phoneId] != null) {
+                            r.callback.onSignalStrengthsChanged(mSignalStrength[r.phoneId]);
                         }
                     } catch (RemoteException ex) {
                         remove(r.binder);
@@ -1166,8 +1168,8 @@
                         TelephonyCallback.EVENT_ALWAYS_REPORTED_SIGNAL_STRENGTH_CHANGED)) {
                     updateReportSignalStrengthDecision(r.subId);
                     try {
-                        if (mSignalStrength[phoneId] != null) {
-                            r.callback.onSignalStrengthsChanged(mSignalStrength[phoneId]);
+                        if (mSignalStrength[r.phoneId] != null) {
+                            r.callback.onSignalStrengthsChanged(mSignalStrength[r.phoneId]);
                         }
                     } catch (RemoteException ex) {
                         remove(r.binder);
@@ -1176,11 +1178,13 @@
                 if (validateEventAndUserLocked(
                         r, TelephonyCallback.EVENT_CELL_INFO_CHANGED)) {
                     try {
-                        if (DBG_LOC) log("listen: mCellInfo[" + phoneId + "] = "
-                                + mCellInfo.get(phoneId));
+                        if (DBG_LOC) {
+                            log("listen: mCellInfo[" + r.phoneId + "] = "
+                                    + mCellInfo.get(r.phoneId));
+                        }
                         if (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE)
                                 && checkFineLocationAccess(r, Build.VERSION_CODES.Q)) {
-                            r.callback.onCellInfoChanged(mCellInfo.get(phoneId));
+                            r.callback.onCellInfoChanged(mCellInfo.get(r.phoneId));
                         }
                     } catch (RemoteException ex) {
                         remove(r.binder);
@@ -1188,22 +1192,22 @@
                 }
                 if (events.contains(TelephonyCallback.EVENT_PRECISE_CALL_STATE_CHANGED)) {
                     try {
-                        r.callback.onPreciseCallStateChanged(mPreciseCallState[phoneId]);
+                        r.callback.onPreciseCallStateChanged(mPreciseCallState[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_CALL_DISCONNECT_CAUSE_CHANGED)) {
                     try {
-                        r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[phoneId],
-                                mCallPreciseDisconnectCause[phoneId]);
+                        r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[r.phoneId],
+                                mCallPreciseDisconnectCause[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_IMS_CALL_DISCONNECT_CAUSE_CHANGED)) {
                     try {
-                        r.callback.onImsCallDisconnectCauseChanged(mImsReasonInfo.get(phoneId));
+                        r.callback.onImsCallDisconnectCauseChanged(mImsReasonInfo.get(r.phoneId));
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
@@ -1212,7 +1216,7 @@
                         TelephonyCallback.EVENT_PRECISE_DATA_CONNECTION_STATE_CHANGED)) {
                     try {
                         for (PreciseDataConnectionState pdcs
-                                : mPreciseDataConnectionStates.get(phoneId).values()) {
+                                : mPreciseDataConnectionStates.get(r.phoneId).values()) {
                             r.callback.onPreciseDataConnectionStateChanged(pdcs);
                         }
                     } catch (RemoteException ex) {
@@ -1229,29 +1233,29 @@
                 if (events.contains(TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED)) {
                     try {
                         r.callback.onVoiceActivationStateChanged(
-                                mVoiceActivationState[phoneId]);
+                                mVoiceActivationState[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_DATA_ACTIVATION_STATE_CHANGED)) {
                     try {
-                        r.callback.onDataActivationStateChanged(mDataActivationState[phoneId]);
+                        r.callback.onDataActivationStateChanged(mDataActivationState[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_USER_MOBILE_DATA_STATE_CHANGED)) {
                     try {
-                        r.callback.onUserMobileDataStateChanged(mUserMobileDataState[phoneId]);
+                        r.callback.onUserMobileDataStateChanged(mUserMobileDataState[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_DISPLAY_INFO_CHANGED)) {
                     try {
-                        if (mTelephonyDisplayInfos[phoneId] != null) {
-                            r.callback.onDisplayInfoChanged(mTelephonyDisplayInfos[phoneId]);
+                        if (mTelephonyDisplayInfos[r.phoneId] != null) {
+                            r.callback.onDisplayInfoChanged(mTelephonyDisplayInfos[r.phoneId]);
                         }
                     } catch (RemoteException ex) {
                         remove(r.binder);
@@ -1288,20 +1292,20 @@
                 }
                 if (events.contains(TelephonyCallback.EVENT_SRVCC_STATE_CHANGED)) {
                     try {
-                        r.callback.onSrvccStateChanged(mSrvccState[phoneId]);
+                        r.callback.onSrvccStateChanged(mSrvccState[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)) {
                     try {
-                        r.callback.onCallAttributesChanged(mCallAttributes[phoneId]);
+                        r.callback.onCallAttributesChanged(mCallAttributes[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
                 }
                 if (events.contains(TelephonyCallback.EVENT_BARRING_INFO_CHANGED)) {
-                    BarringInfo barringInfo = mBarringInfo.get(phoneId);
+                    BarringInfo barringInfo = mBarringInfo.get(r.phoneId);
                     BarringInfo biNoLocation = barringInfo != null
                             ? barringInfo.createLocationInfoSanitizedCopy() : null;
                     if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
@@ -1319,8 +1323,8 @@
                         r.callback.onPhysicalChannelConfigChanged(
                                 shouldSanitizeLocationForPhysicalChannelConfig(r)
                                         ? getLocationSanitizedConfigs(
-                                                mPhysicalChannelConfigs.get(phoneId))
-                                        : mPhysicalChannelConfigs.get(phoneId));
+                                                mPhysicalChannelConfigs.get(r.phoneId))
+                                        : mPhysicalChannelConfigs.get(r.phoneId));
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
@@ -1329,7 +1333,7 @@
                         TelephonyCallback.EVENT_DATA_ENABLED_CHANGED)) {
                     try {
                         r.callback.onDataEnabledChanged(
-                                mIsDataEnabled[phoneId], mDataEnabledReason[phoneId]);
+                                mIsDataEnabled[r.phoneId], mDataEnabledReason[r.phoneId]);
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
@@ -1337,9 +1341,9 @@
                 if (events.contains(
                         TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED)) {
                     try {
-                        if (mLinkCapacityEstimateLists.get(phoneId) != null) {
+                        if (mLinkCapacityEstimateLists.get(r.phoneId) != null) {
                             r.callback.onLinkCapacityEstimateChanged(mLinkCapacityEstimateLists
-                                    .get(phoneId));
+                                    .get(r.phoneId));
                         }
                     } catch (RemoteException ex) {
                         remove(r.binder);
@@ -1581,7 +1585,7 @@
                     }
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_SERVICE_STATE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
 
                         try {
                             ServiceState stateToSend;
@@ -1643,7 +1647,7 @@
                         if ((activationType == SIM_ACTIVATION_TYPE_VOICE)
                                 && r.matchTelephonyCallbackEvent(
                                         TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED)
-                                && idMatch(r.subId, subId, phoneId)) {
+                                && idMatch(r, subId, phoneId)) {
                             if (DBG) {
                                 log("notifyVoiceActivationStateForPhoneId: callback.onVASC r=" + r
                                         + " subId=" + subId + " phoneId=" + phoneId
@@ -1654,7 +1658,7 @@
                         if ((activationType == SIM_ACTIVATION_TYPE_DATA)
                                 && r.matchTelephonyCallbackEvent(
                                         TelephonyCallback.EVENT_DATA_ACTIVATION_STATE_CHANGED)
-                                && idMatch(r.subId, subId, phoneId)) {
+                                && idMatch(r, subId, phoneId)) {
                             if (DBG) {
                                 log("notifyDataActivationStateForPhoneId: callback.onDASC r=" + r
                                         + " subId=" + subId + " phoneId=" + phoneId
@@ -1696,7 +1700,7 @@
                             TelephonyCallback.EVENT_SIGNAL_STRENGTHS_CHANGED)
                             || r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_ALWAYS_REPORTED_SIGNAL_STRENGTH_CHANGED))
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             if (DBG) {
                                 log("notifySignalStrengthForPhoneId: callback.onSsS r=" + r
@@ -1710,7 +1714,7 @@
                     }
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_SIGNAL_STRENGTH_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             int gsmSignalStrength = signalStrength.getGsmSignalStrength();
                             int ss = (gsmSignalStrength == 99 ? -1 : gsmSignalStrength);
@@ -1757,7 +1761,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_CARRIER_NETWORK_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onCarrierNetworkChange(active);
                         } catch (RemoteException ex) {
@@ -1789,7 +1793,7 @@
                 for (Record r : mRecords) {
                     if (validateEventAndUserLocked(
                             r, TelephonyCallback.EVENT_CELL_INFO_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)
+                            && idMatch(r, subId, phoneId)
                             && (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE)
                                     && checkFineLocationAccess(r, Build.VERSION_CODES.Q))) {
                         try {
@@ -1823,7 +1827,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_MESSAGE_WAITING_INDICATOR_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onMessageWaitingIndicatorChanged(mwi);
                         } catch (RemoteException ex) {
@@ -1850,7 +1854,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_USER_MOBILE_DATA_STATE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onUserMobileDataStateChanged(state);
                         } catch (RemoteException ex) {
@@ -1889,7 +1893,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_DISPLAY_INFO_CHANGED)
-                            && idMatchWithoutDefaultPhoneCheck(r.subId, subId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             if (!mConfigurationProvider.isDisplayInfoNrAdvancedSupported(
                                     r.callingPackage, Binder.getCallingUserHandle())) {
@@ -1941,7 +1945,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_CALL_FORWARDING_INDICATOR_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onCallForwardingIndicatorChanged(cfi);
                         } catch (RemoteException ex) {
@@ -1970,7 +1974,7 @@
                     // Notify by correct subId.
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_DATA_ACTIVITY_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onDataActivity(state);
                         } catch (RemoteException ex) {
@@ -2018,7 +2022,7 @@
                     for (Record r : mRecords) {
                         if (r.matchTelephonyCallbackEvent(
                                 TelephonyCallback.EVENT_DATA_CONNECTION_STATE_CHANGED)
-                                && idMatch(r.subId, subId, phoneId)) {
+                                && idMatch(r, subId, phoneId)) {
                             try {
                                 if (DBG) {
                                     log("Notify data connection state changed on sub: " + subId);
@@ -2043,7 +2047,7 @@
                     for (Record r : mRecords) {
                         if (r.matchTelephonyCallbackEvent(
                                 TelephonyCallback.EVENT_PRECISE_DATA_CONNECTION_STATE_CHANGED)
-                                && idMatch(r.subId, subId, phoneId)) {
+                                && idMatch(r, subId, phoneId)) {
                             try {
                                 r.callback.onPreciseDataConnectionStateChanged(preciseState);
                             } catch (RemoteException ex) {
@@ -2090,7 +2094,7 @@
                 for (Record r : mRecords) {
                     if (validateEventAndUserLocked(
                             r, TelephonyCallback.EVENT_CELL_LOCATION_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)
+                            && idMatch(r, subId, phoneId)
                             && (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE)
                                     && checkFineLocationAccess(r, Build.VERSION_CODES.Q))) {
                         try {
@@ -2144,7 +2148,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_PRECISE_CALL_STATE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onPreciseCallStateChanged(mPreciseCallState[phoneId]);
                         } catch (RemoteException ex) {
@@ -2153,7 +2157,7 @@
                     }
                     if (notifyCallAttributes && r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onCallAttributesChanged(mCallAttributes[phoneId]);
                         } catch (RemoteException ex) {
@@ -2178,7 +2182,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_CALL_DISCONNECT_CAUSE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[phoneId],
                                     mCallPreciseDisconnectCause[phoneId]);
@@ -2203,7 +2207,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_IMS_CALL_DISCONNECT_CAUSE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             if (DBG_LOC) {
                                 log("notifyImsCallDisconnectCause: mImsReasonInfo="
@@ -2235,7 +2239,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_SRVCC_STATE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             if (DBG_LOC) {
                                 log("notifySrvccStateChanged: mSrvccState=" + state + " r=" + r);
@@ -2264,7 +2268,7 @@
                     }
                     if ((r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_OEM_HOOK_RAW))
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onOemHookRawEvent(rawData);
                         } catch (RemoteException ex) {
@@ -2344,7 +2348,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_RADIO_POWER_STATE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onRadioPowerStateChanged(state);
                         } catch (RemoteException ex) {
@@ -2373,7 +2377,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_EMERGENCY_NUMBER_LIST_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onEmergencyNumberListChanged(mEmergencyNumberList);
                             if (VDBG) {
@@ -2460,7 +2464,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onCallAttributesChanged(mCallAttributes[phoneId]);
                         } catch (RemoteException ex) {
@@ -2491,7 +2495,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_REGISTRATION_FAILURE)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onRegistrationFailed(
                                     checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
@@ -2534,7 +2538,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_BARRING_INFO_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             if (DBG_LOC) {
                                 log("notifyBarringInfo: mBarringInfo="
@@ -2579,7 +2583,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_PHYSICAL_CHANNEL_CONFIG_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             if (DBG_LOC) {
                                 log("notifyPhysicalChannelConfig: mPhysicalChannelConfigs="
@@ -2646,7 +2650,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_DATA_ENABLED_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onDataEnabledChanged(enabled, reason);
                         } catch (RemoteException ex) {
@@ -2681,7 +2685,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             if (VDBG) {
                                 log("notifyAllowedNetworkTypesChanged: reason= " + reason
@@ -2723,7 +2727,7 @@
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED)
-                            && idMatch(r.subId, subId, phoneId)) {
+                            && idMatch(r, subId, phoneId)) {
                         try {
                             r.callback.onLinkCapacityEstimateChanged(linkCapacityEstimateList);
                         } catch (RemoteException ex) {
@@ -3173,33 +3177,24 @@
     }
 
     /**
-     * If the registrant specified a subId, then we should only notify it if subIds match.
-     * If the registrant registered with DEFAULT subId, we should notify only when the related subId
-     * is default subId (which could be INVALID if there's no default subId).
+     * Match the sub id or phone id of the event to the record
      *
-     * This should be the correct way to check record ID match. in idMatch the record's phoneId is
-     * speculated based on subId passed by the registrant so it's not a good reference.
-     * But to avoid triggering potential regression only replace idMatch with it when an issue with
-     * idMatch is reported. Eventually this should replace all instances of idMatch.
+     * We follow the rules below:
+     * 1) If sub id of the event is invalid, phone id should be used.
+     * 2) The event on default sub should be notified to the records
+     * which register the default sub id.
+     * 3) Sub id should be exactly matched for all other cases.
      */
-    private boolean idMatchWithoutDefaultPhoneCheck(int subIdInRecord, int subIdToNotify) {
-        if (subIdInRecord == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
-            return (subIdToNotify == mDefaultSubId);
-        } else {
-            return (subIdInRecord == subIdToNotify);
-        }
-    }
+    boolean idMatch(Record r, int subId, int phoneId) {
 
-    boolean idMatch(int rSubId, int subId, int phoneId) {
-
-        if(subId < 0) {
-            // Invalid case, we need compare phoneId with default one.
-            return (mDefaultPhoneId == phoneId);
+        if (subId < 0) {
+            // Invalid case, we need compare phoneId.
+            return (r.phoneId == phoneId);
         }
-        if(rSubId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+        if (r.subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
             return (subId == mDefaultSubId);
         } else {
-            return (rSubId == subId);
+            return (r.subId == subId);
         }
     }
 
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 85eadf5..81627a0 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -1167,11 +1167,16 @@
     }
 
     private boolean doesPackageHaveCallingUid(@NonNull String packageName) {
+        int callingUid = mInjector.getCallingUid();
+        int callingUserId = UserHandle.getUserId(callingUid);
+        final long ident = Binder.clearCallingIdentity();
         try {
-            return getContext().getPackageManager().getPackageUid(packageName, 0)
-                    == mInjector.getCallingUid();
+            return getContext().getPackageManager().getPackageUidAsUser(packageName,
+                    callingUserId) == callingUid;
         } catch (PackageManager.NameNotFoundException e) {
             return false;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index fcd049f..d10ab8e 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -142,6 +142,8 @@
     );
 
     public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] {
+            "android.hardware.biometrics.face.IFace/",
+            "android.hardware.biometrics.fingerprint.IFingerprint/",
             "android.hardware.light.ILights/",
             "android.hardware.power.stats.IPowerStats/",
     };
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c911257..d3cad7f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1550,6 +1550,13 @@
     private static final int INDEX_TOTAL_MEMTRACK_GL = 14;
     private static final int INDEX_LAST = 15;
 
+    /**
+     * Used to notify activity lifecycle events.
+     */
+    @Nullable
+    volatile ActivityManagerInternal.VoiceInteractionManagerProvider
+            mVoiceInteractionManagerProvider;
+
     final class UiHandler extends Handler {
         public UiHandler() {
             super(com.android.server.UiThread.get().getLooper(), null, true);
@@ -1886,6 +1893,14 @@
         return mAppOpsService;
     }
 
+    /**
+     * Sets the internal voice interaction manager service.
+     */
+    private void setVoiceInteractionManagerProvider(
+            @Nullable ActivityManagerInternal.VoiceInteractionManagerProvider provider) {
+        mVoiceInteractionManagerProvider = provider;
+    }
+
     static class MemBinder extends Binder {
         ActivityManagerService mActivityManagerService;
         private final PriorityDump.PriorityDumper mPriorityDumper =
@@ -2737,6 +2752,11 @@
                 || event == Event.ACTIVITY_DESTROYED)) {
             contentCaptureService.notifyActivityEvent(userId, activity, event);
         }
+        // TODO(b/201234353): Move the logic to client side.
+        if (mVoiceInteractionManagerProvider != null && (event == Event.ACTIVITY_PAUSED
+                || event == Event.ACTIVITY_RESUMED || event == Event.ACTIVITY_STOPPED)) {
+            mVoiceInteractionManagerProvider.notifyActivityEventChanged();
+        }
     }
 
     /**
@@ -16366,6 +16386,21 @@
                 return mProcessList.getIsolatedProcessesLocked(uid);
             }
         }
+
+        /** @see ActivityManagerService#sendIntentSender */
+        @Override
+        public int sendIntentSender(IIntentSender target, IBinder allowlistToken, int code,
+                Intent intent, String resolvedType,
+                IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+            return ActivityManagerService.this.sendIntentSender(target, allowlistToken, code,
+                    intent, resolvedType, finishedReceiver, requiredPermission, options);
+        }
+
+        @Override
+        public void setVoiceInteractionManagerProvider(
+                @Nullable ActivityManagerInternal.VoiceInteractionManagerProvider provider) {
+            ActivityManagerService.this.setVoiceInteractionManagerProvider(provider);
+        }
     }
 
     long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) {
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 36c0de9..ad0485b 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -1033,6 +1033,7 @@
                     mService.setProcessTrackerStateLOSP(app, trackerMemFactor, now);
                     state.setProcStateChanged(false);
                 }
+                trimMemoryUiHiddenIfNecessaryLSP(app);
                 if (curProcState >= ActivityManager.PROCESS_STATE_HOME && !app.isKilledByAm()) {
                     if (trimMemoryLevel < curLevel[0] && (thread = app.getThread()) != null) {
                         try {
@@ -1075,24 +1076,6 @@
                     }
                     profile.setTrimMemoryLevel(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND);
                 } else {
-                    if ((curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
-                                || state.isSystemNoUi()) && profile.hasPendingUiClean()) {
-                        // If this application is now in the background and it
-                        // had done UI, then give it the special trim level to
-                        // have it free UI resources.
-                        final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
-                        if (trimMemoryLevel < level && (thread = app.getThread()) != null) {
-                            try {
-                                if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
-                                    Slog.v(TAG_OOM_ADJ, "Trimming memory of bg-ui "
-                                            + app.processName + " to " + level);
-                                }
-                                thread.scheduleTrimMemory(level);
-                            } catch (RemoteException e) {
-                            }
-                        }
-                        profile.setPendingUiClean(false);
-                    }
                     if (trimMemoryLevel < fgTrimLevel && (thread = app.getThread()) != null) {
                         try {
                             if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
@@ -1119,28 +1102,36 @@
                     mService.setProcessTrackerStateLOSP(app, trackerMemFactor, now);
                     state.setProcStateChanged(false);
                 }
-                if ((state.getCurProcState() >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
-                            || state.isSystemNoUi()) && profile.hasPendingUiClean()) {
-                    if (profile.getTrimMemoryLevel() < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
-                            && (thread = app.getThread()) != null) {
-                        try {
-                            if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
-                                Slog.v(TAG_OOM_ADJ,
-                                        "Trimming memory of ui hidden " + app.processName
-                                        + " to " + ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
-                            }
-                            thread.scheduleTrimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
-                        } catch (RemoteException e) {
-                        }
-                    }
-                    profile.setPendingUiClean(false);
-                }
+                trimMemoryUiHiddenIfNecessaryLSP(app);
                 profile.setTrimMemoryLevel(0);
             });
         }
         return allChanged;
     }
 
+    @GuardedBy({"mService", "mProcLock"})
+    private void trimMemoryUiHiddenIfNecessaryLSP(ProcessRecord app) {
+        if ((app.mState.getCurProcState() >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
+                || app.mState.isSystemNoUi()) && app.mProfile.hasPendingUiClean()) {
+            // If this application is now in the background and it
+            // had done UI, then give it the special trim level to
+            // have it free UI resources.
+            final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
+            IApplicationThread thread;
+            if (app.mProfile.getTrimMemoryLevel() < level && (thread = app.getThread()) != null) {
+                try {
+                    if (DEBUG_SWITCH || DEBUG_OOM_ADJ) {
+                        Slog.v(TAG_OOM_ADJ, "Trimming memory of bg-ui "
+                                + app.processName + " to " + level);
+                    }
+                    thread.scheduleTrimMemory(level);
+                } catch (RemoteException e) {
+                }
+            }
+            app.mProfile.setPendingUiClean(false);
+        }
+    }
+
     @GuardedBy("mProcLock")
     long getLowRamTimeSinceIdleLPr(long now) {
         return mLowRamTimeSinceLastIdle + (mLowRamStartTime > 0 ? (now - mLowRamStartTime) : 0);
diff --git a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
index 7ba032f..cf4c8a3 100644
--- a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
@@ -114,6 +114,9 @@
     private int mScreenState;
 
     @GuardedBy("this")
+    private int[] mPerDisplayScreenStates = null;
+
+    @GuardedBy("this")
     private boolean mUseLatestStates = true;
 
     @GuardedBy("this")
@@ -291,8 +294,8 @@
     }
 
     @Override
-    public Future<?> scheduleSyncDueToScreenStateChange(
-            int flags, boolean onBattery, boolean onBatteryScreenOff, int screenState) {
+    public Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
+            boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
         synchronized (BatteryExternalStatsWorker.this) {
             if (mCurrentFuture == null || (mUpdateFlags & UPDATE_CPU) == 0) {
                 mOnBattery = onBattery;
@@ -301,6 +304,7 @@
             }
             // always update screen state
             mScreenState = screenState;
+            mPerDisplayScreenStates = perDisplayScreenStates;
             return scheduleSyncLocked("screen-state", flags);
         }
     }
@@ -432,6 +436,7 @@
             final boolean onBattery;
             final boolean onBatteryScreenOff;
             final int screenState;
+            final int[] displayScreenStates;
             final boolean useLatestStates;
             synchronized (BatteryExternalStatsWorker.this) {
                 updateFlags = mUpdateFlags;
@@ -440,6 +445,7 @@
                 onBattery = mOnBattery;
                 onBatteryScreenOff = mOnBatteryScreenOff;
                 screenState = mScreenState;
+                displayScreenStates = mPerDisplayScreenStates;
                 useLatestStates = mUseLatestStates;
                 mUpdateFlags = 0;
                 mCurrentReason = null;
@@ -461,7 +467,8 @@
                     }
                     try {
                         updateExternalStatsLocked(reason, updateFlags, onBattery,
-                                onBatteryScreenOff, screenState, useLatestStates);
+                                onBatteryScreenOff, screenState, displayScreenStates,
+                                useLatestStates);
                     } finally {
                         if (DEBUG) {
                             Slog.d(TAG, "end updateExternalStatsSync");
@@ -506,7 +513,8 @@
 
     @GuardedBy("mWorkerLock")
     private void updateExternalStatsLocked(final String reason, int updateFlags, boolean onBattery,
-            boolean onBatteryScreenOff, int screenState, boolean useLatestStates) {
+            boolean onBatteryScreenOff, int screenState, int[] displayScreenStates,
+            boolean useLatestStates) {
         // We will request data from external processes asynchronously, and wait on a timeout.
         SynchronousResultReceiver wifiReceiver = null;
         SynchronousResultReceiver bluetoothReceiver = null;
@@ -659,11 +667,12 @@
 
             // Inform mStats about each applicable measured energy (unless addressed elsewhere).
             if (measuredEnergyDeltas != null) {
-                final long displayChargeUC = measuredEnergyDeltas.displayChargeUC;
-                if (displayChargeUC != MeasuredEnergySnapshot.UNAVAILABLE) {
-                    // If updating, pass in what BatteryExternalStatsWorker thinks screenState is.
-                    mStats.updateDisplayMeasuredEnergyStatsLocked(displayChargeUC, screenState,
-                            elapsedRealtime);
+                final long[] displayChargeUC = measuredEnergyDeltas.displayChargeUC;
+                if (displayChargeUC != null && displayChargeUC.length > 0) {
+                    // If updating, pass in what BatteryExternalStatsWorker thinks
+                    // displayScreenStates is.
+                    mStats.updateDisplayMeasuredEnergyStatsLocked(displayChargeUC,
+                            displayScreenStates, elapsedRealtime);
                 }
 
                 final long gnssChargeUC = measuredEnergyDeltas.gnssChargeUC;
@@ -948,6 +957,7 @@
                 switch (consumer.type) {
                     case EnergyConsumerType.OTHER:
                     case EnergyConsumerType.CPU_CLUSTER:
+                    case EnergyConsumerType.DISPLAY:
                         break;
                     default:
                         Slog.w(TAG, "EnergyConsumer '" + consumer.name + "' has unexpected ordinal "
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 9a349ef..cd9eb90 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -1215,7 +1215,7 @@
             mHandler.post(() -> {
                 if (DBG) Slog.d(TAG, "begin noteScreenState");
                 synchronized (mStats) {
-                    mStats.noteScreenStateLocked(state, elapsedRealtime, uptime, currentTime);
+                    mStats.noteScreenStateLocked(0, state, elapsedRealtime, uptime, currentTime);
                 }
                 if (DBG) Slog.d(TAG, "end noteScreenState");
             });
@@ -1230,7 +1230,7 @@
             final long uptime = SystemClock.uptimeMillis();
             mHandler.post(() -> {
                 synchronized (mStats) {
-                    mStats.noteScreenBrightnessLocked(brightness, elapsedRealtime, uptime);
+                    mStats.noteScreenBrightnessLocked(0, brightness, elapsedRealtime, uptime);
                 }
             });
         }
diff --git a/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java b/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java
index a9fca4f..0359aa5 100644
--- a/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java
+++ b/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java
@@ -49,6 +49,9 @@
     /** Number of ordinals for {@link EnergyConsumerType#CPU_CLUSTER}. */
     private final int mNumCpuClusterOrdinals;
 
+    /** Number of ordinals for {@link EnergyConsumerType#DISPLAY}. */
+    private final int mNumDisplayOrdinals;
+
     /** Number of ordinals for {@link EnergyConsumerType#OTHER}. */
     private final int mNumOtherOrdinals;
 
@@ -95,6 +98,7 @@
 
         mNumCpuClusterOrdinals = calculateNumOrdinals(EnergyConsumerType.CPU_CLUSTER,
                 idToConsumerMap);
+        mNumDisplayOrdinals = calculateNumOrdinals(EnergyConsumerType.DISPLAY, idToConsumerMap);
         mNumOtherOrdinals = calculateNumOrdinals(EnergyConsumerType.OTHER, idToConsumerMap);
         mAttributionSnapshots = new SparseArray<>(mNumOtherOrdinals);
     }
@@ -108,7 +112,7 @@
         public long[] cpuClusterChargeUC = null;
 
         /** The chargeUC for {@link EnergyConsumerType#DISPLAY}. */
-        public long displayChargeUC = UNAVAILABLE;
+        public long[] displayChargeUC = null;
 
         /** The chargeUC for {@link EnergyConsumerType#GNSS}. */
         public long gnssChargeUC = UNAVAILABLE;
@@ -212,7 +216,10 @@
                     break;
 
                 case EnergyConsumerType.DISPLAY:
-                    output.displayChargeUC = deltaChargeUC;
+                    if (output.displayChargeUC == null) {
+                        output.displayChargeUC = new long[mNumDisplayOrdinals];
+                    }
+                    output.displayChargeUC[ordinal]  = deltaChargeUC;
                     break;
 
                 case EnergyConsumerType.GNSS:
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1876397..4adfd4e 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -93,11 +93,16 @@
 import android.media.IPlaybackConfigDispatcher;
 import android.media.IRecordingConfigDispatcher;
 import android.media.IRingtonePlayer;
+import android.media.ISpatializerCallback;
+import android.media.ISpatializerHeadToSoundStagePoseCallback;
+import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.ISpatializerOutputCallback;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.IVolumeController;
 import android.media.MediaMetrics;
 import android.media.MediaRecorder.AudioSource;
 import android.media.PlayerBase;
+import android.media.Spatializer;
 import android.media.VolumePolicy;
 import android.media.audiofx.AudioEffect;
 import android.media.audiopolicy.AudioMix;
@@ -198,7 +203,8 @@
  */
 public class AudioService extends IAudioService.Stub
         implements AccessibilityManager.TouchExplorationStateChangeListener,
-            AccessibilityManager.AccessibilityServicesStateChangeListener {
+            AccessibilityManager.AccessibilityServicesStateChangeListener,
+            AudioSystemAdapter.OnRoutingUpdatedListener {
 
     private static final String TAG = "AS.AudioService";
 
@@ -238,7 +244,7 @@
      */
     private static final int FLAG_ADJUST_VOLUME = 1;
 
-    private final Context mContext;
+    final Context mContext;
     private final ContentResolver mContentResolver;
     private final AppOpsManager mAppOps;
 
@@ -312,12 +318,16 @@
     private static final int MSG_SET_A2DP_DEV_CONNECTION_STATE = 38;
     private static final int MSG_A2DP_DEV_CONFIG_CHANGE = 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;
+    private static final int MSG_PERSIST_SPATIAL_AUDIO_ENABLED = 43;
 
     // start of messages handled under wakelock
     //   these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
     //   and not with sendMsg(..., ..., SENDMSG_QUEUE, ...)
     private static final int MSG_DISABLE_AUDIO_FOR_UID = 100;
     private static final int MSG_INIT_STREAMS_VOLUMES = 101;
+    private static final int MSG_INIT_SPATIALIZER = 102;
     // end of messages handled under wakelock
 
     // retry delay in case of failure to indicate system ready to AudioFlinger
@@ -871,6 +881,8 @@
 
         mSfxHelper = new SoundEffectsHelper(mContext);
 
+        mSpatializerHelper = new SpatializerHelper(this, mAudioSystem);
+
         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
         mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator();
 
@@ -1032,9 +1044,13 @@
 
         mMonitorRotation = SystemProperties.getBoolean("ro.audio.monitorRotation", false);
 
+        mHasSpatializerEffect = SystemProperties.getBoolean("ro.audio.spatializer_enabled", false);
+
         // done with service initialization, continue additional work in our Handler thread
         queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_STREAMS_VOLUMES,
                 0 /* arg1 */,  0 /* arg2 */, null /* obj */,  0 /* delay */);
+        queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_SPATIALIZER,
+                0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
     }
 
     /**
@@ -1224,6 +1240,25 @@
         updateVibratorInfos();
     }
 
+    //-----------------------------------------------------------------
+    // routing monitoring from AudioSystemAdapter
+    @Override
+    public void onRoutingUpdatedFromNative() {
+        if (!mHasSpatializerEffect) {
+            return;
+        }
+        sendMsg(mAudioHandler,
+                MSG_ROUTING_UPDATED,
+                SENDMSG_REPLACE, 0, 0, null,
+                /*delay*/ 0);
+    }
+
+    void monitorRoutingChanges(boolean enabled) {
+        mAudioSystem.setRoutingListener(enabled ? this : null);
+    }
+
+
+    //-----------------------------------------------------------------
     RoleObserver mRoleObserver;
 
     class RoleObserver implements OnRoleHoldersChangedListener {
@@ -1408,6 +1443,11 @@
             }
         }
 
+        if (mHasSpatializerEffect) {
+            mSpatializerHelper.reset(/* featureEnabled */ isSpatialAudioEnabled());
+            monitorRoutingChanges(true);
+        }
+
         onIndicateSystemReady();
         // indicate the end of reconfiguration phase to audio HAL
         AudioSystem.setParameters("restarting=false");
@@ -7593,6 +7633,19 @@
                     mAudioEventWakeLock.release();
                     break;
 
+                case MSG_INIT_SPATIALIZER:
+                    mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect);
+                    if (mHasSpatializerEffect) {
+                        mSpatializerHelper.setFeatureEnabled(isSpatialAudioEnabled());
+                        monitorRoutingChanges(true);
+                    }
+                    mAudioEventWakeLock.release();
+                    break;
+
+                case MSG_INIT_HEADTRACKING_SENSORS:
+                    mSpatializerHelper.onInitSensors(/*init*/ msg.arg1 == 1);
+                    break;
+
                 case MSG_CHECK_MUSIC_ACTIVE:
                     onCheckMusicActive((String) msg.obj);
                     break;
@@ -7725,6 +7778,14 @@
                 case MSG_DISPATCH_AUDIO_MODE:
                     dispatchMode(msg.arg1);
                     break;
+
+                case MSG_ROUTING_UPDATED:
+                    mSpatializerHelper.onRoutingUpdated();
+                    break;
+
+                case MSG_PERSIST_SPATIAL_AUDIO_ENABLED:
+                    onPersistSpatialAudioEnabled(msg.arg1 == 1);
+                    break;
             }
         }
     }
@@ -8293,6 +8354,240 @@
     }
 
     //==========================================================================================
+    private final @NonNull SpatializerHelper mSpatializerHelper;
+    /**
+     * Initialized from property ro.audio.spatializer_enabled
+     * Should only be 1 when the device ships with a Spatializer effect
+     */
+    private final boolean mHasSpatializerEffect;
+    /**
+     * Default value for the spatial audio feature
+     */
+    private static final boolean SPATIAL_AUDIO_ENABLED_DEFAULT = true;
+
+    /**
+     * persist in user settings whether the feature is enabled.
+     * Can change when {@link Spatializer#setEnabled(boolean)} is called and successfully
+     * changes the state of the feature
+     * @param featureEnabled
+     */
+    void persistSpatialAudioEnabled(boolean featureEnabled) {
+        sendMsg(mAudioHandler,
+                MSG_PERSIST_SPATIAL_AUDIO_ENABLED,
+                SENDMSG_REPLACE, featureEnabled ? 1 : 0, 0, null,
+                /*delay ms*/ 100);
+    }
+
+    void onPersistSpatialAudioEnabled(boolean enabled) {
+        Settings.Secure.putIntForUser(mContentResolver,
+                Settings.Secure.SPATIAL_AUDIO_ENABLED, enabled ? 1 : 0,
+                UserHandle.USER_CURRENT);
+    }
+
+    boolean isSpatialAudioEnabled() {
+        return Settings.Secure.getIntForUser(mContentResolver,
+                Settings.Secure.SPATIAL_AUDIO_ENABLED, SPATIAL_AUDIO_ENABLED_DEFAULT ? 1 : 0,
+                UserHandle.USER_CURRENT) == 1;
+    }
+
+    private void enforceModifyDefaultAudioEffectsPermission() {
+        if (mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Missing MODIFY_DEFAULT_AUDIO_EFFECTS permission");
+        }
+    }
+
+    /**
+     * Returns the immersive audio level that the platform is capable of
+     * @see Spatializer#getImmersiveAudioLevel()
+     */
+    public int getSpatializerImmersiveAudioLevel() {
+        return mSpatializerHelper.getCapableImmersiveAudioLevel();
+    }
+
+    /** @see Spatializer#isEnabled() */
+    public boolean isSpatializerEnabled() {
+        return mSpatializerHelper.isEnabled();
+    }
+
+    /** @see Spatializer#isAvailable() */
+    public boolean isSpatializerAvailable() {
+        return mSpatializerHelper.isAvailable();
+    }
+
+    /** @see Spatializer#setSpatializerEnabled(boolean) */
+    public void setSpatializerEnabled(boolean enabled) {
+        enforceModifyDefaultAudioEffectsPermission();
+        mSpatializerHelper.setFeatureEnabled(enabled);
+    }
+
+    /** @see Spatializer#canBeSpatialized() */
+    public boolean canBeSpatialized(
+            @NonNull AudioAttributes attributes, @NonNull AudioFormat format) {
+        Objects.requireNonNull(attributes);
+        Objects.requireNonNull(format);
+        return mSpatializerHelper.canBeSpatialized(attributes, format);
+    }
+
+    /** @see Spatializer.SpatializerInfoDispatcherStub */
+    public void registerSpatializerCallback(
+            @NonNull ISpatializerCallback cb) {
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerStateCallback(cb);
+    }
+
+    /** @see Spatializer.SpatializerInfoDispatcherStub */
+    public void unregisterSpatializerCallback(
+            @NonNull ISpatializerCallback cb) {
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterStateCallback(cb);
+    }
+
+    /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */
+    public void registerSpatializerHeadTrackingCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerHeadTrackingModeCallback(cb);
+    }
+
+    /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */
+    public void unregisterSpatializerHeadTrackingCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterHeadTrackingModeCallback(cb);
+    }
+
+    /** @see Spatializer#setOnHeadToSoundstagePoseUpdatedListener */
+    public void registerHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerHeadToSoundstagePoseCallback(cb);
+    }
+
+    /** @see Spatializer#clearOnHeadToSoundstagePoseUpdatedListener */
+    public void unregisterHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterHeadToSoundstagePoseCallback(cb);
+    }
+
+    /** @see Spatializer#getSpatializerCompatibleAudioDevices() */
+    public @NonNull List<AudioDeviceAttributes> getSpatializerCompatibleAudioDevices() {
+        enforceModifyAudioRoutingPermission();
+        return mSpatializerHelper.getCompatibleAudioDevices();
+    }
+
+    /** @see Spatializer#addSpatializerCompatibleAudioDevice(AudioDeviceAttributes) */
+    public void addSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(ada);
+        mSpatializerHelper.addCompatibleAudioDevice(ada);
+    }
+
+    /** @see Spatializer#removeSpatializerCompatibleAudioDevice(AudioDeviceAttributes) */
+    public void removeSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(ada);
+        mSpatializerHelper.removeCompatibleAudioDevice(ada);
+    }
+
+    /** @see Spatializer#getSupportedHeadTrackingModes() */
+    public int[] getSupportedHeadTrackingModes() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getSupportedHeadTrackingModes();
+    }
+
+    /** @see Spatializer#getHeadTrackingMode() */
+    public int getActualHeadTrackingMode() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getActualHeadTrackingMode();
+    }
+
+    /** @see Spatializer#getDesiredHeadTrackingMode() */
+    public int getDesiredHeadTrackingMode() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getDesiredHeadTrackingMode();
+    }
+
+    /** @see Spatializer#setGlobalTransform */
+    public void setSpatializerGlobalTransform(@NonNull float[] transform) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(transform);
+        mSpatializerHelper.setGlobalTransform(transform);
+    }
+
+    /** @see Spatializer#recenterHeadTracker() */
+    public void recenterHeadTracker() {
+        enforceModifyDefaultAudioEffectsPermission();
+        mSpatializerHelper.recenterHeadTracker();
+    }
+
+    /** @see Spatializer#setDesiredHeadTrackingMode */
+    public void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) {
+        enforceModifyDefaultAudioEffectsPermission();
+        switch(mode) {
+            case Spatializer.HEAD_TRACKING_MODE_DISABLED:
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
+                break;
+            default:
+                return;
+        }
+        mSpatializerHelper.setDesiredHeadTrackingMode(mode);
+    }
+
+    /** @see Spatializer#setEffectParameter */
+    public void setSpatializerParameter(int key, @NonNull byte[] value) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(value);
+        mSpatializerHelper.setEffectParameter(key, value);
+    }
+
+    /** @see Spatializer#getEffectParameter */
+    public void getSpatializerParameter(int key, @NonNull byte[] value) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(value);
+        mSpatializerHelper.getEffectParameter(key, value);
+    }
+
+    /** @see Spatializer#getOutput */
+    public int getSpatializerOutput() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getOutput();
+    }
+
+    /** @see Spatializer#setOnSpatializerOutputChangedListener */
+    public void registerSpatializerOutputCallback(ISpatializerOutputCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerSpatializerOutputCallback(cb);
+    }
+
+    /** @see Spatializer#clearOnSpatializerOutputChangedListener */
+    public void unregisterSpatializerOutputCallback(ISpatializerOutputCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterSpatializerOutputCallback(cb);
+    }
+
+    /**
+     * post a message to schedule init/release of head tracking sensors
+     * @param init initialization if true, release if false
+     */
+    void postInitSpatializerHeadTrackingSensors(boolean init) {
+        sendMsg(mAudioHandler,
+                MSG_INIT_HEADTRACKING_SENSORS,
+                SENDMSG_REPLACE,
+                /*arg1*/ init ? 1 : 0,
+                0, TAG, /*delay*/ 0);
+    }
+
+    //==========================================================================================
     private boolean readCameraSoundForced() {
         return SystemProperties.getBoolean("audio.camerasound.force", false) ||
                 mContext.getResources().getBoolean(
@@ -8872,6 +9167,12 @@
         sVolumeLogger.dump(pw);
         pw.println("\n");
         dumpSupportedSystemUsage(pw);
+
+        pw.println("\n");
+        pw.println("\nSpatial audio:");
+        pw.println("mHasSpatializerEffect:" + mHasSpatializerEffect);
+        pw.println("isSpatializerEnabled:" + isSpatializerEnabled());
+        pw.println("isSpatialAudioEnabled:" + isSpatialAudioEnabled());
     }
 
     private void dumpSupportedSystemUsage(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 6d56780..ac212ee 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -17,6 +17,7 @@
 package com.android.server.audio;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioSystem;
@@ -24,6 +25,8 @@
 import android.os.SystemClock;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -59,6 +62,9 @@
     private ConcurrentHashMap<AudioAttributes, ArrayList<AudioDeviceAttributes>>
             mDevicesForAttrCache;
     private int[] mMethodCacheHit;
+    private static final Object sRoutingListenerLock = new Object();
+    @GuardedBy("sRoutingListenerLock")
+    private static @Nullable OnRoutingUpdatedListener sRoutingListener;
 
     /**
      * should be false except when trying to debug caching errors. When true, the value retrieved
@@ -76,6 +82,23 @@
             Log.d(TAG, "---- onRoutingUpdated (from native) ----------");
         }
         invalidateRoutingCache();
+        final OnRoutingUpdatedListener listener;
+        synchronized (sRoutingListenerLock) {
+            listener = sRoutingListener;
+        }
+        if (listener != null) {
+            listener.onRoutingUpdatedFromNative();
+        }
+    }
+
+    interface OnRoutingUpdatedListener {
+        void onRoutingUpdatedFromNative();
+    }
+
+    static void setRoutingListener(@Nullable OnRoutingUpdatedListener listener) {
+        synchronized (sRoutingListenerLock) {
+            sRoutingListener = listener;
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/audio/RotationHelper.java b/services/core/java/com/android/server/audio/RotationHelper.java
index ad72166..d4f4245 100644
--- a/services/core/java/com/android/server/audio/RotationHelper.java
+++ b/services/core/java/com/android/server/audio/RotationHelper.java
@@ -17,33 +17,44 @@
 package com.android.server.audio;
 
 import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.hardware.display.DisplayManager;
 import android.media.AudioSystem;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.util.Log;
 import android.view.Surface;
 import android.view.WindowManager;
 
 /**
  * Class to handle device rotation events for AudioService, and forward device rotation
- * to the audio HALs through AudioSystem.
+ * and folded state to the audio HALs through AudioSystem.
  *
  * The role of this class is to monitor device orientation changes, and upon rotation,
  * verify the UI orientation. In case of a change, send the new orientation, in increments
  * of 90deg, through AudioSystem.
  *
+ * Another role of this class is to track device folded state changes. In case of a
+ * change, send the new folded state through AudioSystem.
+ *
  * Note that even though we're responding to device orientation events, we always
  * query the display rotation so audio stays in sync with video/dialogs. This is
  * done with .getDefaultDisplay().getRotation() from WINDOW_SERVICE.
+ *
+ * We also monitor current display ID and audio is able to know which display is active.
  */
 class RotationHelper {
 
     private static final String TAG = "AudioService.RotationHelper";
 
     private static AudioDisplayListener sDisplayListener;
+    private static FoldStateListener sFoldStateListener;
 
     private static final Object sRotationLock = new Object();
+    private static final Object sFoldStateLock = new Object();
     private static int sDeviceRotation = Surface.ROTATION_0; // R/W synchronized on sRotationLock
+    private static boolean sDeviceFold = true; // R/W synchronized on sFoldStateLock
 
     private static Context sContext;
     private static Handler sHandler;
@@ -67,11 +78,17 @@
         ((DisplayManager) sContext.getSystemService(Context.DISPLAY_SERVICE))
                 .registerDisplayListener(sDisplayListener, sHandler);
         updateOrientation();
+
+        sFoldStateListener = new FoldStateListener(sContext, folded -> updateFoldState(folded));
+        sContext.getSystemService(DeviceStateManager.class)
+                .registerCallback(new HandlerExecutor(sHandler), sFoldStateListener);
     }
 
     static void disable() {
         ((DisplayManager) sContext.getSystemService(Context.DISPLAY_SERVICE))
                 .unregisterDisplayListener(sDisplayListener);
+        sContext.getSystemService(DeviceStateManager.class)
+                .unregisterCallback(sFoldStateListener);
     }
 
     /**
@@ -112,6 +129,22 @@
     }
 
     /**
+     * publish the change of device folded state if any.
+     */
+    static void updateFoldState(boolean newFolded) {
+        synchronized (sFoldStateLock) {
+            if (sDeviceFold != newFolded) {
+                sDeviceFold = newFolded;
+                if (newFolded) {
+                    AudioSystem.setParameters("device_folded=on");
+                } else {
+                    AudioSystem.setParameters("device_folded=off");
+                }
+            }
+        }
+    }
+
+    /**
      * Uses android.hardware.display.DisplayManager.DisplayListener
      */
     final static class AudioDisplayListener implements DisplayManager.DisplayListener {
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
new file mode 100644
index 0000000..7cd027c
--- /dev/null
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -0,0 +1,957 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.audio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioFormat;
+import android.media.AudioSystem;
+import android.media.INativeSpatializerCallback;
+import android.media.ISpatializer;
+import android.media.ISpatializerCallback;
+import android.media.ISpatializerHeadToSoundStagePoseCallback;
+import android.media.ISpatializerHeadTrackingCallback;
+import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.ISpatializerOutputCallback;
+import android.media.SpatializationLevel;
+import android.media.Spatializer;
+import android.media.SpatializerHeadTrackingMode;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A helper class to manage Spatializer related functionality
+ */
+public class SpatializerHelper {
+
+    private static final String TAG = "AS.SpatializerHelper";
+    private static final boolean DEBUG = true;
+    private static final boolean DEBUG_MORE = false;
+
+    private static void logd(String s) {
+        if (DEBUG) {
+            Log.i(TAG, s);
+        }
+    }
+
+    private final @NonNull AudioSystemAdapter mASA;
+    private final @NonNull AudioService mAudioService;
+    private @Nullable SensorManager mSensorManager;
+
+    //------------------------------------------------------------
+    // Spatializer state machine
+    private static final int STATE_UNINITIALIZED = 0;
+    private static final int STATE_NOT_SUPPORTED = 1;
+    private static final int STATE_DISABLED_UNAVAILABLE = 3;
+    private static final int STATE_ENABLED_UNAVAILABLE = 4;
+    private static final int STATE_ENABLED_AVAILABLE = 5;
+    private static final int STATE_DISABLED_AVAILABLE = 6;
+    private int mState = STATE_UNINITIALIZED;
+
+    /** current level as reported by native Spatializer in callback */
+    private int mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+    private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+    private int mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+    private int mDesiredHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+    private int mSpatOutput = 0;
+    private @Nullable ISpatializer mSpat;
+    private @Nullable SpatializerCallback mSpatCallback;
+    private @Nullable SpatializerHeadTrackingCallback mSpatHeadTrackingCallback;
+
+
+    // default attributes and format that determine basic availability of spatialization
+    private static final AudioAttributes DEFAULT_ATTRIBUTES = new AudioAttributes.Builder()
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+    private static final AudioFormat DEFAULT_FORMAT = new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    // device array to store the routing for the default attributes and format, size 1 because
+    // media is never expected to be duplicated
+    private static final AudioDeviceAttributes[] ROUTING_DEVICES = new AudioDeviceAttributes[1];
+
+    //---------------------------------------------------------------
+    // audio device compatibility / enabled
+
+    private final ArrayList<AudioDeviceAttributes> mCompatibleAudioDevices = new ArrayList<>(0);
+
+    //------------------------------------------------------
+    // initialization
+    SpatializerHelper(@NonNull AudioService mother, @NonNull AudioSystemAdapter asa) {
+        mAudioService = mother;
+        mASA = asa;
+    }
+
+    synchronized void init(boolean effectExpected) {
+        Log.i(TAG, "Initializing");
+        if (!effectExpected) {
+            Log.i(TAG, "Setting state to STATE_NOT_SUPPORTED due to effect not expected");
+            mState = STATE_NOT_SUPPORTED;
+            return;
+        }
+        if (mState != STATE_UNINITIALIZED) {
+            throw new IllegalStateException(("init() called in state:" + mState));
+        }
+        // is there a spatializer?
+        mSpatCallback = new SpatializerCallback();
+        final ISpatializer spat = AudioSystem.getSpatializer(mSpatCallback);
+        if (spat == null) {
+            Log.i(TAG, "init(): No Spatializer found");
+            mState = STATE_NOT_SUPPORTED;
+            return;
+        }
+        // capabilities of spatializer?
+        try {
+            byte[] levels = spat.getSupportedLevels();
+            if (levels == null
+                    || levels.length == 0
+                    || (levels.length == 1
+                    && levels[0] == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE)) {
+                Log.e(TAG, "Spatializer is useless");
+                mState = STATE_NOT_SUPPORTED;
+                return;
+            }
+            for (byte level : levels) {
+                logd("found support for level: " + level);
+                if (level == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL) {
+                    logd("Setting capable level to LEVEL_MULTICHANNEL");
+                    mCapableSpatLevel = level;
+                    break;
+                }
+            }
+        } catch (RemoteException e) {
+            /* capable level remains at NONE*/
+        } finally {
+            if (spat != null) {
+                try {
+                    spat.release();
+                } catch (RemoteException e) { /* capable level remains at NONE*/ }
+            }
+        }
+        if (mCapableSpatLevel == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) {
+            mState = STATE_NOT_SUPPORTED;
+            return;
+        }
+        mState = STATE_DISABLED_UNAVAILABLE;
+        // note at this point mSpat is still not instantiated
+    }
+
+    /**
+     * Like init() but resets the state and spatializer levels
+     * @param featureEnabled
+     */
+    synchronized void reset(boolean featureEnabled) {
+        Log.i(TAG, "Resetting");
+        mState = STATE_UNINITIALIZED;
+        mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+        mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+        mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+        init(true);
+        setFeatureEnabled(featureEnabled);
+    }
+
+    //------------------------------------------------------
+    // routing monitoring
+    void onRoutingUpdated() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                return;
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+                break;
+        }
+        mASA.getDevicesForAttributes(DEFAULT_ATTRIBUTES).toArray(ROUTING_DEVICES);
+        final boolean able =
+                AudioSystem.canBeSpatialized(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, ROUTING_DEVICES);
+        logd("onRoutingUpdated: can spatialize media 5.1:" + able
+                + " on device:" + ROUTING_DEVICES[0]);
+        setDispatchAvailableState(able);
+    }
+
+    //------------------------------------------------------
+    // spatializer callback from native
+    private final class SpatializerCallback extends INativeSpatializerCallback.Stub {
+
+        public void onLevelChanged(byte level) {
+            logd("SpatializerCallback.onLevelChanged level:" + level);
+            synchronized (SpatializerHelper.this) {
+                mSpatLevel = spatializationLevelToSpatializerInt(level);
+            }
+            // TODO use reported spat level to change state
+
+            // init sensors
+            if (level == SpatializationLevel.NONE) {
+                initSensors(/*init*/false);
+            } else {
+                postInitSensors(true);
+            }
+        }
+
+        public void onOutputChanged(int output) {
+            logd("SpatializerCallback.onOutputChanged output:" + output);
+            int oldOutput;
+            synchronized (SpatializerHelper.this) {
+                oldOutput = mSpatOutput;
+                mSpatOutput = output;
+            }
+            if (oldOutput != output) {
+                dispatchOutputUpdate(output);
+            }
+        }
+    };
+
+    // spatializer head tracking callback from native
+    private final class SpatializerHeadTrackingCallback
+            extends ISpatializerHeadTrackingCallback.Stub {
+        public void onHeadTrackingModeChanged(byte mode)  {
+            logd("SpatializerHeadTrackingCallback.onHeadTrackingModeChanged mode:" + mode);
+            int oldMode, newMode;
+            synchronized (this) {
+                oldMode = mActualHeadTrackingMode;
+                mActualHeadTrackingMode = headTrackingModeTypeToSpatializerInt(mode);
+                newMode = mActualHeadTrackingMode;
+            }
+            if (oldMode != newMode) {
+                dispatchActualHeadTrackingMode(newMode);
+            }
+        }
+
+        public void onHeadToSoundStagePoseUpdated(float[] headToStage)  {
+            if (headToStage == null) {
+                Log.e(TAG, "SpatializerHeadTrackingCallback.onHeadToStagePoseUpdated"
+                        + "null transform");
+                return;
+            }
+            if (headToStage.length != 6) {
+                Log.e(TAG, "SpatializerHeadTrackingCallback.onHeadToStagePoseUpdated"
+                        + " invalid transform length" + headToStage.length);
+                return;
+            }
+            if (DEBUG_MORE) {
+                // 6 values * (4 digits + 1 dot + 2 brackets) = 42 characters
+                StringBuilder t = new StringBuilder(42);
+                for (float val : headToStage) {
+                    t.append("[").append(String.format(Locale.ENGLISH, "%.3f", val)).append("]");
+                }
+                logd("SpatializerHeadTrackingCallback.onHeadToStagePoseUpdated headToStage:" + t);
+            }
+            dispatchPoseUpdate(headToStage);
+        }
+    };
+
+    //------------------------------------------------------
+    // compatible devices
+    /**
+     * @return a shallow copy of the list of compatible audio devices
+     */
+    synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() {
+        return (List<AudioDeviceAttributes>) mCompatibleAudioDevices.clone();
+    }
+
+    synchronized void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        if (!mCompatibleAudioDevices.contains(ada)) {
+            mCompatibleAudioDevices.add(ada);
+        }
+    }
+
+    synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        mCompatibleAudioDevices.remove(ada);
+    }
+
+    //------------------------------------------------------
+    // states
+
+    synchronized boolean isEnabled() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+                return false;
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+            default:
+                return true;
+        }
+    }
+
+    synchronized boolean isAvailable() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+                return false;
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+            default:
+                return true;
+        }
+    }
+
+    synchronized void setFeatureEnabled(boolean enabled) {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+                if (enabled) {
+                    throw(new IllegalStateException("Can't enable when uninitialized"));
+                }
+                return;
+            case STATE_NOT_SUPPORTED:
+                if (enabled) {
+                    Log.e(TAG, "Can't enable when unsupported");
+                }
+                return;
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+                if (enabled) {
+                    createSpat();
+                    break;
+                } else {
+                    // already in disabled state
+                    return;
+                }
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (!enabled) {
+                    releaseSpat();
+                    break;
+                } else {
+                    // already in enabled state
+                    return;
+                }
+        }
+        setDispatchFeatureEnabledState(enabled);
+    }
+
+    synchronized int getCapableImmersiveAudioLevel() {
+        return mCapableSpatLevel;
+    }
+
+    final RemoteCallbackList<ISpatializerCallback> mStateCallbacks =
+            new RemoteCallbackList<ISpatializerCallback>();
+
+    synchronized void registerStateCallback(
+            @NonNull ISpatializerCallback callback) {
+        mStateCallbacks.register(callback);
+    }
+
+    synchronized void unregisterStateCallback(
+            @NonNull ISpatializerCallback callback) {
+        mStateCallbacks.unregister(callback);
+    }
+
+    /**
+     * precondition: mState = STATE_*
+     *               isFeatureEnabled() != featureEnabled
+     * @param featureEnabled
+     */
+    private synchronized void setDispatchFeatureEnabledState(boolean featureEnabled) {
+        if (featureEnabled) {
+            switch (mState) {
+                case STATE_DISABLED_UNAVAILABLE:
+                    mState = STATE_ENABLED_UNAVAILABLE;
+                    break;
+                case STATE_DISABLED_AVAILABLE:
+                    mState = STATE_ENABLED_AVAILABLE;
+                    break;
+                default:
+                    throw(new IllegalStateException("Invalid mState:" + mState
+                            + " for enabled true"));
+            }
+        } else {
+            switch (mState) {
+                case STATE_ENABLED_UNAVAILABLE:
+                    mState = STATE_DISABLED_UNAVAILABLE;
+                    break;
+                case STATE_ENABLED_AVAILABLE:
+                    mState = STATE_DISABLED_AVAILABLE;
+                    break;
+                default:
+                    throw (new IllegalStateException("Invalid mState:" + mState
+                            + " for enabled false"));
+            }
+        }
+        final int nbCallbacks = mStateCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mStateCallbacks.getBroadcastItem(i)
+                        .dispatchSpatializerEnabledChanged(featureEnabled);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchSpatializerEnabledChanged", e);
+            }
+        }
+        mStateCallbacks.finishBroadcast();
+        mAudioService.persistSpatialAudioEnabled(featureEnabled);
+    }
+
+    private synchronized void setDispatchAvailableState(boolean available) {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                throw(new IllegalStateException(
+                        "Should not update available state in state:" + mState));
+            case STATE_DISABLED_UNAVAILABLE:
+                if (available) {
+                    mState = STATE_DISABLED_AVAILABLE;
+                    break;
+                } else {
+                    // already in unavailable state
+                    return;
+                }
+            case STATE_ENABLED_UNAVAILABLE:
+                if (available) {
+                    mState = STATE_ENABLED_AVAILABLE;
+                    break;
+                } else {
+                    // already in unavailable state
+                    return;
+                }
+            case STATE_DISABLED_AVAILABLE:
+                if (available) {
+                    // already in available state
+                    return;
+                } else {
+                    mState = STATE_DISABLED_UNAVAILABLE;
+                    break;
+                }
+            case STATE_ENABLED_AVAILABLE:
+                if (available) {
+                    // already in available state
+                    return;
+                } else {
+                    mState = STATE_ENABLED_UNAVAILABLE;
+                    break;
+                }
+        }
+        final int nbCallbacks = mStateCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mStateCallbacks.getBroadcastItem(i)
+                        .dispatchSpatializerAvailableChanged(available);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchSpatializerEnabledChanged", e);
+            }
+        }
+        mStateCallbacks.finishBroadcast();
+    }
+
+    //------------------------------------------------------
+    // native Spatializer management
+
+    /**
+     * precondition: mState == STATE_DISABLED_*
+     */
+    private void createSpat() {
+        if (mSpat == null) {
+            mSpatCallback = new SpatializerCallback();
+            mSpatHeadTrackingCallback = new SpatializerHeadTrackingCallback();
+            mSpat = AudioSystem.getSpatializer(mSpatCallback);
+            try {
+                mSpat.setLevel((byte)  Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL);
+                //TODO: register heatracking callback only when sensors are registered
+                if (mSpat.isHeadTrackingSupported()) {
+                    mSpat.registerHeadTrackingCallback(mSpatHeadTrackingCallback);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Can't set spatializer level", e);
+                mState = STATE_NOT_SUPPORTED;
+                mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+            }
+        }
+    }
+
+    /**
+     * precondition: mState == STATE_ENABLED_*
+     */
+    private void releaseSpat() {
+        if (mSpat != null) {
+            mSpatCallback = null;
+            try {
+                mSpat.registerHeadTrackingCallback(null);
+                mSpat.release();
+                mSpat = null;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Can't set release spatializer cleanly", e);
+            }
+        }
+    }
+
+    //------------------------------------------------------
+    // virtualization capabilities
+    synchronized boolean canBeSpatialized(
+            @NonNull AudioAttributes attributes, @NonNull AudioFormat format) {
+        logd("canBeSpatialized usage:" + attributes.getUsage()
+                + " format:" + format.toLogFriendlyString());
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+                logd("canBeSpatialized false due to state:" + mState);
+                return false;
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                break;
+        }
+
+        // filter on AudioAttributes usage
+        switch (attributes.getUsage()) {
+            case AudioAttributes.USAGE_MEDIA:
+            case AudioAttributes.USAGE_GAME:
+                break;
+            default:
+                logd("canBeSpatialized false due to usage:" + attributes.getUsage());
+                return false;
+        }
+        AudioDeviceAttributes[] devices =
+                // going through adapter to take advantage of routing cache
+                (AudioDeviceAttributes[]) mASA.getDevicesForAttributes(attributes).toArray();
+        final boolean able = AudioSystem.canBeSpatialized(attributes, format, devices);
+        logd("canBeSpatialized returning " + able);
+        return able;
+    }
+
+    //------------------------------------------------------
+    // head tracking
+    final RemoteCallbackList<ISpatializerHeadTrackingModeCallback> mHeadTrackingModeCallbacks =
+            new RemoteCallbackList<ISpatializerHeadTrackingModeCallback>();
+
+    synchronized void registerHeadTrackingModeCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback callback) {
+        mHeadTrackingModeCallbacks.register(callback);
+    }
+
+    synchronized void unregisterHeadTrackingModeCallback(
+            @NonNull ISpatializerHeadTrackingModeCallback callback) {
+        mHeadTrackingModeCallbacks.unregister(callback);
+    }
+
+    synchronized int[] getSupportedHeadTrackingModes() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+                return new int[0];
+            case STATE_NOT_SUPPORTED:
+                // return an empty list when Spatializer functionality is not supported
+                // because the list of head tracking modes you can set is actually empty
+                // as defined in {@link Spatializer#getSupportedHeadTrackingModes()}
+                return new int[0];
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    return new int[0];
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            final byte[] values = mSpat.getSupportedHeadTrackingModes();
+            ArrayList<Integer> list = new ArrayList<>(0);
+            for (byte value : values) {
+                switch (value) {
+                    case SpatializerHeadTrackingMode.OTHER:
+                    case SpatializerHeadTrackingMode.DISABLED:
+                        // not expected here, skip
+                        break;
+                    case SpatializerHeadTrackingMode.RELATIVE_WORLD:
+                    case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
+                        list.add(headTrackingModeTypeToSpatializerInt(value));
+                        break;
+                    default:
+                        Log.e(TAG, "Unexpected head tracking mode:" + value,
+                                new IllegalArgumentException("invalid mode"));
+                        break;
+                }
+            }
+            int[] modes = new int[list.size()];
+            for (int i = 0; i < list.size(); i++) {
+                modes[i] = list.get(i);
+            }
+            return modes;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getSupportedHeadTrackingModes", e);
+            return new int[] { Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED };
+        }
+    }
+
+    synchronized int getActualHeadTrackingMode() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+                return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+            case STATE_NOT_SUPPORTED:
+                return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            return headTrackingModeTypeToSpatializerInt(mSpat.getActualHeadTrackingMode());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getActualHeadTrackingMode", e);
+            return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+        }
+    }
+
+    synchronized int getDesiredHeadTrackingMode() {
+        return mDesiredHeadTrackingMode;
+    }
+
+    synchronized void setGlobalTransform(@NonNull float[] transform) {
+        if (transform.length != 6) {
+            throw new IllegalArgumentException("invalid array size" + transform.length);
+        }
+        if (!checkSpatForHeadTracking("setGlobalTransform")) {
+            return;
+        }
+        try {
+            mSpat.setGlobalTransform(transform);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setGlobalTransform", e);
+        }
+    }
+
+    synchronized void recenterHeadTracker() {
+        if (!checkSpatForHeadTracking("recenterHeadTracker")) {
+            return;
+        }
+        try {
+            mSpat.recenterHeadTracker();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling recenterHeadTracker", e);
+        }
+    }
+
+    synchronized void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) {
+        if (!checkSpatForHeadTracking("setDesiredHeadTrackingMode")) {
+            return;
+        }
+        try {
+            if (mode != mDesiredHeadTrackingMode) {
+                mSpat.setDesiredHeadTrackingMode(spatializerIntToHeadTrackingModeType(mode));
+                mDesiredHeadTrackingMode = mode;
+                dispatchDesiredHeadTrackingMode(mode);
+            }
+
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling setDesiredHeadTrackingMode", e);
+        }
+    }
+
+    private boolean checkSpatForHeadTracking(String funcName) {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                return false;
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    throw (new IllegalStateException(
+                            "null Spatializer when calling " + funcName));
+                }
+                break;
+        }
+        return true;
+    }
+
+    private void dispatchActualHeadTrackingMode(int newMode) {
+        final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mHeadTrackingModeCallbacks.getBroadcastItem(i)
+                        .dispatchSpatializerActualHeadTrackingModeChanged(newMode);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchSpatializerActualHeadTrackingModeChanged", e);
+            }
+        }
+        mHeadTrackingModeCallbacks.finishBroadcast();
+    }
+
+    private void dispatchDesiredHeadTrackingMode(int newMode) {
+        final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mHeadTrackingModeCallbacks.getBroadcastItem(i)
+                        .dispatchSpatializerDesiredHeadTrackingModeChanged(newMode);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchSpatializerDesiredHeadTrackingModeChanged", e);
+            }
+        }
+        mHeadTrackingModeCallbacks.finishBroadcast();
+    }
+
+    //------------------------------------------------------
+    // head pose
+    final RemoteCallbackList<ISpatializerHeadToSoundStagePoseCallback> mHeadPoseCallbacks =
+            new RemoteCallbackList<ISpatializerHeadToSoundStagePoseCallback>();
+
+    synchronized void registerHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback callback) {
+        mHeadPoseCallbacks.register(callback);
+    }
+
+    synchronized void unregisterHeadToSoundstagePoseCallback(
+            @NonNull ISpatializerHeadToSoundStagePoseCallback callback) {
+        mHeadPoseCallbacks.unregister(callback);
+    }
+
+    private void dispatchPoseUpdate(float[] pose) {
+        final int nbCallbacks = mHeadPoseCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mHeadPoseCallbacks.getBroadcastItem(i)
+                        .dispatchPoseChanged(pose);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchPoseChanged", e);
+            }
+        }
+        mHeadPoseCallbacks.finishBroadcast();
+    }
+
+    //------------------------------------------------------
+    // vendor parameters
+    synchronized void setEffectParameter(int key, @NonNull byte[] value) {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                throw (new IllegalStateException(
+                        "Can't set parameter key:" + key + " without a spatializer"));
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    throw (new IllegalStateException(
+                            "null Spatializer for setParameter for key:" + key));
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            mSpat.setParameter(key, value);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in setParameter for key:" + key, e);
+        }
+    }
+
+    synchronized void getEffectParameter(int key, @NonNull byte[] value) {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                throw (new IllegalStateException(
+                        "Can't get parameter key:" + key + " without a spatializer"));
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    throw (new IllegalStateException(
+                            "null Spatializer for getParameter for key:" + key));
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            mSpat.getParameter(key, value);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in getParameter for key:" + key, e);
+        }
+    }
+
+    //------------------------------------------------------
+    // output
+
+    /** @see Spatializer#getOutput */
+    synchronized int getOutput() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                throw (new IllegalStateException(
+                        "Can't get output without a spatializer"));
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    throw (new IllegalStateException(
+                            "null Spatializer for getOutput"));
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            return mSpat.getOutput();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in getOutput", e);
+            return 0;
+        }
+    }
+
+    final RemoteCallbackList<ISpatializerOutputCallback> mOutputCallbacks =
+            new RemoteCallbackList<ISpatializerOutputCallback>();
+
+    synchronized void registerSpatializerOutputCallback(
+            @NonNull ISpatializerOutputCallback callback) {
+        mOutputCallbacks.register(callback);
+    }
+
+    synchronized void unregisterSpatializerOutputCallback(
+            @NonNull ISpatializerOutputCallback callback) {
+        mOutputCallbacks.unregister(callback);
+    }
+
+    private void dispatchOutputUpdate(int output) {
+        final int nbCallbacks = mOutputCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mOutputCallbacks.getBroadcastItem(i).dispatchSpatializerOutputChanged(output);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchOutputUpdate", e);
+            }
+        }
+        mOutputCallbacks.finishBroadcast();
+    }
+
+    //------------------------------------------------------
+    // sensors
+    private void initSensors(boolean init) {
+        if (mSensorManager == null) {
+            mSensorManager = (SensorManager)
+                    mAudioService.mContext.getSystemService(Context.SENSOR_SERVICE);
+        }
+        final int headHandle;
+        final int screenHandle;
+        if (init) {
+            if (mSensorManager == null) {
+                Log.e(TAG, "Null SensorManager, can't init sensors");
+                return;
+            }
+            // TODO replace with dynamic association of sensor for headtracker
+            Sensor headSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
+            headHandle = headSensor.getHandle();
+            //Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
+            //screenHandle = deviceSensor.getHandle();
+            screenHandle = -1;
+        } else {
+            // -1 is disable value
+            screenHandle = -1;
+            headHandle = -1;
+        }
+        try {
+            Log.i(TAG, "setScreenSensor:" + screenHandle);
+            mSpat.setScreenSensor(screenHandle);
+        } catch (Exception e) {
+            Log.e(TAG, "Error calling setScreenSensor:" + screenHandle, e);
+        }
+        try {
+            Log.i(TAG, "setHeadSensor:" + headHandle);
+            mSpat.setHeadSensor(headHandle);
+        } catch (Exception e) {
+            Log.e(TAG, "Error calling setHeadSensor:" + headHandle, e);
+        }
+    }
+
+    private void postInitSensors(boolean init) {
+        mAudioService.postInitSpatializerHeadTrackingSensors(init);
+    }
+
+    synchronized void onInitSensors(boolean init) {
+        final String action = init ? "initializing" : "releasing";
+        if (mSpat == null) {
+            Log.e(TAG, "not " + action + " sensors, null spatializer");
+            return;
+        }
+        try {
+            if (!mSpat.isHeadTrackingSupported()) {
+                Log.e(TAG, "not " + action + " sensors, spatializer doesn't support headtracking");
+                return;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "not " + action + " sensors, error querying headtracking", e);
+            return;
+        }
+        initSensors(init);
+    }
+
+    //------------------------------------------------------
+    // SDK <-> AIDL converters
+    private static int headTrackingModeTypeToSpatializerInt(byte mode) {
+        switch (mode) {
+            case SpatializerHeadTrackingMode.OTHER:
+                return Spatializer.HEAD_TRACKING_MODE_OTHER;
+            case SpatializerHeadTrackingMode.DISABLED:
+                return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+            case SpatializerHeadTrackingMode.RELATIVE_WORLD:
+                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
+            case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
+                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE;
+            default:
+                throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode));
+        }
+    }
+
+    private static byte spatializerIntToHeadTrackingModeType(int sdkMode) {
+        switch (sdkMode) {
+            case Spatializer.HEAD_TRACKING_MODE_OTHER:
+                return SpatializerHeadTrackingMode.OTHER;
+            case Spatializer.HEAD_TRACKING_MODE_DISABLED:
+                return SpatializerHeadTrackingMode.DISABLED;
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
+                return SpatializerHeadTrackingMode.RELATIVE_WORLD;
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
+                return SpatializerHeadTrackingMode.RELATIVE_SCREEN;
+            default:
+                throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode));
+        }
+    }
+
+    private static int spatializationLevelToSpatializerInt(byte level) {
+        switch (level) {
+            case SpatializationLevel.NONE:
+                return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+            case SpatializationLevel.SPATIALIZER_MULTICHANNEL:
+                return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL;
+            case SpatializationLevel.SPATIALIZER_MCHAN_BED_PLUS_OBJECTS:
+                return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS;
+            default:
+                throw(new IllegalArgumentException("Unexpected spatializer level:" + level));
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index b42f898..9c8ccd9 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -47,6 +47,7 @@
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
 import android.hardware.biometrics.PromptInfo;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.SensorPropertiesInternal;
 import android.hardware.face.FaceSensorProperties;
 import android.hardware.face.FaceSensorPropertiesInternal;
@@ -766,8 +767,9 @@
         if (isUdfps && udfpsProps.length == 3) {
             return new FingerprintSensorPropertiesInternal(sensorId,
                     Utils.authenticatorStrengthToPropertyStrength(strength), maxEnrollmentsPerUser,
-                    componentInfo, sensorType, resetLockoutRequiresHardwareAuthToken, udfpsProps[0],
-                    udfpsProps[1], udfpsProps[2]);
+                    componentInfo, sensorType, resetLockoutRequiresHardwareAuthToken,
+                    List.of(new SensorLocationInternal("" /* display */,
+                            udfpsProps[0], udfpsProps[1], udfpsProps[2])));
         } else {
             return new FingerprintSensorPropertiesInternal(sensorId,
                     Utils.authenticatorStrengthToPropertyStrength(strength), maxEnrollmentsPerUser,
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 996f0fd..4f7c6b0 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -50,7 +50,6 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.biometrics.SensorPropertiesInternal;
-import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Binder;
 import android.os.Build;
 import android.os.RemoteException;
@@ -62,7 +61,6 @@
 
 import com.android.internal.R;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 
 import java.util.List;
@@ -541,14 +539,4 @@
                 throw new IllegalArgumentException("Unknown strength: " + strength);
         }
     }
-
-    public static int getUdfpsAuthReason(@NonNull AuthenticationClient<?> client) {
-        if (client.isKeyguard()) {
-            return IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD;
-        } else if (client.isBiometricPrompt()) {
-            return IUdfpsOverlayController.REASON_AUTH_BP;
-        } else {
-            return IUdfpsOverlayController.REASON_AUTH_FPM_OTHER;
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 6f38ed0..85d6d7f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -28,6 +28,7 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -457,4 +458,14 @@
     public boolean wasAuthAttempted() {
         return mAuthAttempted;
     }
+
+    protected int getShowOverlayReason() {
+        if (isKeyguard()) {
+            return BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
+        } else if (isBiometricPrompt()) {
+            return BiometricOverlayConstants.REASON_AUTH_BP;
+        } else {
+            return BiometricOverlayConstants.REASON_AUTH_OTHER;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 9191b8b..2826e0c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -19,7 +19,9 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
+import android.hardware.fingerprint.FingerprintManager;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -128,4 +130,15 @@
     public boolean interruptsPrecedingClients() {
         return true;
     }
+
+    protected int getOverlayReasonFromEnrollReason(@FingerprintManager.EnrollReason int reason) {
+        switch (reason) {
+            case FingerprintManager.ENROLL_FIND_SENSOR:
+                return BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR;
+            case FingerprintManager.ENROLL_ENROLL:
+                return BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
+            default:
+                return BiometricOverlayConstants.REASON_UNKNOWN;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
new file mode 100644
index 0000000..0087178
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlayController;
+import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Single entry point & holder for controllers managing UI overlays for biometrics.
+ *
+ * For common operations, like {@link #show(int, int, AcquisitionClient)}, modalities are
+ * skipped if they are not present (provided as null via the constructor).
+ *
+ * Use the getters, such as {@link #ifUdfps(OverlayControllerConsumer)}, to get a controller for
+ * operations that are unique to a single modality.
+ */
+public final class SensorOverlays {
+
+    private static final String TAG = "SensorOverlays";
+
+    @NonNull private final Optional<IUdfpsOverlayController> mUdfpsOverlayController;
+    @NonNull private final Optional<ISidefpsController> mSidefpsController;
+
+    /**
+     * Create an overlay controller for each modality.
+     *
+     * @param udfpsOverlayController under display fps or null if not present on device
+     * @param sidefpsController side fps or null if not present on device
+     */
+    public SensorOverlays(
+            @Nullable IUdfpsOverlayController udfpsOverlayController,
+            @Nullable ISidefpsController sidefpsController) {
+        mUdfpsOverlayController = Optional.ofNullable(udfpsOverlayController);
+        mSidefpsController = Optional.ofNullable(sidefpsController);
+    }
+
+    /**
+     * Show the overlay.
+     *
+     * @param sensorId sensor id
+     * @param reason reason for showing
+     * @param client client performing operation
+     */
+    public void show(int sensorId, @BiometricOverlayConstants.ShowReason int reason,
+            @NonNull AcquisitionClient<?> client) {
+        if (mSidefpsController.isPresent()) {
+            try {
+                mSidefpsController.get().show(sensorId, reason);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception when showing the side-fps overlay", e);
+            }
+        }
+
+        if (mUdfpsOverlayController.isPresent()) {
+            final IUdfpsOverlayControllerCallback callback =
+                    new IUdfpsOverlayControllerCallback.Stub() {
+                        @Override
+                        public void onUserCanceled() {
+                            client.onUserCanceled();
+                        }
+                    };
+
+            try {
+                mUdfpsOverlayController.get().showUdfpsOverlay(sensorId, reason, callback);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception when showing the UDFPS overlay", e);
+            }
+        }
+    }
+
+    /**
+     * Hide the overlay.
+     *
+     * @param sensorId sensor id
+     */
+    public void hide(int sensorId) {
+        if (mSidefpsController.isPresent()) {
+            try {
+                mSidefpsController.get().hide(sensorId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception when hiding the side-fps overlay", e);
+            }
+        }
+
+        if (mUdfpsOverlayController.isPresent()) {
+            try {
+                mUdfpsOverlayController.get().hideUdfpsOverlay(sensorId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception when hiding the UDFPS overlay", e);
+            }
+        }
+    }
+
+    /**
+     * Use the udfps controller, if present.
+     * @param consumer action
+     */
+    public void ifUdfps(OverlayControllerConsumer<IUdfpsOverlayController> consumer) {
+        if (mUdfpsOverlayController.isPresent()) {
+            try {
+                consumer.accept(mUdfpsOverlayController.get());
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception using overlay controller", e);
+            }
+        }
+    }
+
+    /**
+     * Consumer for a biometric overlay controller.
+     *
+     * This behaves like a normal {@link Consumer} except that it will trap and log
+     * any thrown {@link RemoteException}.
+     *
+     * @param <T> the type of the input to the operation
+     **/
+    @FunctionalInterface
+    public interface OverlayControllerConsumer<T> {
+        /** Perform the operation. */
+        void accept(T t) throws RemoteException;
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/SidefpsHelper.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/SidefpsHelper.java
deleted file mode 100644
index 474066c..0000000
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/SidefpsHelper.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.biometrics.sensors.fingerprint;
-
-import android.annotation.Nullable;
-import android.hardware.fingerprint.ISidefpsController;
-import android.os.RemoteException;
-import android.util.Slog;
-
-/**
- * Contains helper methods for side-fps fingerprint controller.
- */
-public class SidefpsHelper {
-    private static final String TAG = "SidefpsHelper";
-
-    /**
-     * Shows the side-fps affordance
-     * @param sidefpsController controller that shows and hides the side-fps affordance
-     */
-    public static void showOverlay(@Nullable ISidefpsController sidefpsController) {
-        if (sidefpsController == null) {
-            return;
-        }
-
-        try {
-            sidefpsController.show();
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception when showing the side-fps overlay", e);
-        }
-    }
-
-    /**
-     * Hides the side-fps affordance
-     * @param sidefpsController controller that shows and hides the side-fps affordance
-     */
-    public static void hideOverlay(@Nullable ISidefpsController sidefpsController) {
-        if (sidefpsController == null) {
-            return;
-        }
-        try {
-            sidefpsController.hide();
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception when hiding the side-fps overlay", e);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/UdfpsHelper.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/UdfpsHelper.java
index 879c8a0..29661d4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/UdfpsHelper.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/UdfpsHelper.java
@@ -17,17 +17,12 @@
 package com.android.server.biometrics.sensors.fingerprint;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.fingerprint.FingerprintManager;
-import android.hardware.fingerprint.IUdfpsOverlayController;
-import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
 import android.os.RemoteException;
 import android.util.Slog;
 
-import com.android.server.biometrics.sensors.AcquisitionClient;
-
 /**
  * Contains helper methods for under-display fingerprint HIDL.
  */
@@ -68,88 +63,6 @@
         }
     }
 
-    public static int getReasonFromEnrollReason(@FingerprintManager.EnrollReason int reason) {
-        switch (reason) {
-            case FingerprintManager.ENROLL_FIND_SENSOR:
-                return IUdfpsOverlayController.REASON_ENROLL_FIND_SENSOR;
-            case FingerprintManager.ENROLL_ENROLL:
-                return IUdfpsOverlayController.REASON_ENROLL_ENROLLING;
-            default:
-                return IUdfpsOverlayController.REASON_UNKNOWN;
-        }
-    }
-
-    public static void showUdfpsOverlay(int sensorId, int reason,
-            @Nullable IUdfpsOverlayController udfpsOverlayController,
-            @NonNull AcquisitionClient<?> client) {
-        if (udfpsOverlayController == null) {
-            return;
-        }
-
-        final IUdfpsOverlayControllerCallback callback =
-                new IUdfpsOverlayControllerCallback.Stub() {
-                    @Override
-                    public void onUserCanceled() {
-                        client.onUserCanceled();
-                    }
-                };
-
-        try {
-            udfpsOverlayController.showUdfpsOverlay(sensorId, reason, callback);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception when showing the UDFPS overlay", e);
-        }
-    }
-
-    public static void hideUdfpsOverlay(int sensorId,
-            @Nullable IUdfpsOverlayController udfpsOverlayController) {
-        if (udfpsOverlayController == null) {
-            return;
-        }
-        try {
-            udfpsOverlayController.hideUdfpsOverlay(sensorId);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception when hiding the UDFPS overlay", e);
-        }
-    }
-
-    public static void onAcquiredGood(int sensorId,
-            @Nullable IUdfpsOverlayController udfpsOverlayController) {
-        if (udfpsOverlayController == null) {
-            return;
-        }
-
-        try {
-            udfpsOverlayController.onAcquiredGood(sensorId);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception when sending onAcquiredGood", e);
-        }
-    }
-
-    public static void onEnrollmentProgress(int sensorId, int remaining,
-            @Nullable IUdfpsOverlayController udfpsOverlayController) {
-        if (udfpsOverlayController == null) {
-            return;
-        }
-        try {
-            udfpsOverlayController.onEnrollmentProgress(sensorId, remaining);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception when sending onEnrollmentProgress", e);
-        }
-    }
-
-    public static void onEnrollmentHelp(int sensorId,
-            @Nullable IUdfpsOverlayController udfpsOverlayController) {
-        if (udfpsOverlayController == null) {
-            return;
-        }
-        try {
-            udfpsOverlayController.onEnrollmentHelp(sensorId);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception when sending onEnrollmentHelp", e);
-        }
-    }
-
     public static boolean isValidAcquisitionMessage(@NonNull Context context,
             int acquireInfo, int vendorCode) {
         return FingerprintManager.getAcquiredString(context, acquireInfo, vendorCode) != null;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index b405be7..ca051e9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -27,20 +27,20 @@
 import android.hardware.biometrics.common.ICancellationSignal;
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
 
-import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutConsumer;
 import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.SensorOverlays;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
-import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
 
 import java.util.ArrayList;
 
@@ -53,7 +53,7 @@
     private static final String TAG = "FingerprintAuthenticationClient";
 
     @NonNull private final LockoutCache mLockoutCache;
-    @Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
+    @NonNull private final SensorOverlays mSensorOverlays;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
     @NonNull private final CallbackWithProbe<Probe> mALSProbeCallback;
 
@@ -68,6 +68,7 @@
             int sensorId, boolean isStrongBiometric, int statsClient,
             @Nullable TaskStackListener taskStackListener, @NonNull LockoutCache lockoutCache,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
+            @Nullable ISidefpsController sidefpsController,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps) {
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted, owner,
@@ -77,7 +78,7 @@
                 false /* isKeyguardBypassEnabled */);
         setRequestId(requestId);
         mLockoutCache = lockoutCache;
-        mUdfpsOverlayController = udfpsOverlayController;
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mSensorProps = sensorProps;
         mALSProbeCallback = createALSCallback(false /* startWithClient */);
     }
@@ -120,7 +121,7 @@
 
         if (authenticated) {
             mState = STATE_STOPPED;
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.hide(getSensorId());
         } else {
             mState = STATE_STARTED_PAUSED_ATTEMPTED;
         }
@@ -131,7 +132,7 @@
         // For UDFPS, notify SysUI that the illumination can be turned off.
         // See AcquiredInfo#GOOD and AcquiredInfo#RETRYING_CAPTURE
         if (acquiredInfo == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD) {
-            UdfpsHelper.onAcquiredGood(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.ifUdfps(controller -> controller.onAcquiredGood(getSensorId()));
         }
 
         super.onAcquired(acquiredInfo, vendorCode);
@@ -145,27 +146,27 @@
             BiometricNotificationUtils.showBadCalibrationNotification(getContext());
         }
 
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
     }
 
     @Override
     protected void startHalOperation() {
-        UdfpsHelper.showUdfpsOverlay(getSensorId(), Utils.getUdfpsAuthReason(this),
-                mUdfpsOverlayController, this);
+        mSensorOverlays.show(getSensorId(), getShowOverlayReason(), this);
+
         try {
             mCancellationSignal = getFreshDaemon().authenticate(mOperationId);
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
             onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.hide(getSensorId());
             mCallback.onClientFinished(this, false /* success */);
         }
     }
 
     @Override
     protected void stopHalOperation() {
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
         if (mCancellationSignal != null) {
             try {
                 mCancellationSignal.cancel();
@@ -239,7 +240,7 @@
             Slog.e(TAG, "Remote exception", e);
         }
 
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
         mCallback.onClientFinished(this, false /* success */);
     }
 
@@ -256,7 +257,7 @@
             Slog.e(TAG, "Remote exception", e);
         }
 
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
         mCallback.onClientFinished(this, false /* success */);
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index da91cdd..ac3ce89 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.common.ICancellationSignal;
 import android.hardware.biometrics.fingerprint.ISession;
@@ -31,7 +32,7 @@
 import com.android.server.biometrics.sensors.AcquisitionClient;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.DetectionConsumer;
-import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
+import com.android.server.biometrics.sensors.SensorOverlays;
 
 /**
  * Performs fingerprint detection without exposing any matching information (e.g. accept/reject
@@ -42,8 +43,7 @@
     private static final String TAG = "FingerprintDetectClient";
 
     private final boolean mIsStrongBiometric;
-    @Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
-
+    @NonNull private final SensorOverlays mSensorOverlays;
     @Nullable private ICancellationSignal mCancellationSignal;
 
     FingerprintDetectClient(@NonNull Context context, @NonNull LazyDaemon<ISession> lazyDaemon,
@@ -57,7 +57,7 @@
                 BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient);
         setRequestId(requestId);
         mIsStrongBiometric = isStrongBiometric;
-        mUdfpsOverlayController = udfpsOverlayController;
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController*/);
     }
 
     @Override
@@ -68,7 +68,8 @@
 
     @Override
     protected void stopHalOperation() {
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
+
         try {
             mCancellationSignal.cancel();
         } catch (RemoteException e) {
@@ -79,14 +80,13 @@
 
     @Override
     protected void startHalOperation() {
-        UdfpsHelper.showUdfpsOverlay(getSensorId(),
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD,
-                mUdfpsOverlayController, this);
+        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, this);
+
         try {
             mCancellationSignal = getFreshDaemon().detectInteraction();
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when requesting finger detect", e);
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.hide(getSensorId());
             mCallback.onClientFinished(this, false /* success */);
         }
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index c420c5c..ccb34aa 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -39,8 +39,8 @@
 import com.android.server.biometrics.sensors.BiometricUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.EnrollClient;
+import com.android.server.biometrics.sensors.SensorOverlays;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
-import com.android.server.biometrics.sensors.fingerprint.SidefpsHelper;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
 
@@ -49,8 +49,7 @@
     private static final String TAG = "FingerprintEnrollClient";
 
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
-    @Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
-    @Nullable private final ISidefpsController mSidefpsController;
+    @NonNull private final SensorOverlays mSensorOverlays;
 
     private final @FingerprintManager.EnrollReason int mEnrollReason;
     @Nullable private ICancellationSignal mCancellationSignal;
@@ -63,7 +62,7 @@
             @NonNull byte[] hardwareAuthToken, @NonNull String owner,
             @NonNull BiometricUtils<Fingerprint> utils, int sensorId,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
-            @Nullable IUdfpsOverlayController udfpsOvelayController,
+            @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
             int maxTemplatesPerUser, @FingerprintManager.EnrollReason int enrollReason) {
         // UDFPS haptics occur when an image is acquired (instead of when the result is known)
@@ -71,8 +70,7 @@
                 0 /* timeoutSec */, BiometricsProtoEnums.MODALITY_FINGERPRINT, sensorId,
                 !sensorProps.isAnyUdfpsType() /* shouldVibrate */);
         mSensorProps = sensorProps;
-        mUdfpsOverlayController = udfpsOvelayController;
-        mSidefpsController = sidefpsController;
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mMaxTemplatesPerUser = maxTemplatesPerUser;
 
         mEnrollReason = enrollReason;
@@ -91,11 +89,11 @@
     public void onEnrollResult(BiometricAuthenticator.Identifier identifier, int remaining) {
         super.onEnrollResult(identifier, remaining);
 
-        UdfpsHelper.onEnrollmentProgress(getSensorId(), remaining, mUdfpsOverlayController);
+        mSensorOverlays.ifUdfps(
+                controller -> controller.onEnrollmentProgress(getSensorId(), remaining));
 
         if (remaining == 0) {
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
-            SidefpsHelper.hideOverlay(mSidefpsController);
+            mSensorOverlays.hide(getSensorId());
         }
     }
 
@@ -106,12 +104,14 @@
         if (acquiredInfo == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD
                 && mSensorProps.isAnyUdfpsType()) {
             vibrateSuccess();
-            UdfpsHelper.onAcquiredGood(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.ifUdfps(controller -> controller.onAcquiredGood(getSensorId()));
         }
 
-        if (UdfpsHelper.isValidAcquisitionMessage(getContext(), acquiredInfo, vendorCode)) {
-            UdfpsHelper.onEnrollmentHelp(getSensorId(), mUdfpsOverlayController);
-        }
+        mSensorOverlays.ifUdfps(controller -> {
+            if (UdfpsHelper.isValidAcquisitionMessage(getContext(), acquiredInfo, vendorCode)) {
+                controller.onEnrollmentHelp(getSensorId());
+            }
+        });
 
         super.onAcquired(acquiredInfo, vendorCode);
     }
@@ -120,8 +120,7 @@
     public void onError(int errorCode, int vendorCode) {
         super.onError(errorCode, vendorCode);
 
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
-        SidefpsHelper.hideOverlay(mSidefpsController);
+        mSensorOverlays.hide(getSensorId());
     }
 
     @Override
@@ -133,8 +132,8 @@
 
     @Override
     protected void stopHalOperation() {
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
-        SidefpsHelper.hideOverlay(mSidefpsController);
+        mSensorOverlays.hide(getSensorId());
+
         if (mCancellationSignal != null) {
             try {
                 mCancellationSignal.cancel();
@@ -149,10 +148,8 @@
 
     @Override
     protected void startHalOperation() {
-        UdfpsHelper.showUdfpsOverlay(getSensorId(),
-                UdfpsHelper.getReasonFromEnrollReason(mEnrollReason),
-                mUdfpsOverlayController, this);
-        SidefpsHelper.showOverlay(mSidefpsController);
+        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), this);
+
         BiometricNotificationUtils.cancelBadCalibrationNotification(getContext());
         try {
             mCancellationSignal = getFreshDaemon().enroll(
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index ca83dda..0defc3f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -25,10 +25,12 @@
 import android.app.TaskStackListener;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.content.res.TypedArray;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.IInvalidationCallback;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
+import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.common.ComponentInfo;
 import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.SensorProps;
@@ -145,6 +147,8 @@
         mActivityTaskManager = ActivityTaskManager.getInstance();
         mTaskStackListener = new BiometricTaskStackListener();
 
+        final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context);
+
         for (SensorProps prop : props) {
             final int sensorId = prop.commonProps.sensorId;
 
@@ -164,9 +168,12 @@
                             componentInfo,
                             prop.sensorType,
                             true /* resetLockoutRequiresHardwareAuthToken */,
-                            prop.sensorLocations[0].sensorLocationX,
-                            prop.sensorLocations[0].sensorLocationY,
-                            prop.sensorLocations[0].sensorRadius);
+                            !workaroundLocations.isEmpty() ? workaroundLocations :
+                                    List.of(new SensorLocationInternal(
+                                        "" /* displayId */,
+                                        prop.sensorLocations[0].sensorLocationX,
+                                        prop.sensorLocations[0].sensorLocationY,
+                                        prop.sensorLocations[0].sensorRadius)));
             final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler,
                     internalProp, lockoutResetDispatcher, gestureAvailabilityDispatcher);
 
@@ -403,7 +410,7 @@
                     userId, operationId, restricted, opPackageName, cookie,
                     false /* requireConfirmation */, sensorId, isStrongBiometric, statsClient,
                     mTaskStackListener, mSensors.get(sensorId).getLockoutCache(),
-                    mUdfpsOverlayController, allowBackgroundAuthentication,
+                    mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication,
                     mSensors.get(sensorId).getSensorProperties());
             scheduleForSensor(sensorId, client, mFingerprintStateCallback);
         });
@@ -647,4 +654,45 @@
     void setTestHalEnabled(boolean enabled) {
         mTestHalEnabled = enabled;
     }
+
+    // TODO(b/174868353): workaround for gaps in HAL interface (remove and get directly from HAL)
+    // reads values via an overlay instead of querying the HAL
+    @NonNull
+    private List<SensorLocationInternal> getWorkaroundSensorProps(@NonNull Context context) {
+        final List<SensorLocationInternal> sensorLocations = new ArrayList<>();
+
+        final TypedArray sfpsProps = context.getResources().obtainTypedArray(
+                com.android.internal.R.array.config_sfps_sensor_props);
+        for (int i = 0; i < sfpsProps.length(); i++) {
+            final int id = sfpsProps.getResourceId(i, -1);
+            if (id > 0) {
+                final SensorLocationInternal location = parseSensorLocation(
+                        context.getResources().obtainTypedArray(id));
+                if (location != null) {
+                    sensorLocations.add(location);
+                }
+            }
+        }
+        sfpsProps.recycle();
+
+        return sensorLocations;
+    }
+
+    @Nullable
+    private SensorLocationInternal parseSensorLocation(@Nullable TypedArray array) {
+        if (array == null) {
+            return null;
+        }
+
+        try {
+            return new SensorLocationInternal(
+                    array.getString(0),
+                    array.getInt(1, 0),
+                    array.getInt(2, 0),
+                    array.getInt(3, 0));
+        } catch (Exception e) {
+            Slog.w(getTag(), "malformed sensor location", e);
+        }
+        return null;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index d2882aa..5f2f4cf 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -629,7 +629,8 @@
                     mContext, mLazyDaemon, token, requestId, listener, userId, operationId,
                     restricted, opPackageName, cookie, false /* requireConfirmation */,
                     mSensorProperties.sensorId, isStrongBiometric, statsClient,
-                    mTaskStackListener, mLockoutTracker, mUdfpsOverlayController,
+                    mTaskStackListener, mLockoutTracker,
+                    mUdfpsOverlayController, mSidefpsController,
                     allowBackgroundAuthentication, mSensorProperties);
             mScheduler.scheduleClientMonitor(client, mFingerprintStateCallback);
         });
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
index 79ad8e1..dd68b4d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
@@ -426,8 +426,7 @@
         mSensorProperties = new FingerprintSensorPropertiesInternal(sensorProps.sensorId,
                 sensorProps.sensorStrength, maxTemplatesAllowed, sensorProps.componentInfo,
                 FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
-                resetLockoutRequiresHardwareAuthToken, sensorProps.sensorLocationX,
-                sensorProps.sensorLocationY, sensorProps.sensorRadius);
+                resetLockoutRequiresHardwareAuthToken, sensorProps.getAllLocations());
         mMockHalResultController = controller;
         mUserHasTrust = new SparseBooleanArray();
         mTrustManager = context.getSystemService(TrustManager.class);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 7d95ec0..3058e25 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -26,16 +26,17 @@
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
 
-import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.SensorOverlays;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
 
@@ -52,7 +53,7 @@
     private static final String TAG = "Biometrics/FingerprintAuthClient";
 
     private final LockoutFrameworkImpl mLockoutFrameworkImpl;
-    @Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
+    @NonNull private final SensorOverlays mSensorOverlays;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
     @NonNull private final CallbackWithProbe<Probe> mALSProbeCallback;
 
@@ -67,6 +68,7 @@
             @NonNull TaskStackListener taskStackListener,
             @NonNull LockoutFrameworkImpl lockoutTracker,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
+            @Nullable ISidefpsController sidefpsController,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps) {
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
@@ -76,7 +78,7 @@
                 false /* isKeyguardBypassEnabled */);
         setRequestId(requestId);
         mLockoutFrameworkImpl = lockoutTracker;
-        mUdfpsOverlayController = udfpsOverlayController;
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mSensorProps = sensorProps;
         mALSProbeCallback = createALSCallback(false /* startWithClient */);
     }
@@ -112,7 +114,7 @@
         if (authenticated) {
             mState = STATE_STOPPED;
             resetFailedAttempts(getTargetUserId());
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.hide(getSensorId());
         } else {
             mState = STATE_STARTED_PAUSED_ATTEMPTED;
             final @LockoutTracker.LockoutMode int lockoutMode =
@@ -125,7 +127,7 @@
                 // Send the error, but do not invoke the FinishCallback yet. Since lockout is not
                 // controlled by the HAL, the framework must stop the sensor before finishing the
                 // client.
-                UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+                mSensorOverlays.hide(getSensorId());
                 onErrorInternal(errorCode, 0 /* vendorCode */, false /* finish */);
                 cancel();
             }
@@ -140,7 +142,7 @@
             BiometricNotificationUtils.showBadCalibrationNotification(getContext());
         }
 
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
     }
 
     private void resetFailedAttempts(int userId) {
@@ -168,8 +170,8 @@
 
     @Override
     protected void startHalOperation() {
-        UdfpsHelper.showUdfpsOverlay(getSensorId(), Utils.getUdfpsAuthReason(this),
-                mUdfpsOverlayController, this);
+        mSensorOverlays.show(getSensorId(), getShowOverlayReason(), this);
+
         try {
             // GroupId was never used. In fact, groupId is always the same as userId.
             getFreshDaemon().authenticate(mOperationId, getTargetUserId());
@@ -177,14 +179,15 @@
             Slog.e(TAG, "Remote exception when requesting auth", e);
             onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.hide(getSensorId());
             mCallback.onClientFinished(this, false /* success */);
         }
     }
 
     @Override
     protected void stopHalOperation() {
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
+
         try {
             getFreshDaemon().cancel();
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
index 147a206..b854fb3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricFingerprintConstants;
+import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -33,6 +34,7 @@
 import com.android.server.biometrics.sensors.AuthenticationConsumer;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.PerformanceTracker;
+import com.android.server.biometrics.sensors.SensorOverlays;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
 
@@ -48,7 +50,7 @@
     private static final String TAG = "FingerprintDetectClient";
 
     private final boolean mIsStrongBiometric;
-    @Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
+    @NonNull private final SensorOverlays mSensorOverlays;
     private boolean mIsPointerDown;
 
     public FingerprintDetectClient(@NonNull Context context,
@@ -61,13 +63,14 @@
                 true /* shouldVibrate */, BiometricsProtoEnums.MODALITY_FINGERPRINT,
                 BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient);
         setRequestId(requestId);
-        mUdfpsOverlayController = udfpsOverlayController;
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController */);
         mIsStrongBiometric = isStrongBiometric;
     }
 
     @Override
     protected void stopHalOperation() {
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+        mSensorOverlays.hide(getSensorId());
+
         try {
             getFreshDaemon().cancel();
         } catch (RemoteException e) {
@@ -86,16 +89,15 @@
 
     @Override
     protected void startHalOperation() {
-        UdfpsHelper.showUdfpsOverlay(getSensorId(),
-                IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD,
-                mUdfpsOverlayController, this);
+        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, this);
+
         try {
             getFreshDaemon().authenticate(0 /* operationId */, getTargetUserId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when requesting auth", e);
             onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
+            mSensorOverlays.hide(getSensorId());
             mCallback.onClientFinished(this, false /* success */);
         }
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
index dc70534..1ebf44c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
@@ -35,7 +35,7 @@
 import com.android.server.biometrics.sensors.BiometricUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.EnrollClient;
-import com.android.server.biometrics.sensors.fingerprint.SidefpsHelper;
+import com.android.server.biometrics.sensors.SensorOverlays;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
 
@@ -49,8 +49,7 @@
 
     private static final String TAG = "FingerprintEnrollClient";
 
-    @Nullable private final IUdfpsOverlayController mUdfpsOverlayController;
-    @Nullable private final ISidefpsController mSidefpsController;
+    @NonNull private final SensorOverlays mSensorOverlays;
     private final @FingerprintManager.EnrollReason int mEnrollReason;
     private boolean mIsPointerDown;
 
@@ -65,8 +64,7 @@
         super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils,
                 timeoutSec, BiometricsProtoEnums.MODALITY_FINGERPRINT, sensorId,
                 true /* shouldVibrate */);
-        mUdfpsOverlayController = udfpsOverlayController;
-        mSidefpsController = sidefpsController;
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
 
         mEnrollReason = enrollReason;
         if (enrollReason == FingerprintManager.ENROLL_FIND_SENSOR) {
@@ -95,10 +93,8 @@
 
     @Override
     protected void startHalOperation() {
-        UdfpsHelper.showUdfpsOverlay(getSensorId(),
-                UdfpsHelper.getReasonFromEnrollReason(mEnrollReason),
-                mUdfpsOverlayController, this);
-        SidefpsHelper.showOverlay(mSidefpsController);
+        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), this);
+
         BiometricNotificationUtils.cancelBadCalibrationNotification(getContext());
         try {
             // GroupId was never used. In fact, groupId is always the same as userId.
@@ -107,16 +103,15 @@
             Slog.e(TAG, "Remote exception when requesting enroll", e);
             onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
-            SidefpsHelper.hideOverlay(mSidefpsController);
+            mSensorOverlays.hide(getSensorId());
             mCallback.onClientFinished(this, false /* success */);
         }
     }
 
     @Override
     protected void stopHalOperation() {
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
-        SidefpsHelper.hideOverlay(mSidefpsController);
+        mSensorOverlays.hide(getSensorId());
+
         try {
             getFreshDaemon().cancel();
         } catch (RemoteException e) {
@@ -131,11 +126,11 @@
     public void onEnrollResult(BiometricAuthenticator.Identifier identifier, int remaining) {
         super.onEnrollResult(identifier, remaining);
 
-        UdfpsHelper.onEnrollmentProgress(getSensorId(), remaining, mUdfpsOverlayController);
+        mSensorOverlays.ifUdfps(
+                controller -> controller.onEnrollmentProgress(getSensorId(), remaining));
 
         if (remaining == 0) {
-            UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
-            SidefpsHelper.hideOverlay(mSidefpsController);
+            mSensorOverlays.hide(getSensorId());
         }
     }
 
@@ -143,17 +138,18 @@
     public void onAcquired(int acquiredInfo, int vendorCode) {
         super.onAcquired(acquiredInfo, vendorCode);
 
-        if (UdfpsHelper.isValidAcquisitionMessage(getContext(), acquiredInfo, vendorCode)) {
-            UdfpsHelper.onEnrollmentHelp(getSensorId(), mUdfpsOverlayController);
-        }
+        mSensorOverlays.ifUdfps(controller -> {
+            if (UdfpsHelper.isValidAcquisitionMessage(getContext(), acquiredInfo, vendorCode)) {
+                controller.onEnrollmentHelp(getSensorId());
+            }
+        });
     }
 
     @Override
     public void onError(int errorCode, int vendorCode) {
         super.onError(errorCode, vendorCode);
 
-        UdfpsHelper.hideUdfpsOverlay(getSensorId(), mUdfpsOverlayController);
-        SidefpsHelper.hideOverlay(mSidefpsController);
+        mSensorOverlays.hide(getSensorId());
     }
 
     @Override
diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
index 54a4ad4..23f0ffb 100644
--- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
@@ -52,11 +52,11 @@
     public BroadcastRadioService(Context context) {
         super(context);
 
-        mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService();
+        mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock);
         mV1Modules = mHal1.loadModules();
         OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max();
         mHal2 = new com.android.server.broadcastradio.hal2.BroadcastRadioService(
-                max.isPresent() ? max.getAsInt() + 1 : 0);
+                max.isPresent() ? max.getAsInt() + 1 : 0, mLock);
     }
 
     @Override
@@ -111,7 +111,7 @@
             synchronized (mLock) {
                 if (!mHal2.hasAnyModules()) {
                     Slog.i(TAG, "There are no HAL 2.x modules registered");
-                    return new AnnouncementAggregator(listener);
+                    return new AnnouncementAggregator(listener, mLock);
                 }
 
                 return mHal2.addAnnouncementListener(enabledTypes, listener);
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal1/BroadcastRadioService.java
index e8ac547..5da6032 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/BroadcastRadioService.java
@@ -17,16 +17,9 @@
 package com.android.server.broadcastradio.hal1;
 
 import android.annotation.NonNull;
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.hardware.radio.IRadioService;
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ITunerCallback;
 import android.hardware.radio.RadioManager;
-import android.os.ParcelableException;
-
-import com.android.server.SystemService;
 
 import java.util.List;
 import java.util.Objects;
@@ -37,7 +30,7 @@
      */
     private final long mNativeContext = nativeInit();
 
-    private final Object mLock = new Object();
+    private final Object mLock;
 
     @Override
     protected void finalize() throws Throwable {
@@ -51,6 +44,14 @@
     private native Tuner nativeOpenTuner(long nativeContext, int moduleId,
             RadioManager.BandConfig config, boolean withAudio, ITunerCallback callback);
 
+    /**
+     * Constructor. should pass
+     * {@code com.android.server.broadcastradio.BroadcastRadioService#mLock} for lock.
+     */
+    public BroadcastRadioService(@NonNull Object lock) {
+        mLock = lock;
+    }
+
     public @NonNull List<RadioManager.ModuleProperties> loadModules() {
         synchronized (mLock) {
             return Objects.requireNonNull(nativeLoadModules(mNativeContext));
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
index 5307697..42e296f 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
@@ -35,7 +35,7 @@
 public class AnnouncementAggregator extends ICloseHandle.Stub {
     private static final String TAG = "BcRadio2Srv.AnnAggr";
 
-    private final Object mLock = new Object();
+    private final Object mLock;
     @NonNull private final IAnnouncementListener mListener;
     private final IBinder.DeathRecipient mDeathRecipient = new DeathRecipient();
 
@@ -45,8 +45,9 @@
     @GuardedBy("mLock")
     private boolean mIsClosed = false;
 
-    public AnnouncementAggregator(@NonNull IAnnouncementListener listener) {
+    public AnnouncementAggregator(@NonNull IAnnouncementListener listener, @NonNull Object lock) {
         mListener = Objects.requireNonNull(listener);
+        mLock = Objects.requireNonNull(lock);
         try {
             listener.asBinder().linkToDeath(mDeathRecipient, 0);
         } catch (RemoteException ex) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index 5e79c59..5c07f76 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -42,7 +42,7 @@
 public class BroadcastRadioService {
     private static final String TAG = "BcRadio2Srv";
 
-    private final Object mLock = new Object();
+    private final Object mLock;
 
     @GuardedBy("mLock")
     private int mNextModuleId = 0;
@@ -68,7 +68,7 @@
                     moduleId = mNextModuleId;
                 }
 
-                RadioModule module = RadioModule.tryLoadingModule(moduleId, serviceName);
+                RadioModule module = RadioModule.tryLoadingModule(moduleId, serviceName, mLock);
                 if (module == null) {
                     return;
                 }
@@ -116,8 +116,9 @@
         }
     };
 
-    public BroadcastRadioService(int nextModuleId) {
+    public BroadcastRadioService(int nextModuleId, Object lock) {
         mNextModuleId = nextModuleId;
+        mLock = lock;
         try {
             IServiceManager manager = IServiceManager.getService();
             if (manager == null) {
@@ -174,7 +175,7 @@
 
     public ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes,
             @NonNull IAnnouncementListener listener) {
-        AnnouncementAggregator aggregator = new AnnouncementAggregator(listener);
+        AnnouncementAggregator aggregator = new AnnouncementAggregator(listener, mLock);
         boolean anySupported = false;
         synchronized (mLock) {
             for (RadioModule module : mModules.values()) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index b7e188c..ef7f4c9 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -58,7 +58,7 @@
     @NonNull private final IBroadcastRadio mService;
     @NonNull public final RadioManager.ModuleProperties mProperties;
 
-    private final Object mLock = new Object();
+    private final Object mLock;
     @NonNull private final Handler mHandler;
 
     @GuardedBy("mLock")
@@ -132,13 +132,15 @@
 
     @VisibleForTesting
     RadioModule(@NonNull IBroadcastRadio service,
-            @NonNull RadioManager.ModuleProperties properties) {
+            @NonNull RadioManager.ModuleProperties properties, @NonNull Object lock) {
         mProperties = Objects.requireNonNull(properties);
         mService = Objects.requireNonNull(service);
+        mLock = Objects.requireNonNull(lock);
         mHandler = new Handler(Looper.getMainLooper());
     }
 
-    public static @Nullable RadioModule tryLoadingModule(int idx, @NonNull String fqName) {
+    public static @Nullable RadioModule tryLoadingModule(int idx, @NonNull String fqName,
+            Object lock) {
         try {
             IBroadcastRadio service = IBroadcastRadio.getService(fqName);
             if (service == null) return null;
@@ -156,7 +158,7 @@
             RadioManager.ModuleProperties prop = Convert.propertiesFromHal(idx, fqName,
                     service.getProperties(), amfmConfig.value, dabConfig.value);
 
-            return new RadioModule(service, prop);
+            return new RadioModule(service, prop, lock);
         } catch (RemoteException ex) {
             Slog.e(TAG, "failed to load module " + fqName, ex);
             return null;
@@ -178,7 +180,8 @@
                 });
                 mHalTunerSession = Objects.requireNonNull(hwSession.value);
             }
-            TunerSession tunerSession = new TunerSession(this, mHalTunerSession, userCb);
+            TunerSession tunerSession = new TunerSession(this, mHalTunerSession, userCb,
+                    mLock);
             mAidlTunerSessions.add(tunerSession);
 
             // Propagate state to new client. Note: These callbacks are invoked while holding mLock
@@ -377,7 +380,7 @@
             }
         };
 
-        synchronized (mService) {
+        synchronized (mLock) {
             mService.registerAnnouncementListener(enabledList, hwListener, (result, closeHnd) -> {
                 halResult.value = result;
                 hwCloseHandle.value = closeHnd;
@@ -401,7 +404,7 @@
         if (id == 0) throw new IllegalArgumentException("Image ID is missing");
 
         byte[] rawImage;
-        synchronized (mService) {
+        synchronized (mLock) {
             List<Byte> rawList = Utils.maybeRethrow(() -> mService.getImage(id));
             rawImage = new byte[rawList.size()];
             for (int i = 0; i < rawList.size(); i++) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
index 7d8c6a4..d476fd6 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
@@ -40,7 +40,7 @@
     private static final String TAG = "BcRadio2Srv.session";
     private static final String kAudioDeviceName = "Radio tuner source";
 
-    private final Object mLock = new Object();
+    private final Object mLock;
 
     private final RadioModule mModule;
     private final ITunerSession mHwSession;
@@ -53,10 +53,12 @@
     private RadioManager.BandConfig mDummyConfig = null;
 
     TunerSession(@NonNull RadioModule module, @NonNull ITunerSession hwSession,
-            @NonNull android.hardware.radio.ITunerCallback callback) {
+            @NonNull android.hardware.radio.ITunerCallback callback,
+            @NonNull Object lock) {
         mModule = Objects.requireNonNull(module);
         mHwSession = Objects.requireNonNull(hwSession);
         mCallback = Objects.requireNonNull(callback);
+        mLock = Objects.requireNonNull(lock);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index b2d35f4..42b676f 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -20,9 +20,14 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
+import android.app.compat.CompatChanges;
 import android.app.TaskStackListener;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
+import android.compat.annotation.Overridable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -35,17 +40,21 @@
 import android.hardware.ICameraService;
 import android.hardware.ICameraServiceProxy;
 import android.hardware.camera2.CameraMetadata;
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.hardware.display.DisplayManager;
 import android.media.AudioManager;
 import android.nfc.INfcAdapter;
 import android.os.Binder;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.stats.camera.nano.CameraProtos.CameraStreamProto;
 import android.util.ArrayMap;
@@ -57,8 +66,8 @@
 import android.view.Surface;
 import android.view.WindowManagerGlobal;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.framework.protobuf.nano.MessageNano;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
@@ -94,6 +103,95 @@
 
     public static final String CAMERA_SERVICE_PROXY_BINDER_NAME = "media.camera.proxy";
 
+    /**
+     * When enabled this change id forces the packages it is applied to override the default
+     * camera rotate & crop behavior. The default behavior along with all possible override
+     * combinations is discussed in the table below.
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    @TestApi
+    public static final long OVERRIDE_CAMERA_ROTATE_AND_CROP_DEFAULTS = 189229956L; // buganizer id
+
+    /**
+     * When enabled this change id forces the packages it is applied to ignore the current value of
+     * 'android:resizeableActivity' as well as target SDK equal to or below M and consider the
+     * activity as non-resizeable. In this case, the value of camera rotate & crop will only depend
+     * on potential mismatches between the orientation of the camera and the fixed orientation of
+     * the activity. You can check the table below for further details on the possible override
+     * combinations.
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    @TestApi
+    public static final long OVERRIDE_CAMERA_RESIZABLE_AND_SDK_CHECK = 191513214L; // buganizer id
+
+    /**
+     * This change id forces the packages it is applied to override the default camera rotate & crop
+     * behavior. Enabling it will set the crop & rotate parameter to
+     * {@link android.hardware.camera2.CaptureRequest#SCALER_ROTATE_AND_CROP_90} and disabling it
+     * will reset the parameter to
+     * {@link android.hardware.camera2.CaptureRequest#SCALER_ROTATE_AND_CROP_NONE} as long as camera
+     * clients include {@link android.hardware.camera2.CaptureRequest#SCALER_ROTATE_AND_CROP_AUTO}
+     * in their capture requests.
+     *
+     * This treatment only takes effect if OVERRIDE_CAMERA_ROTATE_AND_CROP_DEFAULTS is also enabled.
+     * The table below includes further information about the possible override combinations.
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    @TestApi
+    public static final long OVERRIDE_CAMERA_ROTATE_AND_CROP = 190069291L; //buganizer id
+
+    /**
+     * Possible override combinations
+     *
+     *            |OVERRIDE     |          |OVERRIDE_
+     *            |CAMERA_      |OVERRIDE  |CAMERA_
+     *            |ROTATE_      |CAMERA_   |RESIZEABLE_
+     *            |AND_CROP_    |ROTATE_   |AND_SDK_
+     *            |DEFAULTS     |AND_CROP  |CHECK
+     * ______________________________________________
+     * Default    |             |          |
+     * Behavior   | D           |D         |D
+     * ______________________________________________
+     * Ignore     |             |          |
+     * SDK&Resize | D           |D         |E
+     * ______________________________________________
+     * Default    |             |          |
+     * Behavior   | D           |E         |D
+     * ______________________________________________
+     * Ignore     |             |          |
+     * SDK&Resize | D           |E         |E
+     * ______________________________________________
+     * Rotate&Crop|             |          |
+     * disabled   | E           |D         |D
+     * ______________________________________________
+     * Rotate&Crop|             |          |
+     * disabled   | E           |D         |E
+     * ______________________________________________
+     * Rotate&Crop|             |          |
+     * enabled    | E           |E         |D
+     * ______________________________________________
+     * Rotate&Crop|             |          |
+     * enabled    | E           |E         |E
+     * ______________________________________________
+     * Where:
+     * E -> Override enabled
+     * D -> Override disabled
+     * Default behavior ->  Rotate&crop will be enabled only in cases
+     *                      where the fixed app orientation mismatches
+     *                      with the orientation of the camera.
+     *                      Additionally the app must either target M (or below)
+     *                      or is declared as non-resizeable.
+     * Ignore SDK&Resize -> Rotate&crop will be enabled only in cases
+     *                      where the fixed app orientation mismatches
+     *                      with the orientation of the camera.
+     */
+
     // Flags arguments to NFC adapter to enable/disable NFC
     public static final int DISABLE_POLLING_FLAGS = 0x1000;
     public static final int ENABLE_POLLING_FLAGS = 0x0000;
@@ -254,6 +352,7 @@
         private boolean isFixedOrientationLandscape;
         private boolean isFixedOrientationPortrait;
         private int displayId;
+        private int userId;
     }
 
     private final class TaskStateHandler extends TaskStackListener {
@@ -270,6 +369,7 @@
                 info.frontTaskId = taskInfo.taskId;
                 info.isResizeable = taskInfo.isResizeable;
                 info.displayId = taskInfo.displayId;
+                info.userId = taskInfo.userId;
                 info.isFixedOrientationLandscape = ActivityInfo.isFixedOrientationLandscape(
                         taskInfo.topActivityInfo.screenOrientation);
                 info.isFixedOrientationPortrait = ActivityInfo.isFixedOrientationPortrait(
@@ -300,7 +400,7 @@
             Log.e(TAG, "Top task with package name: " + packageName + " not found!");
             return null;
         }
-    };
+    }
 
     private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
         @Override
@@ -342,7 +442,8 @@
          * Gets whether crop-rotate-scale is needed.
          */
         private boolean getNeedCropRotateScale(@NonNull Context ctx, @NonNull String packageName,
-                @Nullable TaskInfo taskInfo, int sensorOrientation, int lensFacing) {
+                @Nullable TaskInfo taskInfo, int sensorOrientation, int lensFacing,
+                boolean ignoreResizableAndSdkCheck) {
             if (taskInfo == null) {
                 return false;
             }
@@ -354,9 +455,11 @@
                 return false;
             }
 
-            // Only enable the crop-rotate-scale workaround if the app targets M or below and is not
+            // In case the activity behavior is not explicitly overridden, enable the
+            // crop-rotate-scale workaround if the app targets M (or below) or is not
             // resizeable.
-            if (!isMOrBelow(ctx, packageName) && taskInfo.isResizeable) {
+            if (!ignoreResizableAndSdkCheck && !isMOrBelow(ctx, packageName) &&
+                    taskInfo.isResizeable) {
                 Slog.v(TAG,
                         "The activity is N or above and claims to support resizeable-activity. "
                                 + "Crop-rotate-scale is disabled.");
@@ -364,22 +467,32 @@
             }
 
             DisplayManager displayManager = ctx.getSystemService(DisplayManager.class);
-            Display display = displayManager.getDisplay(taskInfo.displayId);
-            int rotation = display.getRotation();
             int rotationDegree = 0;
-            switch (rotation) {
-                case Surface.ROTATION_0:
-                    rotationDegree = 0;
-                    break;
-                case Surface.ROTATION_90:
-                    rotationDegree = 90;
-                    break;
-                case Surface.ROTATION_180:
-                    rotationDegree = 180;
-                    break;
-                case Surface.ROTATION_270:
-                    rotationDegree = 270;
-                    break;
+            if (displayManager != null) {
+                Display display = displayManager.getDisplay(taskInfo.displayId);
+                if (display == null) {
+                    Slog.e(TAG, "Invalid display id: " + taskInfo.displayId);
+                    return false;
+                }
+
+                int rotation = display.getRotation();
+                switch (rotation) {
+                    case Surface.ROTATION_0:
+                        rotationDegree = 0;
+                        break;
+                    case Surface.ROTATION_90:
+                        rotationDegree = 90;
+                        break;
+                    case Surface.ROTATION_180:
+                        rotationDegree = 180;
+                        break;
+                    case Surface.ROTATION_270:
+                        rotationDegree = 270;
+                        break;
+                }
+            } else {
+                Slog.e(TAG, "Failed to query display manager!");
+                return false;
             }
 
             // Here we only need to know whether the camera is landscape or portrait. Therefore we
@@ -411,9 +524,28 @@
             //  regions in capture requests/results to account for thea physical rotation. The
             //  former is somewhat tricky as it assumes that camera clients always check for the
             //  current value by retrieving the camera characteristics from the camera device.
-            return getNeedCropRotateScale(mContext, packageName,
-                    mTaskStackListener.getFrontTaskInfo(packageName), sensorOrientation,
-                    lensFacing);
+            TaskInfo taskInfo = mTaskStackListener.getFrontTaskInfo(packageName);
+            if ((taskInfo != null) && (CompatChanges.isChangeEnabled(
+                        OVERRIDE_CAMERA_ROTATE_AND_CROP_DEFAULTS, packageName,
+                        UserHandle.getUserHandleForUid(taskInfo.userId)))) {
+                if (CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_ROTATE_AND_CROP, packageName,
+                        UserHandle.getUserHandleForUid(taskInfo.userId))) {
+                    Slog.v(TAG, "OVERRIDE_CAMERA_ROTATE_AND_CROP enabled!");
+                    return true;
+                } else {
+                    Slog.v(TAG, "OVERRIDE_CAMERA_ROTATE_AND_CROP disabled!");
+                    return false;
+                }
+            }
+            boolean ignoreResizableAndSdkCheck = false;
+            if ((taskInfo != null) && (CompatChanges.isChangeEnabled(
+                    OVERRIDE_CAMERA_RESIZABLE_AND_SDK_CHECK, packageName,
+                    UserHandle.getUserHandleForUid(taskInfo.userId)))) {
+                Slog.v(TAG, "OVERRIDE_CAMERA_RESIZABLE_AND_SDK_CHECK enabled!");
+                ignoreResizableAndSdkCheck = true;
+            }
+            return getNeedCropRotateScale(mContext, packageName, taskInfo, sensorOrientation,
+                    lensFacing, ignoreResizableAndSdkCheck);
         }
 
         @Override
@@ -447,6 +579,8 @@
         }
     };
 
+    private final FoldStateListener mFoldStateListener;
+
     public CameraServiceProxy(Context context) {
         super(context);
         mContext = context;
@@ -459,6 +593,14 @@
         // Don't keep any extra logging threads if not needed
         mLogWriterService.setKeepAliveTime(1, TimeUnit.SECONDS);
         mLogWriterService.allowCoreThreadTimeOut(true);
+
+        mFoldStateListener = new FoldStateListener(mContext, folded -> {
+            if (folded) {
+                setDeviceStateFlags(ICameraService.DEVICE_STATE_FOLDED);
+            } else {
+                clearDeviceStateFlags(ICameraService.DEVICE_STATE_FOLDED);
+            }
+        });
     }
 
     /**
@@ -471,7 +613,7 @@
      *
      * @see #clearDeviceStateFlags(int)
      */
-    public void setDeviceStateFlags(@DeviceStateFlags int deviceStateFlags) {
+    private void setDeviceStateFlags(@DeviceStateFlags int deviceStateFlags) {
         synchronized (mLock) {
             mHandler.removeMessages(MSG_NOTIFY_DEVICE_STATE);
             mDeviceState |= deviceStateFlags;
@@ -491,7 +633,7 @@
      *
      * @see #setDeviceStateFlags(int)
      */
-    public void clearDeviceStateFlags(@DeviceStateFlags int deviceStateFlags) {
+    private void clearDeviceStateFlags(@DeviceStateFlags int deviceStateFlags) {
         synchronized (mLock) {
             mHandler.removeMessages(MSG_NOTIFY_DEVICE_STATE);
             mDeviceState &= ~deviceStateFlags;
@@ -550,11 +692,17 @@
             }
 
             try {
-                WindowManagerGlobal.getWindowManagerService().registerDisplayWindowListener(
-                        mDisplayWindowListener);
+                int[] displayIds = WindowManagerGlobal.getWindowManagerService()
+                        .registerDisplayWindowListener(mDisplayWindowListener);
+                for (int i = 0; i < displayIds.length; i++) {
+                    mDisplayWindowListener.onDisplayAdded(displayIds[i]);
+                }
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to register display window listener!");
             }
+
+            mContext.getSystemService(DeviceStateManager.class)
+                    .registerCallback(new HandlerExecutor(mHandler), mFoldStateListener);
         }
     }
 
diff --git a/services/core/java/com/android/server/compat/CompatChange.java b/services/core/java/com/android/server/compat/CompatChange.java
index 0f97b90..b5846b5 100644
--- a/services/core/java/com/android/server/compat/CompatChange.java
+++ b/services/core/java/com/android/server/compat/CompatChange.java
@@ -36,9 +36,9 @@
 import com.android.server.compat.overrides.OverrideValue;
 import com.android.server.compat.overrides.RawOverrideValue;
 
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Represents the state of a single compatibility change.
@@ -82,8 +82,8 @@
 
     ChangeListener mListener = null;
 
-    private Map<String, Boolean> mEvaluatedOverrides;
-    private Map<String, PackageOverride> mRawOverrides;
+    private ConcurrentHashMap<String, Boolean> mEvaluatedOverrides;
+    private ConcurrentHashMap<String, PackageOverride> mRawOverrides;
 
     public CompatChange(long changeId) {
         this(changeId, null, -1, -1, false, false, null, false);
@@ -114,11 +114,11 @@
               description, overridable);
 
         // Initialize override maps.
-        mEvaluatedOverrides = new HashMap<>();
-        mRawOverrides = new HashMap<>();
+        mEvaluatedOverrides = new ConcurrentHashMap<>();
+        mRawOverrides = new ConcurrentHashMap<>();
     }
 
-    void registerListener(ChangeListener listener) {
+    synchronized void registerListener(ChangeListener listener) {
         if (mListener != null) {
             throw new IllegalStateException(
                     "Listener for change " + toString() + " already registered.");
@@ -131,8 +131,6 @@
      * Force the enabled state of this change for a given package name. The change will only take
      * effect after that packages process is killed and restarted.
      *
-     * <p>Note, this method is not thread safe so callers must ensure thread safety.
-     *
      * @param pname Package name to enable the change for.
      * @param enabled Whether or not to enable the change.
      */
@@ -155,14 +153,12 @@
      * Tentatively set the state of this change for a given package name.
      * The override will only take effect after that package is installed, if applicable.
      *
-     * <p>Note, this method is not thread safe so callers must ensure thread safety.
-     *
      * @param packageName Package name to tentatively enable the change for.
      * @param override The package override to be set
      * @param allowedState Whether the override is allowed.
      * @param versionCode The version code of the package.
      */
-    void addPackageOverride(String packageName, PackageOverride override,
+    synchronized void addPackageOverride(String packageName, PackageOverride override,
             OverrideAllowedState allowedState, @Nullable Long versionCode) {
         if (getLoggingOnly()) {
             throw new IllegalArgumentException(
@@ -185,16 +181,17 @@
      * @return {@code true} if the recheck yielded a result that requires invalidating caches
      *         (a deferred override was consolidated or a regular override was removed).
      */
-    boolean recheckOverride(String packageName, OverrideAllowedState allowedState,
+    synchronized boolean recheckOverride(String packageName, OverrideAllowedState allowedState,
             @Nullable Long versionCode) {
+        if (packageName == null) {
+            return false;
+        }
         boolean allowed = (allowedState.state == OverrideAllowedState.ALLOWED);
-
         // If the app is not installed or no longer has raw overrides, evaluate to false
-        if (versionCode == null || !hasRawOverride(packageName) || !allowed) {
+        if (versionCode == null || !mRawOverrides.containsKey(packageName) || !allowed) {
             removePackageOverrideInternal(packageName);
             return false;
         }
-
         // Evaluate the override based on its version
         int overrideValue = mRawOverrides.get(packageName).evaluate(versionCode);
         switch (overrideValue) {
@@ -211,10 +208,6 @@
         return true;
     }
 
-    boolean hasPackageOverride(String pname) {
-        return mRawOverrides.containsKey(pname);
-    }
-
     /**
      * Remove any package override for the given package name, restoring the default behaviour.
      *
@@ -224,9 +217,11 @@
      * @param allowedState Whether the override is allowed.
      * @param versionCode The version code of the package.
      */
-    boolean removePackageOverride(String pname, OverrideAllowedState allowedState,
+    synchronized boolean removePackageOverride(String pname, OverrideAllowedState allowedState,
             @Nullable Long versionCode) {
-        if (mRawOverrides.remove(pname) != null) {
+        if (mRawOverrides.containsKey(pname)) {
+            allowedState.enforce(getId(), pname);
+            mRawOverrides.remove(pname);
             recheckOverride(pname, allowedState, versionCode);
             return true;
         }
@@ -244,8 +239,11 @@
         if (app == null) {
             return defaultValue();
         }
-        if (mEvaluatedOverrides.containsKey(app.packageName)) {
-            return mEvaluatedOverrides.get(app.packageName);
+        if (app.packageName != null) {
+            final Boolean enabled = mEvaluatedOverrides.get(app.packageName);
+            if (enabled != null) {
+                return enabled;
+            }
         }
         if (getDisabled()) {
             return false;
@@ -269,9 +267,12 @@
      * @return {@code true} if the change should be enabled for the package.
      */
     boolean willBeEnabled(String packageName) {
-        if (hasRawOverride(packageName)) {
-            int eval = mRawOverrides.get(packageName).evaluateForAllVersions();
-            switch (eval) {
+        if (packageName == null) {
+            return defaultValue();
+        }
+        final PackageOverride override = mRawOverrides.get(packageName);
+        if (override != null) {
+            switch (override.evaluateForAllVersions()) {
                 case VALUE_ENABLED:
                     return true;
                 case VALUE_DISABLED:
@@ -292,30 +293,12 @@
         return !getDisabled();
     }
 
-    /**
-     * Checks whether a change has an override for a package.
-     * @param packageName name of the package
-     * @return true if there is such override
-     */
-    private boolean hasOverride(String packageName) {
-        return mEvaluatedOverrides.containsKey(packageName);
-    }
-
-    /**
-     * Checks whether a change has a deferred override for a package.
-     * @param packageName name of the package
-     * @return true if there is such a deferred override
-     */
-    private boolean hasRawOverride(String packageName) {
-        return mRawOverrides.containsKey(packageName);
-    }
-
-    void clearOverrides() {
+    synchronized void clearOverrides() {
         mRawOverrides.clear();
         mEvaluatedOverrides.clear();
     }
 
-    void loadOverrides(ChangeOverrides changeOverrides) {
+    synchronized void loadOverrides(ChangeOverrides changeOverrides) {
         // Load deferred overrides for backwards compatibility
         if (changeOverrides.getDeferred() != null) {
             for (OverrideValue override : changeOverrides.getDeferred().getOverrideValue()) {
@@ -348,7 +331,7 @@
         }
     }
 
-    ChangeOverrides saveOverrides() {
+    synchronized ChangeOverrides saveOverrides() {
         if (mRawOverrides.isEmpty()) {
             return null;
         }
@@ -406,7 +389,7 @@
         return sb.append(")").toString();
     }
 
-    private void notifyListener(String packageName) {
+    private synchronized void notifyListener(String packageName) {
         if (mListener != null) {
             mListener.onCompatChange(packageName);
         }
diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java
index 3faffe1..2bf1ccd 100644
--- a/services/core/java/com/android/server/compat/CompatConfig.java
+++ b/services/core/java/com/android/server/compat/CompatConfig.java
@@ -28,7 +28,6 @@
 import android.os.Environment;
 import android.text.TextUtils;
 import android.util.LongArray;
-import android.util.LongSparseArray;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
@@ -55,11 +54,12 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import javax.xml.datatype.DatatypeConfigurationException;
 
@@ -76,9 +76,7 @@
     private static final String STATIC_OVERRIDES_PRODUCT_DIR = "/product/etc/appcompat";
     private static final String OVERRIDES_FILE = "compat_framework_overrides.xml";
 
-    private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
-    @GuardedBy("mReadWriteLock")
-    private final LongSparseArray<CompatChange> mChanges = new LongSparseArray<>();
+    private final ConcurrentHashMap<Long, CompatChange> mChanges = new ConcurrentHashMap<>();
 
     private final OverrideValidatorImpl mOverrideValidator;
     private final AndroidBuildClassifier mAndroidBuildClassifier;
@@ -113,21 +111,13 @@
     /**
      * Adds a change.
      *
-     * <p>This is intended to be used by code that reads change config from the filesystem. This
-     * should be done at system startup time.
-     *
-     * <p>Any change with the same ID will be overwritten.
+     * <p>This is intended to be used by unit tests only.
      *
      * @param change the change to add
      */
+    @VisibleForTesting
     void addChange(CompatChange change) {
-        mReadWriteLock.writeLock().lock();
-        try {
-            mChanges.put(change.getId(), change);
-            invalidateCache();
-        } finally {
-            mReadWriteLock.writeLock().unlock();
-        }
+        mChanges.put(change.getId(), change);
     }
 
     /**
@@ -143,20 +133,14 @@
      */
     long[] getDisabledChanges(ApplicationInfo app) {
         LongArray disabled = new LongArray();
-        mReadWriteLock.readLock().lock();
-        try {
-            for (int i = 0; i < mChanges.size(); ++i) {
-                CompatChange c = mChanges.valueAt(i);
-                if (!c.isEnabled(app, mAndroidBuildClassifier)) {
-                    disabled.add(c.getId());
-                }
+        for (CompatChange c : mChanges.values()) {
+            if (!c.isEnabled(app, mAndroidBuildClassifier)) {
+                disabled.add(c.getId());
             }
-        } finally {
-            mReadWriteLock.readLock().unlock();
         }
-        // Note: we don't need to explicitly sort the array, as the behaviour of LongSparseArray
-        // (mChanges) ensures it's already sorted.
-        return disabled.toArray();
+        final long[] sortedChanges = disabled.toArray();
+        Arrays.sort(sortedChanges);
+        return sortedChanges;
     }
 
     /**
@@ -166,15 +150,10 @@
      * @return the change ID, or {@code -1} if no change with that name exists
      */
     long lookupChangeId(String name) {
-        mReadWriteLock.readLock().lock();
-        try {
-            for (int i = 0; i < mChanges.size(); ++i) {
-                if (TextUtils.equals(mChanges.valueAt(i).getName(), name)) {
-                    return mChanges.keyAt(i);
-                }
+        for (CompatChange c : mChanges.values()) {
+            if (TextUtils.equals(c.getName(), name)) {
+                return c.getId();
             }
-        } finally {
-            mReadWriteLock.readLock().unlock();
         }
         return -1;
     }
@@ -188,17 +167,12 @@
      * change ID is not known, as unknown changes are enabled by default.
      */
     boolean isChangeEnabled(long changeId, ApplicationInfo app) {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            if (c == null) {
-                // we know nothing about this change: default behaviour is enabled.
-                return true;
-            }
-            return c.isEnabled(app, mAndroidBuildClassifier);
-        } finally {
-            mReadWriteLock.readLock().unlock();
+        CompatChange c = mChanges.get(changeId);
+        if (c == null) {
+            // we know nothing about this change: default behaviour is enabled.
+            return true;
         }
+        return c.isEnabled(app, mAndroidBuildClassifier);
     }
 
     /**
@@ -210,17 +184,12 @@
      * {@code true} if the change ID is not known, as unknown changes are enabled by default.
      */
     boolean willChangeBeEnabled(long changeId, String packageName) {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            if (c == null) {
-                // we know nothing about this change: default behaviour is enabled.
-                return true;
-            }
-            return c.willBeEnabled(packageName);
-        } finally {
-            mReadWriteLock.readLock().unlock();
+        CompatChange c = mChanges.get(changeId);
+        if (c == null) {
+            // we know nothing about this change: default behaviour is enabled.
+            return true;
         }
+        return c.willBeEnabled(packageName);
     }
 
     /**
@@ -239,7 +208,7 @@
      * @return {@code true} if the change existed before adding the override
      * @throws IllegalStateException if overriding is not allowed
      */
-    boolean addOverride(long changeId, String packageName, boolean enabled) {
+    synchronized boolean addOverride(long changeId, String packageName, boolean enabled) {
         boolean alreadyKnown = addOverrideUnsafe(changeId, packageName,
                 new PackageOverride.Builder().setEnabled(enabled).build());
         saveOverrides();
@@ -250,12 +219,11 @@
     /**
      * Overrides the enabled state for a given change and app.
      *
-     * <p>Note, package overrides are not persistent and will be lost on system or runtime restart.
      *
      * @param overrides   list of overrides to default changes config.
      * @param packageName app for which the overrides will be applied.
      */
-    void addOverrides(CompatibilityOverrideConfig overrides, String packageName) {
+    synchronized void addOverrides(CompatibilityOverrideConfig overrides, String packageName) {
         for (Long changeId : overrides.overrides.keySet()) {
             addOverrideUnsafe(changeId, packageName, overrides.overrides.get(changeId));
         }
@@ -265,36 +233,24 @@
 
     private boolean addOverrideUnsafe(long changeId, String packageName,
             PackageOverride overrides) {
-        boolean alreadyKnown = true;
+        final AtomicBoolean alreadyKnown = new AtomicBoolean(true);
         OverrideAllowedState allowedState =
                 mOverrideValidator.getOverrideAllowedState(changeId, packageName);
         allowedState.enforce(changeId, packageName);
         Long versionCode = getVersionCodeOrNull(packageName);
-        mReadWriteLock.writeLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            if (c == null) {
-                alreadyKnown = false;
-                c = new CompatChange(changeId);
-                addChange(c);
-            }
-            c.addPackageOverride(packageName, overrides, allowedState, versionCode);
-            invalidateCache();
-        } finally {
-            mReadWriteLock.writeLock().unlock();
-        }
-        return alreadyKnown;
+
+        final CompatChange c = mChanges.computeIfAbsent(changeId, (key) -> {
+            alreadyKnown.set(false);
+            return new CompatChange(changeId);
+        });
+        c.addPackageOverride(packageName, overrides, allowedState, versionCode);
+        invalidateCache();
+        return alreadyKnown.get();
     }
 
     /** Checks whether the change is known to the compat config. */
     boolean isKnownChangeId(long changeId) {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            return c != null;
-        } finally {
-            mReadWriteLock.readLock().unlock();
-        }
+        return mChanges.containsKey(changeId);
     }
 
     /**
@@ -302,55 +258,35 @@
      * target SDK gated).
      */
     int maxTargetSdkForChangeIdOptIn(long changeId) {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            if (c != null && c.getEnableSinceTargetSdk() != -1) {
-                return c.getEnableSinceTargetSdk() - 1;
-            }
-            return -1;
-        } finally {
-            mReadWriteLock.readLock().unlock();
+        CompatChange c = mChanges.get(changeId);
+        if (c != null && c.getEnableSinceTargetSdk() != -1) {
+            return c.getEnableSinceTargetSdk() - 1;
         }
+        return -1;
     }
 
     /**
      * Returns whether the change is marked as logging only.
      */
     boolean isLoggingOnly(long changeId) {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            return c != null && c.getLoggingOnly();
-        } finally {
-            mReadWriteLock.readLock().unlock();
-        }
+        CompatChange c = mChanges.get(changeId);
+        return c != null && c.getLoggingOnly();
     }
 
     /**
      * Returns whether the change is marked as disabled.
      */
     boolean isDisabled(long changeId) {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            return c != null && c.getDisabled();
-        } finally {
-            mReadWriteLock.readLock().unlock();
-        }
+        CompatChange c = mChanges.get(changeId);
+        return c != null && c.getDisabled();
     }
 
     /**
      * Returns whether the change is overridable.
      */
     boolean isOverridable(long changeId) {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            return c != null && c.getOverridable();
-        } finally {
-            mReadWriteLock.readLock().unlock();
-        }
+        CompatChange c = mChanges.get(changeId);
+        return c != null && c.getOverridable();
     }
 
     /**
@@ -363,10 +299,12 @@
      * @param packageName the app package name that was overridden
      * @return {@code true} if an override existed;
      */
-    boolean removeOverride(long changeId, String packageName) {
+    synchronized boolean removeOverride(long changeId, String packageName) {
         boolean overrideExists = removeOverrideUnsafe(changeId, packageName);
-        saveOverrides();
-        invalidateCache();
+        if (overrideExists) {
+            saveOverrides();
+            invalidateCache();
+        }
         return overrideExists;
     }
 
@@ -376,14 +314,9 @@
      */
     private boolean removeOverrideUnsafe(long changeId, String packageName) {
         Long versionCode = getVersionCodeOrNull(packageName);
-        mReadWriteLock.writeLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            if (c != null) {
-                return removeOverrideUnsafe(c, packageName, versionCode);
-            }
-        } finally {
-            mReadWriteLock.writeLock().unlock();
+        CompatChange c = mChanges.get(changeId);
+        if (c != null) {
+            return removeOverrideUnsafe(c, packageName, versionCode);
         }
         return false;
     }
@@ -397,13 +330,7 @@
         long changeId = change.getId();
         OverrideAllowedState allowedState =
                 mOverrideValidator.getOverrideAllowedState(changeId, packageName);
-        if (change.hasPackageOverride(packageName)) {
-            allowedState.enforce(changeId, packageName);
-            change.removePackageOverride(packageName, allowedState, versionCode);
-            invalidateCache();
-            return true;
-        }
-        return false;
+        return change.removePackageOverride(packageName, allowedState, versionCode);
     }
 
     /**
@@ -414,19 +341,16 @@
      *
      * @param packageName the package for which the overrides should be purged
      */
-    void removePackageOverrides(String packageName) {
+    synchronized void removePackageOverrides(String packageName) {
         Long versionCode = getVersionCodeOrNull(packageName);
-        mReadWriteLock.writeLock().lock();
-        try {
-            for (int i = 0; i < mChanges.size(); ++i) {
-                CompatChange change = mChanges.valueAt(i);
-                removeOverrideUnsafe(change, packageName, versionCode);
-            }
-        } finally {
-            mReadWriteLock.writeLock().unlock();
+        boolean shouldInvalidateCache = false;
+        for (CompatChange change : mChanges.values()) {
+            shouldInvalidateCache |= removeOverrideUnsafe(change, packageName, versionCode);
         }
-        saveOverrides();
-        invalidateCache();
+        if (shouldInvalidateCache) {
+            saveOverrides();
+            invalidateCache();
+        }
     }
 
     /**
@@ -439,34 +363,31 @@
      * @param overridesToRemove list of change IDs for which to restore the default behaviour.
      * @param packageName       the package for which the overrides should be purged
      */
-    void removePackageOverrides(CompatibilityOverridesToRemoveConfig overridesToRemove,
+    synchronized void removePackageOverrides(CompatibilityOverridesToRemoveConfig overridesToRemove,
             String packageName) {
+        boolean shouldInvalidateCache = false;
         for (Long changeId : overridesToRemove.changeIds) {
-            removeOverrideUnsafe(changeId, packageName);
+            shouldInvalidateCache |= removeOverrideUnsafe(changeId, packageName);
         }
-        saveOverrides();
-        invalidateCache();
+        if (shouldInvalidateCache) {
+            saveOverrides();
+            invalidateCache();
+        }
     }
 
     private long[] getAllowedChangesSinceTargetSdkForPackage(String packageName,
             int targetSdkVersion) {
         LongArray allowed = new LongArray();
-        mReadWriteLock.readLock().lock();
-        try {
-            for (int i = 0; i < mChanges.size(); ++i) {
-                CompatChange change = mChanges.valueAt(i);
-                if (change.getEnableSinceTargetSdk() != targetSdkVersion) {
-                    continue;
-                }
-                OverrideAllowedState allowedState =
-                        mOverrideValidator.getOverrideAllowedState(change.getId(),
-                                packageName);
-                if (allowedState.state == OverrideAllowedState.ALLOWED) {
-                    allowed.add(change.getId());
-                }
+        for (CompatChange change : mChanges.values()) {
+            if (change.getEnableSinceTargetSdk() != targetSdkVersion) {
+                continue;
             }
-        } finally {
-            mReadWriteLock.readLock().unlock();
+            OverrideAllowedState allowedState =
+                    mOverrideValidator.getOverrideAllowedState(change.getId(),
+                            packageName);
+            if (allowedState.state == OverrideAllowedState.ALLOWED) {
+                allowed.add(change.getId());
+            }
         }
         return allowed.toArray();
     }
@@ -479,12 +400,15 @@
      */
     int enableTargetSdkChangesForPackage(String packageName, int targetSdkVersion) {
         long[] changes = getAllowedChangesSinceTargetSdkForPackage(packageName, targetSdkVersion);
+        boolean shouldInvalidateCache = false;
         for (long changeId : changes) {
-            addOverrideUnsafe(changeId, packageName,
+            shouldInvalidateCache |= addOverrideUnsafe(changeId, packageName,
                     new PackageOverride.Builder().setEnabled(true).build());
         }
-        saveOverrides();
-        invalidateCache();
+        if (shouldInvalidateCache) {
+            saveOverrides();
+            invalidateCache();
+        }
         return changes.length;
     }
 
@@ -496,30 +420,27 @@
      */
     int disableTargetSdkChangesForPackage(String packageName, int targetSdkVersion) {
         long[] changes = getAllowedChangesSinceTargetSdkForPackage(packageName, targetSdkVersion);
+        boolean shouldInvalidateCache = false;
         for (long changeId : changes) {
-            addOverrideUnsafe(changeId, packageName,
+            shouldInvalidateCache |= addOverrideUnsafe(changeId, packageName,
                     new PackageOverride.Builder().setEnabled(false).build());
         }
-        saveOverrides();
-        invalidateCache();
+        if (shouldInvalidateCache) {
+            saveOverrides();
+            invalidateCache();
+        }
         return changes.length;
     }
 
     boolean registerListener(long changeId, CompatChange.ChangeListener listener) {
-        boolean alreadyKnown = true;
-        mReadWriteLock.writeLock().lock();
-        try {
-            CompatChange c = mChanges.get(changeId);
-            if (c == null) {
-                alreadyKnown = false;
-                c = new CompatChange(changeId);
-                addChange(c);
-            }
-            c.registerListener(listener);
-        } finally {
-            mReadWriteLock.writeLock().unlock();
-        }
-        return alreadyKnown;
+        final AtomicBoolean alreadyKnown = new AtomicBoolean(true);
+        final CompatChange c = mChanges.computeIfAbsent(changeId, (key) -> {
+            alreadyKnown.set(false);
+            invalidateCache();
+            return new CompatChange(changeId);
+        });
+        c.registerListener(listener);
+        return alreadyKnown.get();
     }
 
     boolean defaultChangeIdValue(long changeId) {
@@ -537,12 +458,7 @@
 
     @VisibleForTesting
     void clearChanges() {
-        mReadWriteLock.writeLock().lock();
-        try {
-            mChanges.clear();
-        } finally {
-            mReadWriteLock.writeLock().unlock();
-        }
+        mChanges.clear();
     }
 
     /**
@@ -551,18 +467,12 @@
      * @param pw {@link PrintWriter} instance to which the information will be dumped
      */
     void dumpConfig(PrintWriter pw) {
-        mReadWriteLock.readLock().lock();
-        try {
-            if (mChanges.size() == 0) {
-                pw.println("No compat overrides.");
-                return;
-            }
-            for (int i = 0; i < mChanges.size(); ++i) {
-                CompatChange c = mChanges.valueAt(i);
-                pw.println(c.toString());
-            }
-        } finally {
-            mReadWriteLock.readLock().unlock();
+        if (mChanges.size() == 0) {
+            pw.println("No compat overrides.");
+            return;
+        }
+        for (CompatChange c : mChanges.values()) {
+            pw.println(c.toString());
         }
     }
 
@@ -574,18 +484,12 @@
     CompatibilityChangeConfig getAppConfig(ApplicationInfo applicationInfo) {
         Set<Long> enabled = new HashSet<>();
         Set<Long> disabled = new HashSet<>();
-        mReadWriteLock.readLock().lock();
-        try {
-            for (int i = 0; i < mChanges.size(); ++i) {
-                CompatChange c = mChanges.valueAt(i);
-                if (c.isEnabled(applicationInfo, mAndroidBuildClassifier)) {
-                    enabled.add(c.getId());
-                } else {
-                    disabled.add(c.getId());
-                }
+        for (CompatChange c : mChanges.values()) {
+            if (c.isEnabled(applicationInfo, mAndroidBuildClassifier)) {
+                enabled.add(c.getId());
+            } else {
+                disabled.add(c.getId());
             }
-        } finally {
-            mReadWriteLock.readLock().unlock();
         }
         return new CompatibilityChangeConfig(new ChangeConfig(enabled, disabled));
     }
@@ -596,17 +500,12 @@
      * @return an array of {@link CompatibilityChangeInfo} with the current changes
      */
     CompatibilityChangeInfo[] dumpChanges() {
-        mReadWriteLock.readLock().lock();
-        try {
-            CompatibilityChangeInfo[] changeInfos = new CompatibilityChangeInfo[mChanges.size()];
-            for (int i = 0; i < mChanges.size(); ++i) {
-                CompatChange change = mChanges.valueAt(i);
-                changeInfos[i] = new CompatibilityChangeInfo(change);
-            }
-            return changeInfos;
-        } finally {
-            mReadWriteLock.readLock().unlock();
+        CompatibilityChangeInfo[] changeInfos = new CompatibilityChangeInfo[mChanges.size()];
+        int i = 0;
+        for (CompatChange change : mChanges.values()) {
+            changeInfos[i++] = new CompatibilityChangeInfo(change);
         }
+        return changeInfos;
     }
 
     void initConfigFromLib(File libraryDir) {
@@ -626,10 +525,12 @@
             Config config = com.android.server.compat.config.XmlParser.read(in);
             for (Change change : config.getCompatChange()) {
                 Slog.d(TAG, "Adding: " + change.toString());
-                addChange(new CompatChange(change));
+                mChanges.put(change.getId(), new CompatChange(change));
             }
         } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) {
             Slog.e(TAG, "Encountered an error while reading/parsing compat config file", e);
+        } finally {
+            invalidateCache();
         }
     }
 
@@ -641,15 +542,12 @@
     @VisibleForTesting
     void initOverrides(File dynamicOverridesFile, File staticOverridesFile) {
         // Clear overrides from all changes before loading.
-        mReadWriteLock.writeLock().lock();
-        try {
-            for (int i = 0; i < mChanges.size(); ++i) {
-                mChanges.valueAt(i).clearOverrides();
-            }
-        } finally {
-            mReadWriteLock.writeLock().unlock();
+
+        for (CompatChange c : mChanges.values()) {
+            c.clearOverrides();
         }
 
+
         loadOverrides(staticOverridesFile);
 
         mOverridesFile = dynamicOverridesFile;
@@ -698,18 +596,12 @@
         }
         synchronized (mOverridesFile) {
             Overrides overrides = new Overrides();
-            mReadWriteLock.readLock().lock();
-            try {
-                List<ChangeOverrides> changeOverridesList = overrides.getChangeOverrides();
-                for (int idx = 0; idx < mChanges.size(); ++idx) {
-                    CompatChange c = mChanges.valueAt(idx);
-                    ChangeOverrides changeOverrides = c.saveOverrides();
-                    if (changeOverrides != null) {
-                        changeOverridesList.add(changeOverrides);
-                    }
+            List<ChangeOverrides> changeOverridesList = overrides.getChangeOverrides();
+            for (CompatChange c : mChanges.values()) {
+                ChangeOverrides changeOverrides = c.saveOverrides();
+                if (changeOverrides != null) {
+                    changeOverridesList.add(changeOverrides);
                 }
-            } finally {
-                mReadWriteLock.readLock().unlock();
             }
             // Create the file if it doesn't already exist
             try {
@@ -741,20 +633,11 @@
     void recheckOverrides(String packageName) {
         Long versionCode = getVersionCodeOrNull(packageName);
         boolean shouldInvalidateCache = false;
-        mReadWriteLock.readLock().lock();
-        try {
-            for (int idx = 0; idx < mChanges.size(); ++idx) {
-                CompatChange c = mChanges.valueAt(idx);
-                if (!c.hasPackageOverride(packageName)) {
-                    continue;
-                }
-                OverrideAllowedState allowedState =
-                        mOverrideValidator.getOverrideAllowedStateForRecheck(c.getId(),
-                                packageName);
-                shouldInvalidateCache |= c.recheckOverride(packageName, allowedState, versionCode);
-            }
-        } finally {
-            mReadWriteLock.readLock().unlock();
+        for (CompatChange c : mChanges.values()) {
+            OverrideAllowedState allowedState =
+                    mOverrideValidator.getOverrideAllowedStateForRecheck(c.getId(),
+                            packageName);
+            shouldInvalidateCache |= c.recheckOverride(packageName, allowedState, versionCode);
         }
         if (shouldInvalidateCache) {
             invalidateCache();
diff --git a/services/core/java/com/android/server/compat/overrides/AppCompatOverridesParser.java b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesParser.java
new file mode 100644
index 0000000..11dc1db
--- /dev/null
+++ b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesParser.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.compat.overrides;
+
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+
+import android.app.compat.PackageOverride;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.KeyValueListParser;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * A utility class for parsing App Compat Overrides flags.
+ *
+ * @hide
+ */
+final class AppCompatOverridesParser {
+    /**
+     * Flag for specifying all compat change IDs owned by a namespace. See {@link
+     * #parseOwnedChangeIds} for information on how this flag is parsed.
+     */
+    static final String FLAG_OWNED_CHANGE_IDS = "owned_change_ids";
+
+    /**
+     * Flag for immediately removing overrides for certain packages and change IDs (from the compat
+     * platform), as well as stopping to apply them, in case of an emergency. See {@link
+     * #parseRemoveOverrides} for information on how this flag is parsed.
+     */
+    static final String FLAG_REMOVE_OVERRIDES = "remove_overrides";
+
+    private static final String TAG = "AppCompatOverridesParser";
+
+    private static final String WILDCARD_SYMBOL = "*";
+
+    private static final Pattern BOOLEAN_PATTERN =
+            Pattern.compile("true|false", Pattern.CASE_INSENSITIVE);
+
+    private static final String WILDCARD_NO_OWNED_CHANGE_IDS_WARNING =
+            "Wildcard can't be used in '" + FLAG_REMOVE_OVERRIDES + "' flag with an empty "
+                    + FLAG_OWNED_CHANGE_IDS + "' flag";
+
+    private final PackageManager mPackageManager;
+
+    AppCompatOverridesParser(PackageManager packageManager) {
+        mPackageManager = packageManager;
+    }
+
+    /**
+     * Parses the given {@code configStr} and returns a map from package name to a set of change
+     * IDs to remove for that package.
+     *
+     * <p>The given {@code configStr} is expected to either be:
+     *
+     * <ul>
+     *   <li>'*' (wildcard), to indicate that all owned overrides, specified in {@code
+     *   ownedChangeIds}, for all installed packages should be removed.
+     *   <li>A comma separated key value list, where the key is a package name and the value is
+     *       either:
+     *       <ul>
+     *         <li>'*' (wildcard), to indicate that all owned overrides, specified in {@code
+     *         ownedChangeIds} for that package should be removed.
+     *         <li>A colon separated list of change IDs to remove for that package.
+     *       </ul>
+     * </ul>
+     *
+     * <p>If the given {@code configStr} doesn't match the expected format, an empty map will be
+     * returned. If a specific change ID isn't a valid long, it will be ignored.
+     */
+    Map<String, Set<Long>> parseRemoveOverrides(String configStr, Set<Long> ownedChangeIds) {
+        if (configStr.isEmpty()) {
+            return emptyMap();
+        }
+
+        Map<String, Set<Long>> result = new ArrayMap<>();
+        if (configStr.equals(WILDCARD_SYMBOL)) {
+            if (ownedChangeIds.isEmpty()) {
+                Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING);
+                return emptyMap();
+            }
+            List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications(
+                    MATCH_ANY_USER);
+            for (ApplicationInfo appInfo : installedApps) {
+                result.put(appInfo.packageName, ownedChangeIds);
+            }
+            return result;
+        }
+
+        KeyValueListParser parser = new KeyValueListParser(',');
+        try {
+            parser.setString(configStr);
+        } catch (IllegalArgumentException e) {
+            Slog.w(
+                    TAG,
+                    "Invalid format in '" + FLAG_REMOVE_OVERRIDES + "' flag: " + configStr, e);
+            return emptyMap();
+        }
+        for (int i = 0; i < parser.size(); i++) {
+            String packageName = parser.keyAt(i);
+            String changeIdsStr = parser.getString(packageName, /* def= */ "");
+            if (changeIdsStr.equals(WILDCARD_SYMBOL)) {
+                if (ownedChangeIds.isEmpty()) {
+                    Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING);
+                    continue;
+                }
+                result.put(packageName, ownedChangeIds);
+            } else {
+                for (String changeIdStr : changeIdsStr.split(":")) {
+                    try {
+                        long changeId = Long.parseLong(changeIdStr);
+                        result.computeIfAbsent(packageName, k -> new ArraySet<>()).add(changeId);
+                    } catch (NumberFormatException e) {
+                        Slog.w(
+                                TAG,
+                                "Invalid change ID in '" + FLAG_REMOVE_OVERRIDES + "' flag: "
+                                        + changeIdStr, e);
+                    }
+                }
+            }
+        }
+
+        return result;
+    }
+
+
+    /**
+     * Parses the given {@code configStr}, that is expected to be a comma separated list of change
+     * IDs, into a set.
+     *
+     * <p>If any of the change IDs isn't a valid long, it will be ignored.
+     */
+    static Set<Long> parseOwnedChangeIds(String configStr) {
+        if (configStr.isEmpty()) {
+            return emptySet();
+        }
+
+        Set<Long> result = new ArraySet<>();
+        for (String changeIdStr : configStr.split(",")) {
+            try {
+                result.add(Long.parseLong(changeIdStr));
+            } catch (NumberFormatException e) {
+                Slog.w(TAG,
+                        "Invalid change ID in '" + FLAG_OWNED_CHANGE_IDS + "' flag: " + changeIdStr,
+                        e);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Parses the given {@code configStr}, that is expected to be a comma separated list of changes
+     * overrides, and returns a map from change ID to {@link PackageOverride} instances to add.
+     *
+     * <p>Each change override is in the following format:
+     * '<change-id>:<min-version-code?>:<max-version-code?>:<enabled>'.
+     *
+     * <p>If there are multiple overrides that should be added with the same change ID, the one
+     * that best fits the given {@code versionCode} is added.
+     *
+     * <p>Any overrides whose change ID is in {@code changeIdsToSkip} are ignored.
+     *
+     * <p>If a change override entry in {@code configStr} is invalid, it will be ignored.
+     */
+    static Map<Long, PackageOverride> parsePackageOverrides(String configStr, long versionCode,
+            Set<Long> changeIdsToSkip) {
+        if (configStr.isEmpty()) {
+            return emptyMap();
+        }
+        PackageOverrideComparator comparator = new PackageOverrideComparator(versionCode);
+        Map<Long, PackageOverride> overridesToAdd = new ArrayMap<>();
+        for (String overrideEntryString : configStr.split(",")) {
+            List<String> changeIdAndVersions = Arrays.asList(overrideEntryString.split(":", 4));
+            if (changeIdAndVersions.size() != 4) {
+                Slog.w(TAG, "Invalid change override entry: " + overrideEntryString);
+                continue;
+            }
+            long changeId;
+            try {
+                changeId = Long.parseLong(changeIdAndVersions.get(0));
+            } catch (NumberFormatException e) {
+                Slog.w(TAG, "Invalid change ID in override entry: " + overrideEntryString, e);
+                continue;
+            }
+
+            if (changeIdsToSkip.contains(changeId)) {
+                continue;
+            }
+
+            String minVersionCodeStr = changeIdAndVersions.get(1);
+            String maxVersionCodeStr = changeIdAndVersions.get(2);
+
+            String enabledStr = changeIdAndVersions.get(3);
+            if (!BOOLEAN_PATTERN.matcher(enabledStr).matches()) {
+                Slog.w(TAG, "Invalid enabled string in override entry: " + overrideEntryString);
+                continue;
+            }
+            boolean enabled = Boolean.parseBoolean(enabledStr);
+            PackageOverride.Builder overrideBuilder = new PackageOverride.Builder().setEnabled(
+                    enabled);
+            try {
+                if (!minVersionCodeStr.isEmpty()) {
+                    overrideBuilder.setMinVersionCode(Long.parseLong(minVersionCodeStr));
+                }
+                if (!maxVersionCodeStr.isEmpty()) {
+                    overrideBuilder.setMaxVersionCode(Long.parseLong(maxVersionCodeStr));
+                }
+            } catch (NumberFormatException e) {
+                Slog.w(TAG,
+                        "Invalid min/max version code in override entry: " + overrideEntryString,
+                        e);
+                continue;
+            }
+
+            try {
+                PackageOverride override = overrideBuilder.build();
+                if (!overridesToAdd.containsKey(changeId)
+                        || comparator.compare(override, overridesToAdd.get(changeId)) < 0) {
+                    overridesToAdd.put(changeId, override);
+                }
+            } catch (IllegalArgumentException e) {
+                Slog.w(TAG, "Failed to build PackageOverride", e);
+            }
+        }
+
+        return overridesToAdd;
+    }
+
+    /**
+     * A {@link Comparator} that compares @link PackageOverride} instances with respect to a
+     * specified {@code versionCode} as follows:
+     *
+     * <ul>
+     *   <li>Prefer the {@link PackageOverride} whose version range contains {@code versionCode}.
+     *   <li>Otherwise, prefer the {@link PackageOverride} whose version range is closest to {@code
+     *       versionCode} from below.
+     *   <li>Otherwise, prefer the {@link PackageOverride} whose version range is closest to {@code
+     *       versionCode} from above.
+     * </ul>
+     */
+    private static final class PackageOverrideComparator implements Comparator<PackageOverride> {
+        private final long mVersionCode;
+
+        PackageOverrideComparator(long versionCode) {
+            this.mVersionCode = versionCode;
+        }
+
+        @Override
+        public int compare(PackageOverride o1, PackageOverride o2) {
+            // Prefer overrides whose version range contains versionCode.
+            boolean isVersionInRange1 = isVersionInRange(o1, mVersionCode);
+            boolean isVersionInRange2 = isVersionInRange(o2, mVersionCode);
+            if (isVersionInRange1 != isVersionInRange2) {
+                return isVersionInRange1 ? -1 : 1;
+            }
+
+            // Otherwise, prefer overrides whose version range is before versionCode.
+            boolean isVersionAfterRange1 = isVersionAfterRange(o1, mVersionCode);
+            boolean isVersionAfterRange2 = isVersionAfterRange(o2, mVersionCode);
+            if (isVersionAfterRange1 != isVersionAfterRange2) {
+                return isVersionAfterRange1 ? -1 : 1;
+            }
+
+            // If both overrides' version ranges are either before or after versionCode, prefer
+            // those whose version range is closer to versionCode.
+            return Long.compare(
+                    getVersionProximity(o1, mVersionCode), getVersionProximity(o2, mVersionCode));
+        }
+
+        /**
+         * Returns true if the version range in the given {@code override} contains {@code
+         * versionCode}.
+         */
+        private static boolean isVersionInRange(PackageOverride override, long versionCode) {
+            return override.getMinVersionCode() <= versionCode
+                    && versionCode <= override.getMaxVersionCode();
+        }
+
+        /**
+         * Returns true if the given {@code versionCode} is strictly after the version range in the
+         * given {@code override}.
+         */
+        private static boolean isVersionAfterRange(PackageOverride override, long versionCode) {
+            return override.getMaxVersionCode() < versionCode;
+        }
+
+        /**
+         * Returns true if the given {@code versionCode} is strictly before the version range in the
+         * given {@code override}.
+         */
+        private static boolean isVersionBeforeRange(PackageOverride override, long versionCode) {
+            return override.getMinVersionCode() > versionCode;
+        }
+
+        /**
+         * In case the given {@code versionCode} is strictly before or after the version range in
+         * the given {@code override}, returns the distance from it, otherwise returns zero.
+         */
+        private static long getVersionProximity(PackageOverride override, long versionCode) {
+            if (isVersionAfterRange(override, versionCode)) {
+                return versionCode - override.getMaxVersionCode();
+            }
+            if (isVersionBeforeRange(override, versionCode)) {
+                return override.getMinVersionCode() - versionCode;
+            }
+
+            // Version is in range. Note that when two overrides have a zero version proximity
+            // they will be ordered arbitrarily.
+            return 0;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/compat/overrides/AppCompatOverridesService.java b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesService.java
new file mode 100644
index 0000000..6aed4b0
--- /dev/null
+++ b/services/core/java/com/android/server/compat/overrides/AppCompatOverridesService.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.compat.overrides;
+
+import static android.content.Intent.ACTION_PACKAGE_ADDED;
+import static android.content.Intent.ACTION_PACKAGE_CHANGED;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.provider.DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES;
+
+import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_OWNED_CHANGE_IDS;
+import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_REMOVE_OVERRIDES;
+
+import static java.util.Collections.emptySet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.compat.PackageOverride;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.compat.CompatibilityOverrideConfig;
+import com.android.internal.compat.CompatibilityOverridesToRemoveConfig;
+import com.android.internal.compat.IPlatformCompat;
+import com.android.server.SystemService;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Service for applying per-app compat overrides delivered via Device Config.
+ *
+ * <p>The service listens both on changes to supported Device Config namespaces and on package
+ * added/changed/removed events, and applies overrides accordingly.
+ *
+ * @hide
+ */
+public final class AppCompatOverridesService {
+    private static final String TAG = "AppCompatOverridesService";
+
+    private static final List<String> SUPPORTED_NAMESPACES = Arrays.asList(
+            NAMESPACE_APP_COMPAT_OVERRIDES);
+
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final IPlatformCompat mPlatformCompat;
+    private final List<String> mSupportedNamespaces;
+    private final AppCompatOverridesParser mOverridesParser;
+    private final PackageReceiver mPackageReceiver;
+    private final List<DeviceConfigListener> mDeviceConfigListeners;
+
+    private AppCompatOverridesService(Context context) {
+        this(context, IPlatformCompat.Stub.asInterface(
+                ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)), SUPPORTED_NAMESPACES);
+    }
+
+    @VisibleForTesting
+    AppCompatOverridesService(Context context, IPlatformCompat platformCompat,
+            List<String> supportedNamespaces) {
+        mContext = context;
+        mPackageManager = mContext.getPackageManager();
+        mPlatformCompat = platformCompat;
+        mSupportedNamespaces = supportedNamespaces;
+        mOverridesParser = new AppCompatOverridesParser(mPackageManager);
+        mPackageReceiver = new PackageReceiver(mContext);
+        mDeviceConfigListeners = new ArrayList<>();
+        for (String namespace : mSupportedNamespaces) {
+            mDeviceConfigListeners.add(new DeviceConfigListener(mContext, namespace));
+        }
+    }
+
+    @Override
+    public void finalize() {
+        unregisterDeviceConfigListeners();
+        unregisterPackageReceiver();
+    }
+
+    @VisibleForTesting
+    void registerDeviceConfigListeners() {
+        for (DeviceConfigListener listener : mDeviceConfigListeners) {
+            listener.register();
+        }
+    }
+
+    private void unregisterDeviceConfigListeners() {
+        for (DeviceConfigListener listener : mDeviceConfigListeners) {
+            listener.unregister();
+        }
+    }
+
+    @VisibleForTesting
+    void registerPackageReceiver() {
+        mPackageReceiver.register();
+    }
+
+    private void unregisterPackageReceiver() {
+        mPackageReceiver.unregister();
+    }
+
+    /**
+     * Same as {@link #applyOverrides(Properties, Set, Map)} except all properties of the given
+     * {@code namespace} are fetched via {@link DeviceConfig#getProperties}.
+     */
+    private void applyAllOverrides(String namespace, Set<Long> ownedChangeIds,
+            Map<String, Set<Long>> packageToChangeIdsToSkip) {
+        applyOverrides(DeviceConfig.getProperties(namespace), ownedChangeIds,
+                packageToChangeIdsToSkip);
+    }
+
+    /**
+     * Iterates all package override flags in the given {@code properties}, and for each flag whose
+     * package is installed on the device, parses its value and adds the overrides in it with
+     * respect to the package's current installed version.
+     *
+     * <p>In addition, for each package, removes any override that wasn't just added, whose change
+     * ID is in {@code ownedChangeIds} but not in the respective set in {@code
+     * packageToChangeIdsToSkip}.
+     */
+    private void applyOverrides(Properties properties, Set<Long> ownedChangeIds,
+            Map<String, Set<Long>> packageToChangeIdsToSkip) {
+        Set<String> packageNames = new ArraySet<>(properties.getKeyset());
+        packageNames.remove(FLAG_OWNED_CHANGE_IDS);
+        packageNames.remove(FLAG_REMOVE_OVERRIDES);
+        for (String packageName : packageNames) {
+            Long versionCode = getVersionCodeOrNull(packageName);
+            if (versionCode == null) {
+                // Package isn't installed yet.
+                continue;
+            }
+
+            applyPackageOverrides(properties.getString(packageName, /* defaultValue= */ ""),
+                    packageName, versionCode, ownedChangeIds,
+                    packageToChangeIdsToSkip.getOrDefault(packageName, emptySet()),
+                    /* removeOtherOwnedOverrides= */ true);
+        }
+    }
+
+    /**
+     * Adds all overrides in all supported namespaces for the given {@code packageName}.
+     */
+    private void addAllPackageOverrides(String packageName) {
+        Long versionCode = getVersionCodeOrNull(packageName);
+        if (versionCode == null) {
+            return;
+        }
+
+        for (String namespace : mSupportedNamespaces) {
+            // We apply overrides for each namespace separately so that if there is a failure for
+            // one namespace, the other namespaces won't be affected.
+            Set<Long> ownedChangeIds = getOwnedChangeIds(namespace);
+            applyPackageOverrides(
+                    DeviceConfig.getString(namespace, packageName, /* defaultValue= */ ""),
+                    packageName, versionCode, ownedChangeIds,
+                    getOverridesToRemove(namespace, ownedChangeIds).getOrDefault(packageName,
+                            emptySet()), /* removeOtherOwnedOverrides */ false);
+        }
+    }
+
+    /**
+     * Calls {@link AppCompatOverridesParser#parsePackageOverrides} on the given arguments and adds
+     * the resulting overrides via {@link IPlatformCompat#putOverridesOnReleaseBuilds}.
+     *
+     * <p>In addition, if {@code removeOtherOwnedOverrides} is true, removes any override that
+     * wasn't just added, whose change ID is in {@code ownedChangeIds} but not in {@code
+     * changeIdsToSkip}, via {@link IPlatformCompat#removeOverridesOnReleaseBuilds}.
+     */
+    private void applyPackageOverrides(String configStr, String packageName, long versionCode,
+            Set<Long> ownedChangeIds, Set<Long> changeIdsToSkip,
+            boolean removeOtherOwnedOverrides) {
+        Map<Long, PackageOverride> overridesToAdd = AppCompatOverridesParser.parsePackageOverrides(
+                configStr, versionCode, changeIdsToSkip);
+        putPackageOverrides(packageName, overridesToAdd);
+
+        if (!removeOtherOwnedOverrides) {
+            return;
+        }
+        Set<Long> overridesToRemove = new ArraySet<>();
+        for (Long changeId : ownedChangeIds) {
+            if (!overridesToAdd.containsKey(changeId) && !changeIdsToSkip.contains(changeId)) {
+                overridesToRemove.add(changeId);
+            }
+        }
+        removePackageOverrides(packageName, overridesToRemove);
+    }
+
+    /**
+     * Removes all owned overrides in all supported namespaces for the given {@code packageName}.
+     *
+     * <p>If a certain namespace doesn't have a package override flag for the given {@code
+     * packageName}, that namespace is skipped.</p>
+     */
+    private void removeAllPackageOverrides(String packageName) {
+        for (String namespace : mSupportedNamespaces) {
+            if (DeviceConfig.getString(namespace, packageName, /* defaultValue= */ "").isEmpty()) {
+                // No overrides for this package in this namespace.
+                continue;
+            }
+            // We remove overrides for each namespace separately so that if there is a failure for
+            // one namespace, the other namespaces won't be affected.
+            removePackageOverrides(packageName, getOwnedChangeIds(namespace));
+        }
+    }
+
+    /**
+     * Calls {@link IPlatformCompat#removeOverridesOnReleaseBuilds} on each package name and
+     * respective change IDs in {@code overridesToRemove}.
+     */
+    private void removeOverrides(Map<String, Set<Long>> overridesToRemove) {
+        for (Map.Entry<String, Set<Long>> packageNameAndOverrides : overridesToRemove.entrySet()) {
+            removePackageOverrides(packageNameAndOverrides.getKey(),
+                    packageNameAndOverrides.getValue());
+        }
+    }
+
+    /**
+     * Fetches the value of {@link AppCompatOverridesParser#FLAG_REMOVE_OVERRIDES} for the given
+     * {@code namespace} and parses it into a map from package name to a set of change IDs to
+     * remove for that package.
+     */
+    private Map<String, Set<Long>> getOverridesToRemove(String namespace,
+            Set<Long> ownedChangeIds) {
+        return mOverridesParser.parseRemoveOverrides(
+                DeviceConfig.getString(namespace, FLAG_REMOVE_OVERRIDES, /* defaultValue= */ ""),
+                ownedChangeIds);
+    }
+
+    /**
+     * Fetches the value of {@link AppCompatOverridesParser#FLAG_OWNED_CHANGE_IDS} for the given
+     * {@code namespace} and parses it into a set of change IDs.
+     */
+    private static Set<Long> getOwnedChangeIds(String namespace) {
+        return AppCompatOverridesParser.parseOwnedChangeIds(
+                DeviceConfig.getString(namespace, FLAG_OWNED_CHANGE_IDS, /* defaultValue= */ ""));
+    }
+
+    private void putPackageOverrides(String packageName,
+            Map<Long, PackageOverride> overridesToAdd) {
+        if (overridesToAdd.isEmpty()) {
+            return;
+        }
+        CompatibilityOverrideConfig config = new CompatibilityOverrideConfig(overridesToAdd);
+        try {
+            mPlatformCompat.putOverridesOnReleaseBuilds(config, packageName);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to call IPlatformCompat#putOverridesOnReleaseBuilds", e);
+        }
+    }
+
+    private void removePackageOverrides(String packageName, Set<Long> overridesToRemove) {
+        if (overridesToRemove.isEmpty()) {
+            return;
+        }
+        CompatibilityOverridesToRemoveConfig config = new CompatibilityOverridesToRemoveConfig(
+                overridesToRemove);
+        try {
+            mPlatformCompat.removeOverridesOnReleaseBuilds(config, packageName);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to call IPlatformCompat#removeOverridesOnReleaseBuilds", e);
+        }
+    }
+
+    private boolean isInstalledForAnyUser(String packageName) {
+        return getVersionCodeOrNull(packageName) != null;
+    }
+
+    @Nullable
+    private Long getVersionCodeOrNull(String packageName) {
+        try {
+            ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(packageName,
+                    MATCH_ANY_USER);
+            return applicationInfo.longVersionCode;
+        } catch (PackageManager.NameNotFoundException e) {
+            // Package isn't installed for any user.
+            return null;
+        }
+    }
+
+    /**
+     * SystemService lifecycle for AppCompatOverridesService.
+     *
+     * @hide
+     */
+    public static final class Lifecycle extends SystemService {
+        private AppCompatOverridesService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            mService = new AppCompatOverridesService(getContext());
+            mService.registerDeviceConfigListeners();
+            mService.registerPackageReceiver();
+        }
+    }
+
+    /**
+     * A {@link DeviceConfig.OnPropertiesChangedListener} that listens on changes to a given
+     * namespace and adds/removes overrides according to the changed flags.
+     */
+    private final class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
+        private final Context mContext;
+        private final String mNamespace;
+
+        private DeviceConfigListener(Context context, String namespace) {
+            mContext = context;
+            mNamespace = namespace;
+        }
+
+        private void register() {
+            DeviceConfig.addOnPropertiesChangedListener(mNamespace, mContext.getMainExecutor(),
+                    this);
+        }
+
+        private void unregister() {
+            DeviceConfig.removeOnPropertiesChangedListener(this);
+        }
+
+        @Override
+        public void onPropertiesChanged(Properties properties) {
+            boolean removeOverridesFlagChanged = properties.getKeyset().contains(
+                    FLAG_REMOVE_OVERRIDES);
+            boolean ownedChangedIdsFlagChanged = properties.getKeyset().contains(
+                    FLAG_OWNED_CHANGE_IDS);
+
+            Set<Long> ownedChangeIds = getOwnedChangeIds(mNamespace);
+            Map<String, Set<Long>> overridesToRemove = getOverridesToRemove(mNamespace,
+                    ownedChangeIds);
+            if (removeOverridesFlagChanged || ownedChangedIdsFlagChanged) {
+                // In both cases it's possible that overrides that weren't removed before should
+                // now be removed.
+                removeOverrides(overridesToRemove);
+            }
+
+            if (removeOverridesFlagChanged) {
+                // We need to re-apply all overrides in the namespace since the remove overrides
+                // flag might have blocked some of them from being applied before.
+                applyAllOverrides(mNamespace, ownedChangeIds, overridesToRemove);
+            } else {
+                applyOverrides(properties, ownedChangeIds, overridesToRemove);
+            }
+        }
+    }
+
+    /**
+     * A {@link BroadcastReceiver} that listens on package added/changed/removed events and
+     * adds/removes overrides according to the corresponding Device Config flags.
+     */
+    private final class PackageReceiver extends BroadcastReceiver {
+        private final Context mContext;
+        private final IntentFilter mIntentFilter;
+
+        private PackageReceiver(Context context) {
+            mContext = context;
+            mIntentFilter = new IntentFilter();
+            mIntentFilter.addAction(ACTION_PACKAGE_ADDED);
+            mIntentFilter.addAction(ACTION_PACKAGE_CHANGED);
+            mIntentFilter.addAction(ACTION_PACKAGE_REMOVED);
+            mIntentFilter.addDataScheme("package");
+        }
+
+        private void register() {
+            mContext.registerReceiverForAllUsers(this, mIntentFilter, /* broadcastPermission= */
+                    null, /* scheduler= */ null);
+        }
+
+        private void unregister() {
+            mContext.unregisterReceiver(this);
+        }
+
+        @Override
+        public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
+            Uri data = intent.getData();
+            if (data == null) {
+                Slog.w(TAG, "Failed to get package name in package receiver");
+                return;
+            }
+            String packageName = data.getSchemeSpecificPart();
+            String action = intent.getAction();
+            if (action == null) {
+                Slog.w(TAG, "Failed to get action in package receiver");
+                return;
+            }
+            switch (action) {
+                case ACTION_PACKAGE_ADDED:
+                case ACTION_PACKAGE_CHANGED:
+                    addAllPackageOverrides(packageName);
+                    break;
+                case ACTION_PACKAGE_REMOVED:
+                    if (!isInstalledForAnyUser(packageName)) {
+                        removeAllPackageOverrides(packageName);
+                    }
+                    break;
+                default:
+                    Slog.w(TAG, "Unsupported action in package receiver: " + action);
+                    break;
+            }
+        }
+    };
+}
diff --git a/services/core/java/com/android/server/compat/overrides/TEST_MAPPING b/services/core/java/com/android/server/compat/overrides/TEST_MAPPING
new file mode 100644
index 0000000..4b8f08e
--- /dev/null
+++ b/services/core/java/com/android/server/compat/overrides/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "FrameworksMockingServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.compat.overrides"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceState.java b/services/core/java/com/android/server/devicestate/DeviceState.java
index e693bcc..7fe24ff 100644
--- a/services/core/java/com/android/server/devicestate/DeviceState.java
+++ b/services/core/java/com/android/server/devicestate/DeviceState.java
@@ -19,11 +19,14 @@
 import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
 import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
 
+import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /**
@@ -39,6 +42,19 @@
  * @see DeviceStateManagerService
  */
 public final class DeviceState {
+    /**
+     * Flag that indicates sticky requests should be cancelled when this device state becomes the
+     * base device state.
+     */
+    public static final int FLAG_CANCEL_STICKY_REQUESTS = 1 << 0;
+
+    /** @hide */
+    @IntDef(prefix = {"FLAG_"}, flag = true, value = {
+            FLAG_CANCEL_STICKY_REQUESTS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeviceStateFlags {}
+
     /** Unique identifier for the device state. */
     @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE)
     private final int mIdentifier;
@@ -47,14 +63,19 @@
     @NonNull
     private final String mName;
 
+    @DeviceStateFlags
+    private final int mFlags;
+
     public DeviceState(
             @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
-            @NonNull String name) {
+            @NonNull String name,
+            @DeviceStateFlags int flags) {
         Preconditions.checkArgumentInRange(identifier, MINIMUM_DEVICE_STATE, MAXIMUM_DEVICE_STATE,
                 "identifier");
 
         mIdentifier = identifier;
         mName = name;
+        mFlags = flags;
     }
 
     /** Returns the unique identifier for the device state. */
@@ -69,6 +90,11 @@
         return mName;
     }
 
+    @DeviceStateFlags
+    public int getFlags() {
+        return mFlags;
+    }
+
     @Override
     public String toString() {
         return "DeviceState{" + "identifier=" + mIdentifier + ", name='" + mName + '\'' + '}';
@@ -80,11 +106,12 @@
         if (o == null || getClass() != o.getClass()) return false;
         DeviceState that = (DeviceState) o;
         return mIdentifier == that.mIdentifier
-                && Objects.equals(mName, that.mName);
+                && Objects.equals(mName, that.mName)
+                && mFlags == that.mFlags;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mIdentifier, mName);
+        return Objects.hash(mIdentifier, mName, mFlags);
     }
 }
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index a8b0994..792feea 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -19,23 +19,27 @@
 import static android.Manifest.permission.CONTROL_DEVICE_STATE;
 import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
 import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
-import static android.hardware.devicestate.DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES;
 
+import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_SUSPENDED;
+
+import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.devicestate.DeviceStateInfo;
 import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateManagerInternal;
 import android.hardware.devicestate.IDeviceStateManager;
 import android.hardware.devicestate.IDeviceStateManagerCallback;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
-import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Slog;
 import android.util.SparseArray;
 
@@ -43,14 +47,21 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.DisplayThread;
+import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.policy.DeviceStatePolicyImpl;
+import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.WindowProcessController;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.WeakHashMap;
 
 /**
  * A system service that manages the state of a device with user-configurable hardware like a
@@ -81,10 +92,19 @@
     private static final boolean DEBUG = false;
 
     private final Object mLock = new Object();
+    // Handler on the {@link DisplayThread} used to dispatch calls to the policy and to registered
+    // callbacks though its handler (mHandler). Provides a guarantee of callback order when
+    // leveraging mHandler and also enables posting messages with the service lock held.
+    private final Handler mHandler;
     @NonNull
     private final DeviceStatePolicy mDeviceStatePolicy;
     @NonNull
     private final BinderService mBinderService;
+    @NonNull
+    private final OverrideRequestController mOverrideRequestController;
+    @VisibleForTesting
+    @NonNull
+    public ActivityTaskManagerInternal mActivityTaskManagerInternal;
 
     // All supported device states keyed by identifier.
     @GuardedBy("mLock")
@@ -109,17 +129,16 @@
     @NonNull
     private Optional<DeviceState> mBaseState = Optional.empty();
 
+    // The current active override request. When set the device state specified here will take
+    // precedence over mBaseState.
+    @GuardedBy("mLock")
+    @NonNull
+    private Optional<OverrideRequest> mActiveOverride = Optional.empty();
+
     // List of processes registered to receive notifications about changes to device state and
     // request status indexed by process id.
     @GuardedBy("mLock")
     private final SparseArray<ProcessRecord> mProcessRecords = new SparseArray<>();
-    // List of override requests with the highest precedence request at the end.
-    @GuardedBy("mLock")
-    private final ArrayList<OverrideRequestRecord> mRequestRecords = new ArrayList<>();
-    // Set of override requests that are pending a call to notifyStatusIfNeeded() to be notified
-    // of a change in status.
-    @GuardedBy("mLock")
-    private final ArraySet<OverrideRequestRecord> mRequestsPendingStatusChange = new ArraySet<>();
 
     public DeviceStateManagerService(@NonNull Context context) {
         this(context, new DeviceStatePolicyImpl(context));
@@ -128,14 +147,27 @@
     @VisibleForTesting
     DeviceStateManagerService(@NonNull Context context, @NonNull DeviceStatePolicy policy) {
         super(context);
+        // We use the DisplayThread because this service indirectly drives
+        // display (on/off) and window (position) events through its callbacks.
+        DisplayThread displayThread = DisplayThread.get();
+        mHandler = new Handler(displayThread.getLooper());
+        mOverrideRequestController = new OverrideRequestController(
+                this::onOverrideRequestStatusChangedLocked);
         mDeviceStatePolicy = policy;
         mDeviceStatePolicy.getDeviceStateProvider().setListener(new DeviceStateProviderListener());
         mBinderService = new BinderService();
+        mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
     }
 
     @Override
     public void onStart() {
         publishBinderService(Context.DEVICE_STATE_SERVICE, mBinderService);
+        publishLocalService(DeviceStateManagerInternal.class, new LocalService());
+    }
+
+    @VisibleForTesting
+    Handler getHandler() {
+        return mHandler;
     }
 
     /**
@@ -191,12 +223,10 @@
     @NonNull
     Optional<DeviceState> getOverrideState() {
         synchronized (mLock) {
-            if (mRequestRecords.isEmpty()) {
-                return Optional.empty();
+            if (mActiveOverride.isPresent()) {
+                return getStateLocked(mActiveOverride.get().getRequestedState());
             }
-
-            OverrideRequestRecord topRequest = mRequestRecords.get(mRequestRecords.size() - 1);
-            return Optional.of(topRequest.mRequestedState);
+            return Optional.empty();
         }
     }
 
@@ -212,13 +242,6 @@
     }
 
     /** Returns the list of currently supported device state identifiers. */
-    private int[] getSupportedStateIdentifiers() {
-        synchronized (mLock) {
-            return getSupportedStateIdentifiersLocked();
-        }
-    }
-
-    /** Returns the list of currently supported device state identifiers. */
     private int[] getSupportedStateIdentifiersLocked() {
         int[] supportedStates = new int[mDeviceStates.size()];
         for (int i = 0; i < supportedStates.length; i++) {
@@ -247,43 +270,41 @@
     }
 
     private void updateSupportedStates(DeviceState[] supportedDeviceStates) {
-        boolean updatedPendingState;
-        boolean hasBaseState;
         synchronized (mLock) {
             final int[] oldStateIdentifiers = getSupportedStateIdentifiersLocked();
 
+            // Whether or not at least one device state has the flag FLAG_CANCEL_STICKY_REQUESTS
+            // set. If set to true, the OverrideRequestController will be configured to allow sticky
+            // requests.
+            boolean hasTerminalDeviceState = false;
             mDeviceStates.clear();
             for (int i = 0; i < supportedDeviceStates.length; i++) {
                 DeviceState state = supportedDeviceStates[i];
+                if ((state.getFlags() & DeviceState.FLAG_CANCEL_STICKY_REQUESTS) != 0) {
+                    hasTerminalDeviceState = true;
+                }
                 mDeviceStates.put(state.getIdentifier(), state);
             }
 
+            mOverrideRequestController.setStickyRequestsAllowed(hasTerminalDeviceState);
+
             final int[] newStateIdentifiers = getSupportedStateIdentifiersLocked();
             if (Arrays.equals(oldStateIdentifiers, newStateIdentifiers)) {
                 return;
             }
 
-            final int requestSize = mRequestRecords.size();
-            for (int i = 0; i < requestSize; i++) {
-                OverrideRequestRecord request = mRequestRecords.get(i);
-                if (!isSupportedStateLocked(request.mRequestedState.getIdentifier())) {
-                    request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED);
-                }
+            mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers);
+            updatePendingStateLocked();
+
+            if (!mPendingState.isPresent()) {
+                // If the change in the supported states didn't result in a change of the pending
+                // state commitPendingState() will never be called and the callbacks will never be
+                // notified of the change.
+                notifyDeviceStateInfoChangedAsync();
             }
 
-            updatedPendingState = updatePendingStateLocked();
-            hasBaseState = mBaseState.isPresent();
+            mHandler.post(this::notifyPolicyIfNeeded);
         }
-
-        if (hasBaseState && !updatedPendingState) {
-            // If the change in the supported states didn't result in a change of the pending state
-            // commitPendingState() will never be called and the callbacks will never be notified
-            // of the change.
-            notifyDeviceStateInfoChanged();
-        }
-
-        notifyRequestsOfStatusChangeIfNeeded();
-        notifyPolicyIfNeeded();
     }
 
     /**
@@ -311,7 +332,6 @@
      * @see #isSupportedStateLocked(int)
      */
     private void setBaseState(int identifier) {
-        boolean updatedPendingState;
         synchronized (mLock) {
             final Optional<DeviceState> baseStateOptional = getStateLocked(identifier);
             if (!baseStateOptional.isPresent()) {
@@ -325,26 +345,21 @@
             }
             mBaseState = Optional.of(baseState);
 
-            final int requestSize = mRequestRecords.size();
-            for (int i = 0; i < requestSize; i++) {
-                OverrideRequestRecord request = mRequestRecords.get(i);
-                if ((request.mFlags & FLAG_CANCEL_WHEN_BASE_CHANGES) > 0) {
-                    request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED);
-                }
+            if ((baseState.getFlags() & DeviceState.FLAG_CANCEL_STICKY_REQUESTS) != 0) {
+                mOverrideRequestController.cancelStickyRequests();
+            }
+            mOverrideRequestController.handleBaseStateChanged();
+            updatePendingStateLocked();
+
+            if (!mPendingState.isPresent()) {
+                // If the change in base state didn't result in a change of the pending state
+                // commitPendingState() will never be called and the callbacks will never be
+                // notified of the change.
+                notifyDeviceStateInfoChangedAsync();
             }
 
-            updatedPendingState = updatePendingStateLocked();
+            mHandler.post(this::notifyPolicyIfNeeded);
         }
-
-        if (!updatedPendingState) {
-            // If the change in base state didn't result in a change of the pending state
-            // commitPendingState() will never be called and the callbacks will never be notified
-            // of the change.
-            notifyDeviceStateInfoChanged();
-        }
-
-        notifyRequestsOfStatusChangeIfNeeded();
-        notifyPolicyIfNeeded();
     }
 
     /**
@@ -362,8 +377,8 @@
         }
 
         final DeviceState stateToConfigure;
-        if (!mRequestRecords.isEmpty()) {
-            stateToConfigure = mRequestRecords.get(mRequestRecords.size() - 1).mRequestedState;
+        if (mActiveOverride.isPresent()) {
+            stateToConfigure = getStateLocked(mActiveOverride.get().getRequestedState()).get();
         } else if (mBaseState.isPresent()
                 && isSupportedStateLocked(mBaseState.get().getIdentifier())) {
             // Base state could have recently become unsupported after a change in supported states.
@@ -429,108 +444,106 @@
      * </p>
      */
     private void commitPendingState() {
-        // Update the current state.
         synchronized (mLock) {
             final DeviceState newState = mPendingState.get();
             if (DEBUG) {
                 Slog.d(TAG, "Committing state: " + newState);
             }
 
-            if (!mRequestRecords.isEmpty()) {
-                final OverrideRequestRecord topRequest =
-                        mRequestRecords.get(mRequestRecords.size() - 1);
-                if (topRequest.mRequestedState.getIdentifier() == newState.getIdentifier()) {
-                    // The top request could have come in while the service was awaiting callback
-                    // from the policy. In that case we only set it to active if it matches the
-                    // current committed state, otherwise it will be set to active when its
-                    // requested state is committed.
-                    topRequest.setStatusLocked(OverrideRequestRecord.STATUS_ACTIVE);
-                }
-            }
-
             FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_STATE_CHANGED,
                     newState.getIdentifier(), !mCommittedState.isPresent());
 
             mCommittedState = Optional.of(newState);
             mPendingState = Optional.empty();
             updatePendingStateLocked();
+
+            // Notify callbacks of a change.
+            notifyDeviceStateInfoChangedAsync();
+
+            // The top request could have come in while the service was awaiting callback
+            // from the policy. In that case we only set it to active if it matches the
+            // current committed state, otherwise it will be set to active when its
+            // requested state is committed.
+            OverrideRequest activeRequest = mActiveOverride.orElse(null);
+            if (activeRequest != null
+                    && activeRequest.getRequestedState() == newState.getIdentifier()) {
+                ProcessRecord processRecord = mProcessRecords.get(activeRequest.getPid());
+                if (processRecord != null) {
+                    processRecord.notifyRequestActiveAsync(activeRequest.getToken());
+                }
+            }
+
+            // Try to configure the next state if needed.
+            mHandler.post(this::notifyPolicyIfNeeded);
         }
-
-        // Notify callbacks of a change.
-        notifyDeviceStateInfoChanged();
-
-        // Notify the top request that it's active.
-        notifyRequestsOfStatusChangeIfNeeded();
-
-        // Try to configure the next state if needed.
-        notifyPolicyIfNeeded();
     }
 
-    private void notifyDeviceStateInfoChanged() {
-        if (Thread.holdsLock(mLock)) {
-            throw new IllegalStateException(
-                    "Attempting to notify callbacks with service lock held.");
-        }
-
-        // Grab the lock and copy the process records and the current info.
-        ArrayList<ProcessRecord> registeredProcesses;
-        DeviceStateInfo info;
+    private void notifyDeviceStateInfoChangedAsync() {
         synchronized (mLock) {
             if (mProcessRecords.size() == 0) {
                 return;
             }
 
-            registeredProcesses = new ArrayList<>();
+            ArrayList<ProcessRecord> registeredProcesses = new ArrayList<>();
             for (int i = 0; i < mProcessRecords.size(); i++) {
                 registeredProcesses.add(mProcessRecords.valueAt(i));
             }
 
-            info = getDeviceStateInfoLocked();
-        }
+            DeviceStateInfo info = getDeviceStateInfoLocked();
 
-        // After releasing the lock, send the notifications out.
-        for (int i = 0; i < registeredProcesses.size(); i++) {
-            registeredProcesses.get(i).notifyDeviceStateInfoAsync(info);
+            for (int i = 0; i < registeredProcesses.size(); i++) {
+                registeredProcesses.get(i).notifyDeviceStateInfoAsync(info);
+            }
         }
     }
 
-    /**
-     * Notifies all dirty requests (requests that have a change in status, but have not yet been
-     * notified) that their status has changed.
-     */
-    private void notifyRequestsOfStatusChangeIfNeeded() {
-        if (Thread.holdsLock(mLock)) {
-            throw new IllegalStateException(
-                    "Attempting to notify requests with service lock held.");
-        }
-
-        ArraySet<OverrideRequestRecord> dirtyRequests;
-        synchronized (mLock) {
-            if (mRequestsPendingStatusChange.isEmpty()) {
-                return;
+    private void onOverrideRequestStatusChangedLocked(@NonNull OverrideRequest request,
+            @OverrideRequestController.RequestStatus int status) {
+        if (status == STATUS_ACTIVE) {
+            mActiveOverride = Optional.of(request);
+        } else if (status == STATUS_SUSPENDED || status == STATUS_CANCELED) {
+            if (mActiveOverride.isPresent() && mActiveOverride.get() == request) {
+                mActiveOverride = Optional.empty();
             }
-
-            dirtyRequests = new ArraySet<>(mRequestsPendingStatusChange);
-            mRequestsPendingStatusChange.clear();
+        } else {
+            throw new IllegalArgumentException("Unknown request status: " + status);
         }
 
-        // After releasing the lock, send the notifications out.
-        for (int i = 0; i < dirtyRequests.size(); i++) {
-            dirtyRequests.valueAt(i).notifyStatusIfNeeded();
+        boolean updatedPendingState = updatePendingStateLocked();
+
+        ProcessRecord processRecord = mProcessRecords.get(request.getPid());
+        if (processRecord == null) {
+            // If the process is no longer registered with the service, for example if it has died,
+            // there is no need to notify it of a change in request status.
+            mHandler.post(this::notifyPolicyIfNeeded);
+            return;
         }
+
+        if (status == STATUS_ACTIVE) {
+            if (!updatedPendingState && !mPendingState.isPresent()) {
+                // If the pending state was not updated and there is not currently a pending state
+                // then this newly active request will never be notified of a change in state.
+                // Schedule the notification now.
+                processRecord.notifyRequestActiveAsync(request.getToken());
+            }
+        } else if (status == STATUS_SUSPENDED) {
+            processRecord.notifyRequestSuspendedAsync(request.getToken());
+        } else {
+            processRecord.notifyRequestCanceledAsync(request.getToken());
+        }
+
+        mHandler.post(this::notifyPolicyIfNeeded);
     }
 
     private void registerProcess(int pid, IDeviceStateManagerCallback callback) {
-        DeviceStateInfo currentInfo;
-        ProcessRecord record;
-        // Grab the lock to register the callback and get the current state.
         synchronized (mLock) {
             if (mProcessRecords.contains(pid)) {
                 throw new SecurityException("The calling process has already registered an"
                         + " IDeviceStateManagerCallback.");
             }
 
-            record = new ProcessRecord(callback, pid);
+            ProcessRecord record = new ProcessRecord(callback, pid, this::handleProcessDied,
+                    mHandler);
             try {
                 callback.asBinder().linkToDeath(record, 0);
             } catch (RemoteException ex) {
@@ -538,34 +551,21 @@
             }
             mProcessRecords.put(pid, record);
 
-            currentInfo = mCommittedState.isPresent() ? getDeviceStateInfoLocked() : null;
-        }
-
-        if (currentInfo != null) {
-            // If there is not a committed state we'll wait to notify the process of the initial
-            // value.
-            record.notifyDeviceStateInfoAsync(currentInfo);
+            DeviceStateInfo currentInfo = mCommittedState.isPresent()
+                    ? getDeviceStateInfoLocked() : null;
+            if (currentInfo != null) {
+                // If there is not a committed state we'll wait to notify the process of the initial
+                // value.
+                record.notifyDeviceStateInfoAsync(currentInfo);
+            }
         }
     }
 
     private void handleProcessDied(ProcessRecord processRecord) {
         synchronized (mLock) {
-            // Cancel all requests from this process.
-            final int requestCount = processRecord.mRequestRecords.size();
-            for (int i = 0; i < requestCount; i++) {
-                final OverrideRequestRecord request = processRecord.mRequestRecords.valueAt(i);
-                // Cancel the request but don't mark it as dirty since there's no need to send
-                // notifications if the process has died.
-                request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED,
-                        false /* markDirty */);
-            }
-
             mProcessRecords.remove(processRecord.mPid);
-
-            updatePendingStateLocked();
+            mOverrideRequestController.handleProcessDied(processRecord.mPid);
         }
-
-        notifyPolicyIfNeeded();
     }
 
     private void requestStateInternal(int state, int flags, int callingPid,
@@ -577,7 +577,7 @@
                         + " has no registered callback.");
             }
 
-            if (processRecord.mRequestRecords.get(token) != null) {
+            if (mOverrideRequestController.hasRequest(token)) {
                 throw new IllegalStateException("Request has already been made for the supplied"
                         + " token: " + token);
             }
@@ -588,27 +588,9 @@
                         + " is not supported.");
             }
 
-            OverrideRequestRecord topRecord = mRequestRecords.isEmpty()
-                    ? null : mRequestRecords.get(mRequestRecords.size() - 1);
-            if (topRecord != null) {
-                topRecord.setStatusLocked(OverrideRequestRecord.STATUS_SUSPENDED);
-            }
-
-            final OverrideRequestRecord request =
-                    new OverrideRequestRecord(processRecord, token, deviceState.get(), flags);
-            mRequestRecords.add(request);
-            processRecord.mRequestRecords.put(request.mToken, request);
-
-            final boolean updatedPendingState = updatePendingStateLocked();
-            if (!updatedPendingState && !mPendingState.isPresent()) {
-                // We don't set the status of the new request to ACTIVE if the request updated the
-                // pending state as it will be set in commitPendingState().
-                request.setStatusLocked(OverrideRequestRecord.STATUS_ACTIVE, true /* markDirty */);
-            }
+            OverrideRequest request = new OverrideRequest(token, callingPid, state, flags);
+            mOverrideRequestController.addRequest(request);
         }
-
-        notifyRequestsOfStatusChangeIfNeeded();
-        notifyPolicyIfNeeded();
     }
 
     private void cancelRequestInternal(int callingPid, @NonNull IBinder token) {
@@ -619,18 +601,8 @@
                         + " has no registered callback.");
             }
 
-            OverrideRequestRecord request = processRecord.mRequestRecords.get(token);
-            if (request == null) {
-                throw new IllegalStateException("No known request for the given token");
-            }
-
-            request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED);
-
-            updatePendingStateLocked();
+            mOverrideRequestController.cancelRequest(token);
         }
-
-        notifyRequestsOfStatusChangeIfNeeded();
-        notifyPolicyIfNeeded();
     }
 
     private void dumpInternal(PrintWriter pw) {
@@ -650,16 +622,7 @@
                 pw.println("  " + i + ": mPid=" + processRecord.mPid);
             }
 
-            final int requestCount = mRequestRecords.size();
-            pw.println();
-            pw.println("Override requests: size=" + requestCount);
-            for (int i = 0; i < requestCount; i++) {
-                OverrideRequestRecord requestRecord = mRequestRecords.get(i);
-                pw.println("  " + i + ": mPid=" + requestRecord.mProcessRecord.mPid
-                        + ", mRequestedState=" + requestRecord.mRequestedState
-                        + ", mFlags=" + requestRecord.mFlags
-                        + ", mStatus=" + requestRecord.statusToString(requestRecord.mStatus));
-            }
+            mOverrideRequestController.dumpInternal(pw);
         }
     }
 
@@ -683,142 +646,107 @@
         }
     }
 
-    private final class ProcessRecord implements IBinder.DeathRecipient {
+    private static final class ProcessRecord implements IBinder.DeathRecipient {
+        public interface DeathListener {
+            void onProcessDied(ProcessRecord record);
+        }
+
+        private static final int STATUS_ACTIVE = 0;
+
+        private static final int STATUS_SUSPENDED = 1;
+
+        private static final int STATUS_CANCELED = 2;
+
+        @IntDef(prefix = {"STATUS_"}, value = {
+                STATUS_ACTIVE,
+                STATUS_SUSPENDED,
+                STATUS_CANCELED
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        private @interface RequestStatus {}
+
         private final IDeviceStateManagerCallback mCallback;
         private final int mPid;
+        private final DeathListener mDeathListener;
+        private final Handler mHandler;
 
-        private final ArrayMap<IBinder, OverrideRequestRecord> mRequestRecords = new ArrayMap<>();
+        private final WeakHashMap<IBinder, Integer> mLastNotifiedStatus = new WeakHashMap<>();
 
-        ProcessRecord(IDeviceStateManagerCallback callback, int pid) {
+        ProcessRecord(IDeviceStateManagerCallback callback, int pid, DeathListener deathListener,
+                Handler handler) {
             mCallback = callback;
             mPid = pid;
+            mDeathListener = deathListener;
+            mHandler = handler;
         }
 
         @Override
         public void binderDied() {
-            handleProcessDied(this);
+            mDeathListener.onProcessDied(this);
         }
 
         public void notifyDeviceStateInfoAsync(@NonNull DeviceStateInfo info) {
-            try {
-                mCallback.onDeviceStateInfoChanged(info);
-            } catch (RemoteException ex) {
-                Slog.w(TAG, "Failed to notify process " + mPid + " that device state changed.",
-                        ex);
-            }
-        }
-
-        public void notifyRequestActiveAsync(OverrideRequestRecord request) {
-            try {
-                mCallback.onRequestActive(request.mToken);
-            } catch (RemoteException ex) {
-                Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
-                        ex);
-            }
-        }
-
-        public void notifyRequestSuspendedAsync(OverrideRequestRecord request) {
-            try {
-                mCallback.onRequestSuspended(request.mToken);
-            } catch (RemoteException ex) {
-                Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
-                        ex);
-            }
-        }
-
-        public void notifyRequestCanceledAsync(OverrideRequestRecord request) {
-            try {
-                mCallback.onRequestCanceled(request.mToken);
-            } catch (RemoteException ex) {
-                Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
-                        ex);
-            }
-        }
-    }
-
-    /** A record describing a request to override the state of the device. */
-    private final class OverrideRequestRecord {
-        public static final int STATUS_UNKNOWN = 0;
-        public static final int STATUS_ACTIVE = 1;
-        public static final int STATUS_SUSPENDED = 2;
-        public static final int STATUS_CANCELED = 3;
-
-        @Nullable
-        public String statusToString(int status) {
-            switch (status) {
-                case STATUS_ACTIVE:
-                    return "ACTIVE";
-                case STATUS_SUSPENDED:
-                    return "SUSPENDED";
-                case STATUS_CANCELED:
-                    return "CANCELED";
-                case STATUS_UNKNOWN:
-                    return "UNKNOWN";
-                default:
-                    return null;
-            }
-        }
-
-        private final ProcessRecord mProcessRecord;
-        @NonNull
-        private final IBinder mToken;
-        @NonNull
-        private final DeviceState mRequestedState;
-        private final int mFlags;
-
-        private int mStatus = STATUS_UNKNOWN;
-        private int mLastNotifiedStatus = STATUS_UNKNOWN;
-
-        OverrideRequestRecord(@NonNull ProcessRecord processRecord, @NonNull IBinder token,
-                @NonNull DeviceState requestedState, int flags) {
-            mProcessRecord = processRecord;
-            mToken = token;
-            mRequestedState = requestedState;
-            mFlags = flags;
-        }
-
-        public void setStatusLocked(int status) {
-            setStatusLocked(status, true /* markDirty */);
-        }
-
-        public void setStatusLocked(int status, boolean markDirty) {
-            if (mStatus != status) {
-                if (mStatus == STATUS_CANCELED) {
-                    throw new IllegalStateException(
-                            "Can not alter the status of a request after set to CANCELED.");
+            mHandler.post(() -> {
+                try {
+                    mCallback.onDeviceStateInfoChanged(info);
+                } catch (RemoteException ex) {
+                    Slog.w(TAG, "Failed to notify process " + mPid + " that device state changed.",
+                            ex);
                 }
-
-                mStatus = status;
-
-                if (mStatus == STATUS_CANCELED) {
-                    mRequestRecords.remove(this);
-                    mProcessRecord.mRequestRecords.remove(mToken);
-                }
-
-                if (markDirty) {
-                    mRequestsPendingStatusChange.add(this);
-                }
-            }
+            });
         }
 
-        public void notifyStatusIfNeeded() {
-            int stateToReport;
-            synchronized (mLock) {
-                if (mLastNotifiedStatus == mStatus) {
-                    return;
+        public void notifyRequestActiveAsync(IBinder token) {
+            @RequestStatus Integer lastStatus = mLastNotifiedStatus.get(token);
+            if (lastStatus != null
+                    && (lastStatus == STATUS_ACTIVE || lastStatus == STATUS_CANCELED)) {
+                return;
+            }
+
+            mLastNotifiedStatus.put(token, STATUS_ACTIVE);
+            mHandler.post(() -> {
+                try {
+                    mCallback.onRequestActive(token);
+                } catch (RemoteException ex) {
+                    Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
+                            ex);
                 }
+            });
+        }
 
-                stateToReport = mStatus;
-                mLastNotifiedStatus = mStatus;
+        public void notifyRequestSuspendedAsync(IBinder token) {
+            @RequestStatus Integer lastStatus = mLastNotifiedStatus.get(token);
+            if (lastStatus != null
+                    && (lastStatus == STATUS_SUSPENDED || lastStatus == STATUS_CANCELED)) {
+                return;
             }
 
-            if (stateToReport == STATUS_ACTIVE) {
-                mProcessRecord.notifyRequestActiveAsync(this);
-            } else if (stateToReport == STATUS_SUSPENDED) {
-                mProcessRecord.notifyRequestSuspendedAsync(this);
-            } else if (stateToReport == STATUS_CANCELED) {
-                mProcessRecord.notifyRequestCanceledAsync(this);
+            mLastNotifiedStatus.put(token, STATUS_SUSPENDED);
+            mHandler.post(() -> {
+                try {
+                    mCallback.onRequestSuspended(token);
+                } catch (RemoteException ex) {
+                    Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
+                            ex);
+                }
+            });
+        }
+
+        public void notifyRequestCanceledAsync(IBinder token) {
+            @RequestStatus Integer lastStatus = mLastNotifiedStatus.get(token);
+            if (lastStatus != null && lastStatus == STATUS_CANCELED) {
+                return;
             }
+
+            mLastNotifiedStatus.put(token, STATUS_CANCELED);
+            mHandler.post(() -> {
+                try {
+                    mCallback.onRequestCanceled(token);
+                } catch (RemoteException ex) {
+                    Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
+                            ex);
+                }
+            });
         }
     }
 
@@ -848,14 +776,21 @@
 
         @Override // Binder call
         public void requestState(IBinder token, int state, int flags) {
-            getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE,
-                    "Permission required to request device state.");
+            final int callingPid = Binder.getCallingPid();
+            // Allow top processes to request a device state change
+            // If the calling process ID is not the top app, then we check if this process
+            // holds a permission to CONTROL_DEVICE_STATE
+            final WindowProcessController topApp = mActivityTaskManagerInternal.getTopApp();
+            if (topApp.getPid() != callingPid) {
+                getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE,
+                        "Permission required to request device state, "
+                                + "or the call must come from the top focused app.");
+            }
 
             if (token == null) {
                 throw new IllegalArgumentException("Request token must not be null.");
             }
 
-            final int callingPid = Binder.getCallingPid();
             final long callingIdentity = Binder.clearCallingIdentity();
             try {
                 requestStateInternal(state, flags, callingPid, token);
@@ -866,14 +801,21 @@
 
         @Override // Binder call
         public void cancelRequest(IBinder token) {
-            getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE,
-                    "Permission required to clear requested device state.");
+            final int callingPid = Binder.getCallingPid();
+            // Allow top processes to cancel a device state change
+            // If the calling process ID is not the top app, then we check if this process
+            // holds a permission to CONTROL_DEVICE_STATE
+            final WindowProcessController topApp = mActivityTaskManagerInternal.getTopApp();
+            if (topApp.getPid() != callingPid) {
+                getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE,
+                        "Permission required to cancel device state, "
+                                + "or the call must come from the top focused app.");
+            }
 
             if (token == null) {
                 throw new IllegalArgumentException("Request token must not be null.");
             }
 
-            final int callingPid = Binder.getCallingPid();
             final long callingIdentity = Binder.clearCallingIdentity();
             try {
                 cancelRequestInternal(callingPid, token);
@@ -901,4 +843,14 @@
             }
         }
     }
+
+    /** Implementation of {@link DeviceStateManagerInternal} published as a local service. */
+    private final class LocalService extends DeviceStateManagerInternal {
+        @Override
+        public int[] getSupportedStateIdentifiers() {
+            synchronized (mLock) {
+                return getSupportedStateIdentifiersLocked();
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
index 56b68b7..eed68f8 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
@@ -27,7 +27,9 @@
 import android.os.ShellCommand;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 /**
  * ShellCommands for {@link DeviceStateManagerService}.
@@ -56,14 +58,18 @@
         switch (cmd) {
             case "state":
                 return runState(pw);
+            case "print-state":
+                return runPrintState(pw);
             case "print-states":
                 return runPrintStates(pw);
+            case "print-states-simple":
+                return runPrintStatesSimple(pw);
             default:
                 return handleDefaultCommands(cmd);
         }
     }
 
-    private void printState(PrintWriter pw) {
+    private void printAllStates(PrintWriter pw) {
         Optional<DeviceState> committedState = mService.getCommittedState();
         Optional<DeviceState> baseState = mService.getBaseState();
         Optional<DeviceState> overrideState = mService.getOverrideState();
@@ -79,7 +85,8 @@
     private int runState(PrintWriter pw) {
         final String nextArg = getNextArg();
         if (nextArg == null) {
-            printState(pw);
+            printAllStates(pw);
+            return 0;
         }
 
         final Context context = mService.getContext();
@@ -123,6 +130,16 @@
         return 0;
     }
 
+    private int runPrintState(PrintWriter pw) {
+        Optional<DeviceState> deviceState = mService.getCommittedState();
+        if (deviceState.isPresent()) {
+            pw.println(deviceState.get().getIdentifier());
+            return 0;
+        }
+        getErrPrintWriter().println("Error: device state not available.");
+        return 1;
+    }
+
     private int runPrintStates(PrintWriter pw) {
         DeviceState[] states = mService.getSupportedStates();
         pw.print("Supported states: [\n");
@@ -133,6 +150,14 @@
         return 0;
     }
 
+    private int runPrintStatesSimple(PrintWriter pw) {
+        pw.print(Arrays.stream(mService.getSupportedStates())
+                .map(DeviceState::getIdentifier)
+                .map(Object::toString)
+                .collect(Collectors.joining(",")));
+        return 0;
+    }
+
     @Override
     public void onHelp() {
         PrintWriter pw = getOutPrintWriter();
@@ -141,8 +166,12 @@
         pw.println("    Print this help text.");
         pw.println("  state [reset|OVERRIDE_DEVICE_STATE]");
         pw.println("    Return or override device state.");
+        pw.println("  print-state");
+        pw.println("    Return the current device state.");
         pw.println("  print-states");
         pw.println("    Return list of currently supported device states.");
+        pw.println("  print-states-simple");
+        pw.println("    Return the currently supported device states in comma separated format.");
     }
 
     private static String toString(@NonNull Optional<DeviceState> state) {
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java
new file mode 100644
index 0000000..35a4c84
--- /dev/null
+++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicestate;
+
+import android.hardware.devicestate.DeviceStateRequest;
+import android.os.IBinder;
+
+/**
+ * A request to override the state managed by {@link DeviceStateManagerService}.
+ *
+ * @see OverrideRequestController
+ */
+final class OverrideRequest {
+    private final IBinder mToken;
+    private final int mPid;
+    private final int mRequestedState;
+    @DeviceStateRequest.RequestFlags
+    private final int mFlags;
+
+    OverrideRequest(IBinder token, int pid, int requestedState,
+            @DeviceStateRequest.RequestFlags int flags) {
+        mToken = token;
+        mPid = pid;
+        mRequestedState = requestedState;
+        mFlags = flags;
+    }
+
+    IBinder getToken() {
+        return mToken;
+    }
+
+    int getPid() {
+        return mPid;
+    }
+
+    int getRequestedState() {
+        return mRequestedState;
+    }
+
+    @DeviceStateRequest.RequestFlags
+    int getFlags() {
+        return mFlags;
+    }
+}
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
new file mode 100644
index 0000000..05c9eb2
--- /dev/null
+++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicestate;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.os.IBinder;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the lifecycle of override requests.
+ * <p>
+ * New requests are added with {@link #addRequest(OverrideRequest)} and are kept active until
+ * either:
+ * <ul>
+ *     <li>A new request is added with {@link #addRequest(OverrideRequest)}, in which case the
+ *     request will become suspended.</li>
+ *     <li>The request is cancelled with {@link #cancelRequest(IBinder)} or as a side effect
+ *     of other methods calls, such as {@link #handleProcessDied(int)}.</li>
+ * </ul>
+ */
+final class OverrideRequestController {
+    static final int STATUS_UNKNOWN = 0;
+    /**
+     * The request is the top-most request.
+     */
+    static final int STATUS_ACTIVE = 1;
+    /**
+     * The request is still present but is being superseded by another request.
+     */
+    static final int STATUS_SUSPENDED = 2;
+    /**
+     * The request is not longer valid.
+     */
+    static final int STATUS_CANCELED = 3;
+
+    @IntDef(prefix = {"STATUS_"}, value = {
+            STATUS_UNKNOWN,
+            STATUS_ACTIVE,
+            STATUS_SUSPENDED,
+            STATUS_CANCELED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface RequestStatus {}
+
+    static String statusToString(@RequestStatus int status) {
+        switch (status) {
+            case STATUS_ACTIVE:
+                return "ACTIVE";
+            case STATUS_SUSPENDED:
+                return "SUSPENDED";
+            case STATUS_CANCELED:
+                return "CANCELED";
+            case STATUS_UNKNOWN:
+                return "UNKNOWN";
+        }
+        throw new IllegalArgumentException("Unknown status: " + status);
+    }
+
+    private final StatusChangeListener mListener;
+    private final List<OverrideRequest> mTmpRequestsToCancel = new ArrayList<>();
+
+    // List of override requests with the most recent override request at the end.
+    private final ArrayList<OverrideRequest> mRequests = new ArrayList<>();
+
+    private boolean mStickyRequestsAllowed;
+    // List of override requests that have outlived their process and will only be cancelled through
+    // a call to cancelStickyRequests().
+    private final ArrayList<OverrideRequest> mStickyRequests = new ArrayList<>();
+
+    OverrideRequestController(@NonNull StatusChangeListener listener) {
+        mListener = listener;
+    }
+
+    /**
+     * Sets sticky requests as either allowed or disallowed. When sticky requests are allowed a call
+     * to {@link #handleProcessDied(int)} will not result in the request being cancelled
+     * immediately. Instead, the request will be marked sticky and must be cancelled with a call
+     * to {@link #cancelStickyRequests()}.
+     */
+    void setStickyRequestsAllowed(boolean stickyRequestsAllowed) {
+        mStickyRequestsAllowed = stickyRequestsAllowed;
+        if (!mStickyRequestsAllowed) {
+            cancelStickyRequests();
+        }
+    }
+
+    /**
+     * Adds a request to the top of the stack and notifies the listener of all changes to request
+     * status as a result of this operation.
+     */
+    void addRequest(@NonNull OverrideRequest request) {
+        mRequests.add(request);
+        mListener.onStatusChanged(request, STATUS_ACTIVE);
+
+        if (mRequests.size() > 1) {
+            OverrideRequest prevRequest = mRequests.get(mRequests.size() - 2);
+            mListener.onStatusChanged(prevRequest, STATUS_SUSPENDED);
+        }
+    }
+
+    /**
+     * Cancels the request with the specified {@code token} and notifies the listener of all changes
+     * to request status as a result of this operation.
+     */
+    void cancelRequest(@NonNull IBinder token) {
+        int index = getRequestIndex(token);
+        if (index == -1) {
+            return;
+        }
+
+        OverrideRequest request = mRequests.remove(index);
+        if (index == mRequests.size() && mRequests.size() > 0) {
+            // We removed the current active request so we need to set the new active request
+            // before cancelling this request.
+            OverrideRequest newTop = getLast(mRequests);
+            mListener.onStatusChanged(newTop, STATUS_ACTIVE);
+        }
+        mListener.onStatusChanged(request, STATUS_CANCELED);
+    }
+
+    /**
+     * Cancels all requests that are currently marked sticky and notifies the listener of all
+     * changes to request status as a result of this operation.
+     *
+     * @see #setStickyRequestsAllowed(boolean)
+     */
+    void cancelStickyRequests() {
+        mTmpRequestsToCancel.clear();
+        mTmpRequestsToCancel.addAll(mStickyRequests);
+        cancelRequestsLocked(mTmpRequestsToCancel);
+    }
+
+    /**
+     * Returns {@code true} if this controller is current managing a request with the specified
+     * {@code token}, {@code false} otherwise.
+     */
+    boolean hasRequest(@NonNull IBinder token) {
+        return getRequestIndex(token) != -1;
+    }
+
+    /**
+     * Notifies the controller that the process with the specified {@code pid} has died. The
+     * controller will notify the listener of all changes to request status as a result of this
+     * operation.
+     */
+    void handleProcessDied(int pid) {
+        if (mRequests.isEmpty()) {
+            return;
+        }
+
+        mTmpRequestsToCancel.clear();
+        OverrideRequest prevActiveRequest = getLast(mRequests);
+        for (OverrideRequest request : mRequests) {
+            if (request.getPid() == pid) {
+                mTmpRequestsToCancel.add(request);
+            }
+        }
+
+        if (mStickyRequestsAllowed) {
+            // Do not cancel the requests now because sticky requests are allowed. These
+            // requests will be cancelled on a call to cancelStickyRequests().
+            mStickyRequests.addAll(mTmpRequestsToCancel);
+            return;
+        }
+
+        cancelRequestsLocked(mTmpRequestsToCancel);
+    }
+
+    /**
+     * Notifies the controller that the base state has changed. The controller will notify the
+     * listener of all changes to request status as a result of this change.
+     *
+     * @return {@code true} if calling this method has lead to a new active request, {@code false}
+     * otherwise.
+     */
+    boolean handleBaseStateChanged() {
+        if (mRequests.isEmpty()) {
+            return false;
+        }
+
+        mTmpRequestsToCancel.clear();
+        OverrideRequest prevActiveRequest = getLast(mRequests);
+        for (int i = 0; i < mRequests.size(); i++) {
+            OverrideRequest request = mRequests.get(i);
+            if ((request.getFlags() & DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES) != 0) {
+                mTmpRequestsToCancel.add(request);
+            }
+        }
+
+        final boolean newActiveRequest = cancelRequestsLocked(mTmpRequestsToCancel);
+        return newActiveRequest;
+    }
+
+    /**
+     * Notifies the controller that the set of supported states has changed. The controller will
+     * notify the listener of all changes to request status as a result of this change.
+     *
+     * @return {@code true} if calling this method has lead to a new active request, {@code false}
+     * otherwise.
+     */
+    boolean handleNewSupportedStates(int[] newSupportedStates) {
+        if (mRequests.isEmpty()) {
+            return false;
+        }
+
+        mTmpRequestsToCancel.clear();
+        for (int i = 0; i < mRequests.size(); i++) {
+            OverrideRequest request = mRequests.get(i);
+            if (!contains(newSupportedStates, request.getRequestedState())) {
+                mTmpRequestsToCancel.add(request);
+            }
+        }
+
+        final boolean newActiveRequest = cancelRequestsLocked(mTmpRequestsToCancel);
+        return newActiveRequest;
+    }
+
+    void dumpInternal(PrintWriter pw) {
+        final int requestCount = mRequests.size();
+        pw.println();
+        pw.println("Override requests: size=" + requestCount);
+        for (int i = 0; i < requestCount; i++) {
+            OverrideRequest overrideRequest = mRequests.get(i);
+            int status = (i == requestCount - 1) ? STATUS_ACTIVE : STATUS_SUSPENDED;
+            pw.println("  " + i + ": mPid=" + overrideRequest.getPid()
+                    + ", mRequestedState=" + overrideRequest.getRequestedState()
+                    + ", mFlags=" + overrideRequest.getFlags()
+                    + ", mStatus=" + statusToString(status));
+        }
+    }
+
+    /**
+     * Handles cancelling a set of requests. If the set of requests to cancel will lead to a new
+     * request becoming active this request will also be notified of its change in state.
+     *
+     * @return {@code true} if calling this method has lead to a new active request, {@code false}
+     * otherwise.
+     */
+    private boolean cancelRequestsLocked(List<OverrideRequest> requestsToCancel) {
+        if (requestsToCancel.isEmpty()) {
+            return false;
+        }
+
+        OverrideRequest prevActiveRequest = getLast(mRequests);
+        boolean causedNewRequestToBecomeActive = false;
+        mRequests.removeAll(requestsToCancel);
+        mStickyRequests.removeAll(requestsToCancel);
+        if (!mRequests.isEmpty()) {
+            OverrideRequest newActiveRequest = getLast(mRequests);
+            if (newActiveRequest != prevActiveRequest) {
+                mListener.onStatusChanged(newActiveRequest, STATUS_ACTIVE);
+                causedNewRequestToBecomeActive = true;
+            }
+        }
+
+        for (int i = 0; i < requestsToCancel.size(); i++) {
+            mListener.onStatusChanged(requestsToCancel.get(i), STATUS_CANCELED);
+        }
+        return causedNewRequestToBecomeActive;
+    }
+
+    private int getRequestIndex(@NonNull IBinder token) {
+        final int numberOfRequests = mRequests.size();
+        if (numberOfRequests == 0) {
+            return -1;
+        }
+
+        for (int i = 0; i < numberOfRequests; i++) {
+            OverrideRequest request = mRequests.get(i);
+            if (request.getToken() == token) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Nullable
+    private static <T> T getLast(List<T> list) {
+        return list.size() > 0 ? list.get(list.size() - 1) : null;
+    }
+
+    private static boolean contains(int[] array, int value) {
+        for (int i = 0; i < array.length; i++) {
+            if (array[i] == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public interface StatusChangeListener {
+        /**
+         * Notifies the listener of a change in request status. If a change within the controller
+         * causes one request to become active and one to become either suspended or cancelled, this
+         * method is guaranteed to be called with the active request first before the suspended or
+         * cancelled request.
+         */
+        void onStatusChanged(@NonNull OverrideRequest request, @RequestStatus int newStatus);
+    }
+}
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index 1acd5d0..9dd2f84 100644
--- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
+++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
@@ -111,8 +111,8 @@
                 for (com.android.server.display.config.layout.Display d: l.getDisplay()) {
                     layout.createDisplayLocked(
                             DisplayAddress.fromPhysicalDisplayId(d.getAddress().longValue()),
-                            d.getIsDefault(),
-                            d.getEnabled());
+                            d.isDefaultDisplay(),
+                            d.isEnabled());
                 }
             }
         } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) {
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index 35f2957..806bcc2 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -16,7 +16,9 @@
 
 package com.android.server.display;
 
+import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.display.DisplayViewport;
 import android.os.IBinder;
@@ -43,6 +45,7 @@
     // The display device does not manage these properties itself, they are set by
     // the display manager service.  The display device shouldn't really be looking at these.
     private int mCurrentLayerStack = -1;
+    private int mCurrentFlags = 0;
     private int mCurrentOrientation = -1;
     private Rect mCurrentLayerStackRect;
     private Rect mCurrentDisplayRect;
@@ -104,6 +107,34 @@
     }
 
     /**
+     * Returns the window token of the level of the WindowManager hierarchy to mirror, or null
+     * if layer mirroring by SurfaceFlinger should not be performed.
+     * For now, only used for mirroring started from MediaProjection.
+     */
+    @Nullable
+    public IBinder getWindowTokenClientToMirrorLocked() {
+        return null;
+    }
+
+    /**
+     * Updates the window token of the level of the level of the WindowManager hierarchy to mirror.
+     * If windowToken is null, then no layer mirroring by SurfaceFlinger to should be performed.
+     * For now, only used for mirroring started from MediaProjection.
+     */
+    public void setWindowTokenClientToMirrorLocked(IBinder windowToken) {
+    }
+
+    /**
+     * Returns the default size of the surface associated with the display, or null if the surface
+     * is not provided for layer mirroring by SurfaceFlinger.
+     * For now, only used for mirroring started from MediaProjection.
+     */
+    @Nullable
+    public Point getDisplaySurfaceDefaultSize() {
+        return null;
+    }
+
+    /**
      * Gets the name of the display device.
      *
      * @return The display device name.
@@ -212,6 +243,19 @@
     }
 
     /**
+     * Sets the display flags while in a transaction.
+     *
+     * Valid display flags:
+     *  {@link SurfaceControl#DISPLAY_RECEIVES_INPUT}
+     */
+    public final void setDisplayFlagsLocked(SurfaceControl.Transaction t, int flags) {
+        if (mCurrentFlags != flags) {
+            mCurrentFlags = flags;
+            t.setDisplayFlags(mDisplayToken, flags);
+        }
+    }
+
+    /**
      * Sets the display projection while in a transaction.
      *
      * @param orientation defines the display's orientation
@@ -298,6 +342,7 @@
         pw.println("mUniqueId=" + mUniqueId);
         pw.println("mDisplayToken=" + mDisplayToken);
         pw.println("mCurrentLayerStack=" + mCurrentLayerStack);
+        pw.println("mCurrentFlags=" + mCurrentFlags);
         pw.println("mCurrentOrientation=" + mCurrentOrientation);
         pw.println("mCurrentLayerStackRect=" + mCurrentLayerStackRect);
         pw.println("mCurrentDisplayRect=" + mCurrentDisplayRect);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
index 57f4486..2b52350 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
@@ -123,6 +123,17 @@
         return null;
     }
 
+    // String uniqueId -> DisplayDevice object with that given uniqueId
+    public DisplayDevice getByUniqueIdLocked(@NonNull String uniqueId) {
+        for (int i = mDisplayDevices.size() - 1; i >= 0; i--) {
+            final DisplayDevice displayDevice = mDisplayDevices.get(i);
+            if (displayDevice.getUniqueId().equals(uniqueId)) {
+                return displayDevice;
+            }
+        }
+        return null;
+    }
+
     private void handleDisplayDeviceAdded(DisplayDevice device) {
         synchronized (mSyncRoot) {
             DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index c220043..f16ed41 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -53,6 +53,7 @@
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateManagerInternal;
 import android.hardware.display.AmbientBrightnessDayStats;
 import android.hardware.display.BrightnessChangeEvent;
 import android.hardware.display.BrightnessConfiguration;
@@ -63,8 +64,6 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayGroupListener;
 import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener;
-import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation;
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
 import android.hardware.display.DisplayViewport;
 import android.hardware.display.DisplayedContentSample;
 import android.hardware.display.DisplayedContentSamplingAttributes;
@@ -133,6 +132,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Consumer;
@@ -201,7 +201,7 @@
     private static final int MSG_DELIVER_DISPLAY_EVENT = 3;
     private static final int MSG_REQUEST_TRAVERSAL = 4;
     private static final int MSG_UPDATE_VIEWPORT = 5;
-    private static final int MSG_LOAD_BRIGHTNESS_CONFIGURATION = 6;
+    private static final int MSG_LOAD_BRIGHTNESS_CONFIGURATIONS = 6;
     private static final int MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE = 7;
     private static final int MSG_DELIVER_DISPLAY_GROUP_EVENT = 8;
 
@@ -212,6 +212,7 @@
     private WindowManagerInternal mWindowManagerInternal;
     private InputManagerInternal mInputManagerInternal;
     private IMediaProjectionManager mProjectionService;
+    private DeviceStateManagerInternal mDeviceStateManager;
     private int[] mUserDisabledHdrTypes = {};
     private boolean mAreUserDisabledHdrTypesAllowed = true;
 
@@ -527,16 +528,29 @@
         final int newUserId = to.getUserIdentifier();
         final int userSerial = getUserManager().getUserSerialNumber(newUserId);
         synchronized (mSyncRoot) {
-            final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(
-                    Display.DEFAULT_DISPLAY);
-            if (mCurrentUserId != newUserId) {
+            boolean userSwitching = mCurrentUserId != newUserId;
+            if (userSwitching) {
                 mCurrentUserId = newUserId;
-                BrightnessConfiguration config =
-                        mPersistentDataStore.getBrightnessConfiguration(userSerial);
-                displayPowerController.setBrightnessConfiguration(config);
-                handleSettingsChange();
             }
-            displayPowerController.onSwitchUser(newUserId);
+            mLogicalDisplayMapper.forEachLocked(logicalDisplay -> {
+                if (logicalDisplay.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) {
+                    return;
+                }
+                final DisplayPowerController dpc = mDisplayPowerControllers.get(
+                        logicalDisplay.getDisplayIdLocked());
+                if (dpc == null) {
+                    return;
+                }
+                if (userSwitching) {
+                    BrightnessConfiguration config =
+                            getBrightnessConfigForDisplayWithPdsFallbackLocked(
+                            logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(),
+                            userSerial);
+                    dpc.setBrightnessConfiguration(config);
+                }
+                dpc.onSwitchUser(newUserId);
+            });
+            handleSettingsChange();
         }
     }
 
@@ -546,10 +560,9 @@
             mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
             mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
 
-            DeviceStateManager deviceStateManager =
-                    mContext.getSystemService(DeviceStateManager.class);
-            deviceStateManager.registerCallback(new HandlerExecutor(mHandler),
-                    new DeviceStateListener());
+            mDeviceStateManager = LocalServices.getService(DeviceStateManagerInternal.class);
+            mContext.getSystemService(DeviceStateManager.class).registerCallback(
+                    new HandlerExecutor(mHandler), new DeviceStateListener());
 
             scheduleTraversalLocked(false);
         }
@@ -638,6 +651,9 @@
         synchronized (mSyncRoot) {
             final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
             if (display != null) {
+                // Do not let constrain be overwritten by override from WindowManager.
+                info.shouldConstrainMetricsForLauncher =
+                        display.getDisplayInfoLocked().shouldConstrainMetricsForLauncher;
                 if (display.setDisplayInfoOverrideFromWindowManagerLocked(info)) {
                     handleLogicalDisplayChangedLocked(display);
                     scheduleTraversalLocked(false);
@@ -1317,6 +1333,13 @@
         if (work != null) {
             mHandler.post(work);
         }
+        final int displayId = display.getDisplayIdLocked();
+        DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
+        if (dpc != null) {
+            dpc.onDisplayChanged();
+        }
+        mPersistentDataStore.saveIfNeeded();
+        mHandler.sendEmptyMessage(MSG_LOAD_BRIGHTNESS_CONFIGURATIONS);
         handleLogicalDisplayChangedLocked(display);
     }
 
@@ -1425,24 +1448,42 @@
         return mDisplayModeDirector.getModeSwitchingType();
     }
 
-    private void setBrightnessConfigurationForUserInternal(
-            @Nullable BrightnessConfiguration c, @UserIdInt int userId,
-            @Nullable String packageName) {
+    private void setBrightnessConfigurationForDisplayInternal(
+            @Nullable BrightnessConfiguration c, String uniqueId, @UserIdInt int userId,
+            String packageName) {
         validateBrightnessConfiguration(c);
         final int userSerial = getUserManager().getUserSerialNumber(userId);
         synchronized (mSyncRoot) {
             try {
-                mPersistentDataStore.setBrightnessConfigurationForUser(c, userSerial,
-                        packageName);
+                DisplayDevice displayDevice = mDisplayDeviceRepo.getByUniqueIdLocked(uniqueId);
+                if (displayDevice == null) {
+                    return;
+                }
+                mPersistentDataStore.setBrightnessConfigurationForDisplayLocked(c, displayDevice,
+                        userSerial, packageName);
             } finally {
                 mPersistentDataStore.saveIfNeeded();
             }
-            if (userId == mCurrentUserId) {
-                mDisplayPowerControllers.get(Display.DEFAULT_DISPLAY).setBrightnessConfiguration(c);
+            if (userId != mCurrentUserId) {
+                return;
+            }
+            DisplayPowerController dpc = getDpcFromUniqueIdLocked(uniqueId);
+            if (dpc != null) {
+                dpc.setBrightnessConfiguration(c);
             }
         }
     }
 
+    private DisplayPowerController getDpcFromUniqueIdLocked(String uniqueId) {
+        final DisplayDevice displayDevice = mDisplayDeviceRepo.getByUniqueIdLocked(uniqueId);
+        final LogicalDisplay logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayDevice);
+        if (logicalDisplay != null) {
+            final int displayId = logicalDisplay.getDisplayIdLocked();
+            return mDisplayPowerControllers.get(displayId);
+        }
+        return null;
+    }
+
     @VisibleForTesting
     void validateBrightnessConfiguration(BrightnessConfiguration config) {
         if (config == null) {
@@ -1465,13 +1506,22 @@
         return false;
     }
 
-    private void loadBrightnessConfiguration() {
+    private void loadBrightnessConfigurations() {
+        int userSerial = getUserManager().getUserSerialNumber(mContext.getUserId());
         synchronized (mSyncRoot) {
-            final int userSerial = getUserManager().getUserSerialNumber(mCurrentUserId);
-            BrightnessConfiguration config =
-                    mPersistentDataStore.getBrightnessConfiguration(userSerial);
-            mDisplayPowerControllers.get(Display.DEFAULT_DISPLAY).setBrightnessConfiguration(
-                    config);
+            mLogicalDisplayMapper.forEachLocked((logicalDisplay) -> {
+                final String uniqueId =
+                        logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+                final BrightnessConfiguration config =
+                        getBrightnessConfigForDisplayWithPdsFallbackLocked(uniqueId, userSerial);
+                if (config != null) {
+                    final DisplayPowerController dpc = mDisplayPowerControllers.get(
+                            logicalDisplay.getDisplayIdLocked());
+                    if (dpc != null) {
+                        dpc.setBrightnessConfiguration(config);
+                    }
+                }
+            });
         }
     }
 
@@ -1682,9 +1732,17 @@
         return SurfaceControl.getDisplayedContentSample(token, maxFrames, timestamp);
     }
 
-    void resetBrightnessConfiguration() {
-        setBrightnessConfigurationForUserInternal(null, mContext.getUserId(),
+    void resetBrightnessConfigurations() {
+        mPersistentDataStore.setBrightnessConfigurationForUser(null, mContext.getUserId(),
                 mContext.getPackageName());
+        mLogicalDisplayMapper.forEachLocked((logicalDisplay -> {
+            if (logicalDisplay.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) {
+                return;
+            }
+            final String uniqueId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+            setBrightnessConfigurationForDisplayInternal(null, uniqueId, mContext.getUserId(),
+                    mContext.getPackageName());
+        }));
     }
 
     void setAutoBrightnessLoggingEnabled(boolean enabled) {
@@ -1725,6 +1783,21 @@
         }
     }
 
+    void setShouldConstrainMetricsForLauncher(boolean constrain) {
+        // Apply constrain for every display.
+        synchronized (mSyncRoot) {
+            int[] displayIds = mLogicalDisplayMapper.getDisplayIdsLocked(Process.myUid());
+            for (int i : displayIds) {
+                final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(i);
+                if (display == null) {
+                    return;
+                }
+                display.getDisplayInfoLocked().shouldConstrainMetricsForLauncher = constrain;
+                setDisplayInfoOverrideFromWindowManagerInternal(i, display.getDisplayInfoLocked());
+            }
+        }
+    }
+
     private void clearViewportsLocked() {
         mViewports.clear();
     }
@@ -1751,10 +1824,13 @@
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
         final boolean ownContent = (info.flags & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY) != 0;
 
+        // Mirror the part of WM hierarchy that corresponds to the provided window token.
+        IBinder windowTokenClientToMirror = device.getWindowTokenClientToMirrorLocked();
+
         // Find the logical display that the display device is showing.
         // Certain displays only ever show their own content.
         LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
-        if (!ownContent) {
+        if (!ownContent && windowTokenClientToMirror == null) {
             if (display != null && !display.hasContentLocked()) {
                 // If the display does not have any content of its own, then
                 // automatically mirror the requested logical display contents if possible.
@@ -2115,6 +2191,18 @@
         return display == null ? null : display.getPrimaryDisplayDeviceLocked();
     }
 
+    private BrightnessConfiguration getBrightnessConfigForDisplayWithPdsFallbackLocked(
+            String uniqueId, int userSerial) {
+        BrightnessConfiguration config =
+                mPersistentDataStore.getBrightnessConfigurationForDisplayLocked(
+                        uniqueId, userSerial);
+        if (config == null) {
+            // Get from global configurations
+            config = mPersistentDataStore.getBrightnessConfiguration(userSerial);
+        }
+        return config;
+    }
+
     private final class DisplayManagerHandler extends Handler {
         public DisplayManagerHandler(Looper looper) {
             super(looper, null, true /*async*/);
@@ -2156,8 +2244,8 @@
                     break;
                 }
 
-                case MSG_LOAD_BRIGHTNESS_CONFIGURATION:
-                    loadBrightnessConfiguration();
+                case MSG_LOAD_BRIGHTNESS_CONFIGURATIONS:
+                    loadBrightnessConfigurations();
                     break;
 
                 case MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE:
@@ -2166,6 +2254,9 @@
                         int displayId = msg.arg1;
                         final LogicalDisplay display =
                                 mLogicalDisplayMapper.getDisplayLocked(displayId);
+                        if (display == null) {
+                            break;
+                        }
                         uids = display.getPendingFrameRateOverrideUids();
                         display.clearPendingFrameRateOverrideUids();
                     }
@@ -2772,6 +2863,19 @@
         @Override // Binder call
         public void setBrightnessConfigurationForUser(
                 BrightnessConfiguration c, @UserIdInt int userId, String packageName) {
+            mLogicalDisplayMapper.forEachLocked(logicalDisplay -> {
+                if (logicalDisplay.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) {
+                    return;
+                }
+                final DisplayDevice displayDevice = logicalDisplay.getPrimaryDisplayDeviceLocked();
+                setBrightnessConfigurationForDisplay(c, displayDevice.getUniqueId(), userId,
+                        packageName);
+            });
+        }
+
+        @Override // Binder call
+        public void setBrightnessConfigurationForDisplay(BrightnessConfiguration c,
+                String uniqueId, int userId, String packageName) {
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS,
                     "Permission required to change the display's brightness configuration");
@@ -2779,21 +2883,19 @@
                 mContext.enforceCallingOrSelfPermission(
                         Manifest.permission.INTERACT_ACROSS_USERS,
                         "Permission required to change the display brightness"
-                        + " configuration of another user");
-            }
-            if (packageName != null && !validatePackageName(getCallingUid(), packageName)) {
-                packageName = null;
+                                + " configuration of another user");
             }
             final long token = Binder.clearCallingIdentity();
             try {
-                setBrightnessConfigurationForUserInternal(c, userId, packageName);
+                setBrightnessConfigurationForDisplayInternal(c, uniqueId, userId, packageName);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
         }
 
         @Override // Binder call
-        public BrightnessConfiguration getBrightnessConfigurationForUser(int userId) {
+        public BrightnessConfiguration getBrightnessConfigurationForDisplay(String uniqueId,
+                int userId) {
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS,
                     "Permission required to read the display's brightness configuration");
@@ -2804,14 +2906,19 @@
                                 + " configuration of another user");
             }
             final long token = Binder.clearCallingIdentity();
+            final int userSerial = getUserManager().getUserSerialNumber(userId);
             try {
-                final int userSerial = getUserManager().getUserSerialNumber(userId);
                 synchronized (mSyncRoot) {
+                    // Get from per-display configurations
                     BrightnessConfiguration config =
-                            mPersistentDataStore.getBrightnessConfiguration(userSerial);
+                            getBrightnessConfigForDisplayWithPdsFallbackLocked(
+                                    uniqueId, userSerial);
                     if (config == null) {
-                        config = mDisplayPowerControllers.get(Display.DEFAULT_DISPLAY)
-                                .getDefaultBrightnessConfiguration();
+                        // Get default configuration
+                        DisplayPowerController dpc = getDpcFromUniqueIdLocked(uniqueId);
+                        if (dpc != null) {
+                            config = dpc.getDefaultBrightnessConfiguration();
+                        }
                     }
                     return config;
                 }
@@ -2820,6 +2927,21 @@
             }
         }
 
+
+
+        @Override // Binder call
+        public BrightnessConfiguration getBrightnessConfigurationForUser(int userId) {
+            final String uniqueId;
+            synchronized (mSyncRoot) {
+                DisplayDevice displayDevice = mLogicalDisplayMapper.getDisplayLocked(
+                        Display.DEFAULT_DISPLAY).getPrimaryDisplayDeviceLocked();
+                uniqueId = displayDevice.getUniqueId();
+            }
+            return getBrightnessConfigurationForDisplay(uniqueId, userId);
+
+
+        }
+
         @Override // Binder call
         public BrightnessConfiguration getDefaultBrightnessConfiguration() {
             mContext.enforceCallingOrSelfPermission(
@@ -3088,7 +3210,7 @@
                 initializeDisplayPowerControllersLocked();
             }
 
-            mHandler.sendEmptyMessage(MSG_LOAD_BRIGHTNESS_CONFIGURATION);
+            mHandler.sendEmptyMessage(MSG_LOAD_BRIGHTNESS_CONFIGURATIONS);
         }
 
         @Override
@@ -3154,6 +3276,53 @@
         }
 
         @Override
+        public Set<DisplayInfo> getPossibleDisplayInfo(int displayId) {
+            synchronized (mSyncRoot) {
+                // Retrieve the group associated with this display id.
+                final int displayGroupId =
+                        mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(displayId);
+                if (displayGroupId == Display.INVALID_DISPLAY_GROUP) {
+                    Slog.w(TAG,
+                            "Can't get possible display info since display group for " + displayId
+                                    + " does not exist");
+                    return new ArraySet<>();
+                }
+
+                // Assume any display in this group can be swapped out for the given display id.
+                Set<DisplayInfo> possibleInfo = new ArraySet<>();
+                final DisplayGroup group = mLogicalDisplayMapper.getDisplayGroupLocked(
+                        displayGroupId);
+                for (int i = 0; i < group.getSizeLocked(); i++) {
+                    final int id = group.getIdLocked(i);
+                    final LogicalDisplay logical = mLogicalDisplayMapper.getDisplayLocked(id);
+                    if (logical == null) {
+                        Slog.w(TAG,
+                                "Can't get possible display info since logical display for "
+                                        + "display id " + id + " does not exist, as part of group "
+                                        + displayGroupId);
+                    } else {
+                        possibleInfo.add(logical.getDisplayInfoLocked());
+                    }
+                }
+
+                // For the supported device states, retrieve the DisplayInfos for the logical
+                // display layout.
+                if (mDeviceStateManager == null) {
+                    Slog.w(TAG, "Can't get supported states since DeviceStateManager not ready");
+                } else {
+                    final int[] supportedStates =
+                            mDeviceStateManager.getSupportedStateIdentifiers();
+                    for (int state : supportedStates) {
+                        possibleInfo.addAll(
+                                mLogicalDisplayMapper.getDisplayInfoForStateLocked(state, displayId,
+                                        displayGroupId));
+                    }
+                }
+                return possibleInfo;
+            }
+        }
+
+        @Override
         public Point getDisplayPosition(int displayId) {
             synchronized (mSyncRoot) {
                 final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
@@ -3316,6 +3485,40 @@
             }
             return config.getRefreshRateLimitations();
         }
+
+        @Override
+        public IBinder getWindowTokenClientToMirror(int displayId) {
+            final DisplayDevice device;
+            synchronized (mSyncRoot) {
+                device = getDeviceForDisplayLocked(displayId);
+                if (device == null) {
+                    return null;
+                }
+            }
+            return device.getWindowTokenClientToMirrorLocked();
+        }
+
+        @Override
+        public void setWindowTokenClientToMirror(int displayId, IBinder windowToken) {
+            synchronized (mSyncRoot) {
+                final DisplayDevice device = getDeviceForDisplayLocked(displayId);
+                if (device != null) {
+                    device.setWindowTokenClientToMirrorLocked(windowToken);
+                }
+            }
+        }
+
+        @Override
+        public Point getDisplaySurfaceDefaultSize(int displayId) {
+            final DisplayDevice device;
+            synchronized (mSyncRoot) {
+                device = getDeviceForDisplayLocked(displayId);
+                if (device == null) {
+                    return null;
+                }
+            }
+            return device.getDisplaySurfaceDefaultSize();
+        }
     }
 
     class DesiredDisplayModeSpecsObserver
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index 48edb73..9412c93 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -58,6 +58,8 @@
                 return setDisplayModeDirectorLoggingEnabled(false);
             case "dwb-set-cct":
                 return setAmbientColorTemperatureOverride();
+            case "constrain-launcher-metrics":
+                return setConstrainLauncherMetrics();
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -88,6 +90,9 @@
         pw.println("    Disable display mode director logging.");
         pw.println("  dwb-set-cct CCT");
         pw.println("    Sets the ambient color temperature override to CCT (use -1 to disable).");
+        pw.println("  constrain-launcher-metrics [true|false]");
+        pw.println("    Sets if Display#getRealSize and getRealMetrics should be constrained for ");
+        pw.println("    Launcher.");
         pw.println();
         Intent.printIntentArgsHelp(pw , "");
     }
@@ -115,7 +120,7 @@
     }
 
     private int resetBrightnessConfiguration() {
-        mService.resetBrightnessConfiguration();
+        mService.resetBrightnessConfigurations();
         return 0;
     }
 
@@ -150,4 +155,15 @@
         mService.setAmbientColorTemperatureOverride(cct);
         return 0;
     }
+
+    private int setConstrainLauncherMetrics() {
+        String constrainText = getNextArg();
+        if (constrainText == null) {
+            getErrPrintWriter().println("Error: no value specified");
+            return 1;
+        }
+        boolean constrain = Boolean.parseBoolean(constrainText);
+        mService.setShouldConstrainMetricsForLauncher(constrain);
+        return 0;
+    }
 }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 1224902..63d32c8 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -739,13 +739,13 @@
         final IBinder token = device.getDisplayTokenLocked();
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
         mHandler.post(() -> {
-            if (mDisplayDevice == device) {
-                return;
+            if (mDisplayDevice != device) {
+                mDisplayDevice = device;
+                mUniqueDisplayId = uniqueId;
+                mDisplayDeviceConfig = config;
+                loadFromDisplayDeviceConfig(token, info);
+                updatePowerState();
             }
-            mDisplayDevice = device;
-            mUniqueDisplayId = uniqueId;
-            mDisplayDeviceConfig = config;
-            loadFromDisplayDeviceConfig(token, info);
         });
     }
 
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index f953cc8..5a9efd7 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -601,14 +601,6 @@
                             && SystemProperties.getBoolean(PROPERTY_EMULATOR_CIRCULAR, false))) {
                         mInfo.flags |= DisplayDeviceInfo.FLAG_ROUND;
                     }
-                    if (res.getBoolean(
-                            com.android.internal.R.bool.config_maskMainBuiltInDisplayCutout)) {
-                        mInfo.flags |= DisplayDeviceInfo.FLAG_MASK_DISPLAY_CUTOUT;
-                    }
-                    mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res,
-                            mInfo.width, mInfo.height);
-                    mInfo.roundedCorners = RoundedCorners.fromResources(
-                            res, mInfo.width, mInfo.height);
                 } else {
                     if (!res.getBoolean(
                                 com.android.internal.R.bool.config_localDisplaysMirrorContent)) {
@@ -620,6 +612,15 @@
                     }
                 }
 
+                if (DisplayCutout.getMaskBuiltInDisplayCutout(res, mInfo.uniqueId)) {
+                    mInfo.flags |= DisplayDeviceInfo.FLAG_MASK_DISPLAY_CUTOUT;
+                }
+                mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res,
+                        mInfo.uniqueId, mInfo.width, mInfo.height);
+
+                mInfo.roundedCorners = RoundedCorners.fromResources(
+                        res, mInfo.uniqueId, mInfo.width, mInfo.height);
+
                 if (mStaticDisplayInfo.isInternal) {
                     mInfo.type = Display.TYPE_INTERNAL;
                     mInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL;
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 9acb4c8..86c9ca9 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display;
 
+import static com.android.server.display.DisplayDeviceInfo.TOUCH_NONE;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -231,6 +233,8 @@
                 info.physicalXDpi = mOverrideDisplayInfo.physicalXDpi;
                 info.physicalYDpi = mOverrideDisplayInfo.physicalYDpi;
                 info.roundedCorners = mOverrideDisplayInfo.roundedCorners;
+                info.shouldConstrainMetricsForLauncher =
+                        mOverrideDisplayInfo.shouldConstrainMetricsForLauncher;
             }
             mInfo.set(info);
         }
@@ -512,6 +516,11 @@
             boolean isBlanked) {
         // Set the layer stack.
         device.setLayerStackLocked(t, isBlanked ? BLANK_LAYER_STACK : mLayerStack);
+        // Also inform whether the device is the same one sent to inputflinger for its layerstack.
+        // TODO(b/188914255): Remove once input can dispatch against device vs layerstack.
+        device.setDisplayFlagsLocked(t,
+                device.getDisplayDeviceInfoLocked().touch != TOUCH_NONE
+                        ? SurfaceControl.DISPLAY_RECEIVES_INPUT : 0);
 
         // Set the color mode and allowed display mode.
         if (device == mPrimaryDisplayDevice) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 4c9a2d7..0fbc3e8 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -22,8 +22,11 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PowerManager;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -38,6 +41,7 @@
 
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -67,7 +71,7 @@
     public static final int DISPLAY_GROUP_EVENT_CHANGED = 2;
     public static final int DISPLAY_GROUP_EVENT_REMOVED = 3;
 
-    private static final int TIMEOUT_STATE_TRANSITION_MILLIS = 500;
+    private static final int TIMEOUT_STATE_TRANSITION_MILLIS = 300;
 
     private static final int MSG_TRANSITION_TO_PENDING_DEVICE_STATE = 1;
 
@@ -96,6 +100,11 @@
     private final boolean mSupportsConcurrentInternalDisplays;
 
     /**
+     * Wake the device when transitioning into this device state.
+     */
+    private final int mDeviceStateOnWhichToWakeUp;
+
+    /**
      * Map of all logical displays indexed by logical display id.
      * Any modification to mLogicalDisplays must invalidate the DisplayManagerGlobal cache.
      * TODO: multi-display - Move the aforementioned comment?
@@ -111,6 +120,7 @@
     private final Listener mListener;
     private final DisplayManagerService.SyncRoot mSyncRoot;
     private final LogicalDisplayMapperHandler mHandler;
+    private final PowerManager mPowerManager;
 
     /**
      * Has an entry for every logical display that the rest of the system has been notified about.
@@ -148,12 +158,15 @@
             @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
             @NonNull Handler handler) {
         mSyncRoot = syncRoot;
+        mPowerManager = context.getSystemService(PowerManager.class);
         mHandler = new LogicalDisplayMapperHandler(handler.getLooper());
         mDisplayDeviceRepo = repo;
         mListener = listener;
         mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false);
         mSupportsConcurrentInternalDisplays = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_supportsConcurrentInternalDisplays);
+        mDeviceStateOnWhichToWakeUp = context.getResources().getInteger(
+                com.android.internal.R.integer.config_deviceStateOnWhichToWakeUp);
         mDisplayDeviceRepo.addListener(this);
         mDeviceStateToLayoutMap = new DeviceStateToLayoutMap();
     }
@@ -195,6 +208,9 @@
     }
 
     public LogicalDisplay getDisplayLocked(DisplayDevice device) {
+        if (device == null) {
+            return null;
+        }
         final int count = mLogicalDisplays.size();
         for (int i = 0; i < count; i++) {
             final LogicalDisplay display = mLogicalDisplays.valueAt(i);
@@ -251,6 +267,61 @@
         return mDisplayGroups.get(groupId);
     }
 
+    /**
+     * Returns the set of {@link DisplayInfo} for this device state, only fetching the info that is
+     * part of the same display group as the provided display id. The DisplayInfo represent the
+     * logical display layouts possible for the given device state.
+     *
+     * @param deviceState the state to query possible layouts for
+     * @param displayId   the display id to apply to all displays within the group
+     * @param groupId     the display group to filter display info for. Must be the same group as
+     *                    the display with the provided display id.
+     */
+    public Set<DisplayInfo> getDisplayInfoForStateLocked(int deviceState, int displayId,
+            int groupId) {
+        Set<DisplayInfo> displayInfos = new ArraySet<>();
+        final Layout layout = mDeviceStateToLayoutMap.get(deviceState);
+        final int layoutSize = layout.size();
+        for (int i = 0; i < layoutSize; i++) {
+            Layout.Display displayLayout = layout.getAt(i);
+            if (displayLayout == null) {
+                continue;
+            }
+
+            // If the underlying display-device we want to use for this display
+            // doesn't exist, then skip it. This can happen at startup as display-devices
+            // trickle in one at a time. When the new display finally shows up, the layout is
+            // recalculated so that the display is properly added to the current layout.
+            final DisplayAddress address = displayLayout.getAddress();
+            final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(address);
+            if (device == null) {
+                Slog.w(TAG, "The display device (" + address + "), is not available"
+                        + " for the display state " + deviceState);
+                continue;
+            }
+
+            // Find or create the LogicalDisplay to map the DisplayDevice to.
+            final int logicalDisplayId = displayLayout.getLogicalDisplayId();
+            final LogicalDisplay logicalDisplay = getDisplayLocked(logicalDisplayId);
+            if (logicalDisplay == null) {
+                Slog.w(TAG, "The logical display (" + address + "), is not available"
+                        + " for the display state " + deviceState);
+                continue;
+            }
+            final DisplayInfo temp = logicalDisplay.getDisplayInfoLocked();
+            DisplayInfo displayInfo = new DisplayInfo(temp);
+            if (displayInfo.displayGroupId != groupId) {
+                // Ignore any displays not in the provided group.
+                continue;
+            }
+            // A display in the same group can be swapped out at any point, so set the display id
+            // for all results to the provided display id.
+            displayInfo.displayId = displayId;
+            displayInfos.add(displayInfo);
+        }
+        return displayInfos;
+    }
+
     public void dumpLocked(PrintWriter pw) {
         pw.println("LogicalDisplayMapper:");
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
@@ -258,6 +329,7 @@
 
         ipw.println("mSingleDisplayDemoMode=" + mSingleDisplayDemoMode);
         ipw.println("mCurrentLayout=" + mCurrentLayout);
+        ipw.println("mDeviceStateOnWhichToWakeUp=" + mDeviceStateOnWhichToWakeUp);
 
         final int logicalDisplayCount = mLogicalDisplays.size();
         ipw.println();
@@ -275,7 +347,9 @@
     }
 
     void setDeviceStateLocked(int state) {
-        Slog.i(TAG, "Requesting Transition to state: " + state);
+        final boolean isInteractive  = mPowerManager.isInteractive();
+        Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState
+                + ", interactive=" + isInteractive);
         // As part of a state transition, we may need to turn off some displays temporarily so that
         // the transition is smooth. Plus, on some devices, only one internal displays can be
         // on at a time. We use DISPLAY_PHASE_LAYOUT_TRANSITION to mark a display that needs to be
@@ -284,8 +358,13 @@
             resetLayoutLocked(mDeviceState, state, LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION);
         }
         mPendingDeviceState = state;
-        if (areAllTransitioningDisplaysOffLocked()) {
-            // Nothing to wait on, we're good to go
+        final boolean wakeDevice = mPendingDeviceState == mDeviceStateOnWhichToWakeUp
+                && !isInteractive;
+
+        // If all displays are off already, we can just transition here, unless the device is asleep
+        // and we plan on waking it up. In that case, fall through to the call to wakeUp, and defer
+        // the final transition until later once the device is awake.
+        if (areAllTransitioningDisplaysOffLocked() && !wakeDevice) {
             transitionToPendingStateLocked();
             return;
         }
@@ -296,6 +375,14 @@
         // Send the transitioning phase updates to DisplayManager so that the displays can
         // start turning OFF in preparation for the new layout.
         updateLogicalDisplaysLocked();
+
+        if (wakeDevice) {
+            // We already told the displays to turn off, now we need to wake the device as
+            // we transition to this new state. We do it here so that the waking happens between the
+            // transition from one layout to another.
+            mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_UNFOLD_DEVICE,
+                    "server.display:unfold");
+        }
         mHandler.sendEmptyMessageDelayed(MSG_TRANSITION_TO_PENDING_DEVICE_STATE,
                 TIMEOUT_STATE_TRANSITION_MILLIS);
     }
@@ -422,6 +509,7 @@
                 assignDisplayGroupLocked(display);
                 mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_CHANGED);
 
+            // The display is involved in a display layout transition
             } else if (updateState == UPDATE_STATE_TRANSITION) {
                 mLogicalDisplaysToUpdate.put(displayId,
                         LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION);
@@ -612,14 +700,14 @@
 
             // We consider a display-device as changing/transition if
             // 1) It's already marked as transitioning
-            // 2) It's going from enabled to disabled
+            // 2) It's going from enabled to disabled, or vice versa
             // 3) It's enabled, but it's mapped to a new logical display ID. To the user this
             //    would look like apps moving from one screen to another since task-stacks stay
             //    with the logical display [ID].
             final boolean isTransitioning =
                     (logicalDisplay.getPhase() == LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION)
-                    || (wasEnabled && !willBeEnabled)
-                    || (wasEnabled && deviceHasNewLogicalDisplayId);
+                    || (wasEnabled != willBeEnabled)
+                    || deviceHasNewLogicalDisplayId;
 
             if (isTransitioning) {
                 setDisplayPhase(logicalDisplay, phase);
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index c90ddf4..4b0d43b 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -63,6 +63,15 @@
  *      &lt;display unique-id="XXXXXXX">
  *          &lt;color-mode>0&lt;/color-mode>
  *          &lt;brightness-value>0&lt;/brightness-value>
+ *          &lt;brightness-configurations>
+ *              &lt;brightness-configuration user-serial="0" package-name="com.example"
+ *              timestamp="1234">
+ *                  &lt;brightness-curve description="some text">
+ *                      &lt;brightness-point lux="0" nits="13.25"/>
+ *                      &lt;brightness-point lux="20" nits="35.94"/>
+ *                  &lt;/brightness-curve>
+ *              &lt;/brightness-configuration>
+ *          &lt;/brightness-configurations>
  *      &lt;/display>
  *  &lt;/display-states>
  *  &lt;stable-device-values>
@@ -120,7 +129,8 @@
     private final StableDeviceValues mStableDeviceValues = new StableDeviceValues();
 
     // Brightness configuration by user
-    private BrightnessConfigurations mBrightnessConfigurations = new BrightnessConfigurations();
+    private BrightnessConfigurations mGlobalBrightnessConfigurations =
+            new BrightnessConfigurations();
 
     // True if the data has been loaded.
     private boolean mLoaded;
@@ -293,18 +303,44 @@
         }
     }
 
+    // Used for testing & reset
     public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userSerial,
             @Nullable String packageName) {
         loadIfNeeded();
-        if (mBrightnessConfigurations.setBrightnessConfigurationForUser(c, userSerial,
+        if (mGlobalBrightnessConfigurations.setBrightnessConfigurationForUser(c, userSerial,
                 packageName)) {
+
             setDirty();
         }
     }
 
+    public boolean setBrightnessConfigurationForDisplayLocked(BrightnessConfiguration configuration,
+            DisplayDevice device, int userSerial, String packageName) {
+        if (device == null || !device.hasStableUniqueId()) {
+            return false;
+        }
+        DisplayState state = getDisplayState(device.getUniqueId(), /*createIfAbsent*/ true);
+        if (state.setBrightnessConfiguration(configuration, userSerial, packageName)) {
+            setDirty();
+            return true;
+        }
+        return false;
+    }
+
+
+    public BrightnessConfiguration getBrightnessConfigurationForDisplayLocked(
+            String uniqueDisplayId, int userSerial) {
+        loadIfNeeded();
+        DisplayState state = mDisplayStates.get(uniqueDisplayId);
+        if (state != null) {
+            return state.getBrightnessConfiguration(userSerial);
+        }
+        return null;
+    }
+
     public BrightnessConfiguration getBrightnessConfiguration(int userSerial) {
         loadIfNeeded();
-        return mBrightnessConfigurations.getBrightnessConfiguration(userSerial);
+        return mGlobalBrightnessConfigurations.getBrightnessConfiguration(userSerial);
     }
 
     private DisplayState getDisplayState(String uniqueId, boolean createIfAbsent) {
@@ -391,7 +427,7 @@
                 mStableDeviceValues.loadFromXml(parser);
             }
             if (parser.getName().equals(TAG_BRIGHTNESS_CONFIGURATIONS)) {
-                mBrightnessConfigurations.loadFromXml(parser);
+                mGlobalBrightnessConfigurations.loadFromXml(parser);
             }
         }
     }
@@ -470,7 +506,7 @@
         mStableDeviceValues.saveToXml(serializer);
         serializer.endTag(null, TAG_STABLE_DEVICE_VALUES);
         serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
-        mBrightnessConfigurations.saveToXml(serializer);
+        mGlobalBrightnessConfigurations.saveToXml(serializer);
         serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
         serializer.endTag(null, TAG_DISPLAY_MANAGER_STATE);
         serializer.endDocument();
@@ -493,14 +529,18 @@
         }
         pw.println("  StableDeviceValues:");
         mStableDeviceValues.dump(pw, "      ");
-        pw.println("  BrightnessConfigurations:");
-        mBrightnessConfigurations.dump(pw, "      ");
+        pw.println("  GlobalBrightnessConfigurations:");
+        mGlobalBrightnessConfigurations.dump(pw, "      ");
     }
 
     private static final class DisplayState {
         private int mColorMode;
         private float mBrightness;
 
+        // Brightness configuration by user
+        private BrightnessConfigurations mDisplayBrightnessConfigurations =
+                new BrightnessConfigurations();
+
         public boolean setColorMode(int colorMode) {
             if (colorMode == mColorMode) {
                 return false;
@@ -525,6 +565,16 @@
             return mBrightness;
         }
 
+        public boolean setBrightnessConfiguration(BrightnessConfiguration configuration,
+                int userSerial, String packageName) {
+            mDisplayBrightnessConfigurations.setBrightnessConfigurationForUser(
+                    configuration, userSerial, packageName);
+            return true;
+        }
+
+        public BrightnessConfiguration getBrightnessConfiguration(int userSerial) {
+            return mDisplayBrightnessConfigurations.mConfigurations.get(userSerial);
+        }
 
         public void loadFromXml(TypedXmlPullParser parser)
                 throws IOException, XmlPullParserException {
@@ -540,6 +590,9 @@
                         String brightness = parser.nextText();
                         mBrightness = Float.parseFloat(brightness);
                         break;
+                    case TAG_BRIGHTNESS_CONFIGURATIONS:
+                        mDisplayBrightnessConfigurations.loadFromXml(parser);
+                        break;
                 }
             }
         }
@@ -548,15 +601,21 @@
             serializer.startTag(null, TAG_COLOR_MODE);
             serializer.text(Integer.toString(mColorMode));
             serializer.endTag(null, TAG_COLOR_MODE);
+
             serializer.startTag(null, TAG_BRIGHTNESS_VALUE);
             serializer.text(Float.toString(mBrightness));
             serializer.endTag(null, TAG_BRIGHTNESS_VALUE);
 
+            serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
+            mDisplayBrightnessConfigurations.saveToXml(serializer);
+            serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
         }
 
         public void dump(final PrintWriter pw, final String prefix) {
             pw.println(prefix + "ColorMode=" + mColorMode);
             pw.println(prefix + "BrightnessValue=" + mBrightness);
+            pw.println(prefix + "DisplayBrightnessConfigurations: ");
+            mDisplayBrightnessConfigurations.dump(pw, prefix);
         }
     }
 
@@ -621,11 +680,11 @@
 
     private static final class BrightnessConfigurations {
         // Maps from a user ID to the users' given brightness configuration
-        private SparseArray<BrightnessConfiguration> mConfigurations;
+        private final SparseArray<BrightnessConfiguration> mConfigurations;
         // Timestamp of time the configuration was set.
-        private SparseLongArray mTimeStamps;
+        private final SparseLongArray mTimeStamps;
         // Package that set the configuration.
-        private SparseArray<String> mPackageNames;
+        private final SparseArray<String> mPackageNames;
 
         public BrightnessConfigurations() {
             mConfigurations = new SparseArray<>();
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index b7931c8..a592192 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -31,7 +31,9 @@
 import static com.android.server.display.DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_TRUSTED;
 
+import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Point;
 import android.hardware.display.IVirtualDisplayCallback;
 import android.hardware.display.VirtualDisplayConfig;
 import android.media.projection.IMediaProjection;
@@ -231,6 +233,7 @@
         private Display.Mode mMode;
         private boolean mIsDisplayOn;
         private int mDisplayIdToMirror;
+        private IBinder mWindowTokenClientToMirror;
 
         public VirtualDisplayDevice(IBinder displayToken, IBinder appToken,
                 int ownerUid, String ownerPackageName, Surface surface, int flags,
@@ -253,6 +256,7 @@
             mUniqueIndex = uniqueIndex;
             mIsDisplayOn = surface != null;
             mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror();
+            mWindowTokenClientToMirror = virtualDisplayConfig.getWindowTokenClientToMirror();
         }
 
         @Override
@@ -282,6 +286,29 @@
             return mDisplayIdToMirror;
         }
 
+        @Override
+        @Nullable
+        public IBinder getWindowTokenClientToMirrorLocked() {
+            return mWindowTokenClientToMirror;
+        }
+
+        @Override
+        public void setWindowTokenClientToMirrorLocked(IBinder windowToken) {
+            if (mWindowTokenClientToMirror != windowToken) {
+                mWindowTokenClientToMirror = windowToken;
+                sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED);
+                sendTraversalRequestLocked();
+            }
+        }
+
+        @Override
+        public Point getDisplaySurfaceDefaultSize() {
+            if (mSurface == null) {
+                return null;
+            }
+            return mSurface.getDefaultSize();
+        }
+
         @VisibleForTesting
         Surface getSurfaceLocked() {
             return mSurface;
@@ -362,6 +389,7 @@
             pw.println("mDisplayState=" + Display.stateToString(mDisplayState));
             pw.println("mStopped=" + mStopped);
             pw.println("mDisplayIdToMirror=" + mDisplayIdToMirror);
+            pw.println("mWindowTokenClientToMirror=" + mWindowTokenClientToMirror);
         }
 
 
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 9755bdf..0bf110a 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -189,7 +189,7 @@
     private final InputManagerHandler mHandler;
 
     // Context cache used for loading pointer resources.
-    private Context mDisplayContext;
+    private Context mPointerIconDisplayContext;
 
     private final File mDoubleTouchGestureEnableFile;
 
@@ -839,21 +839,31 @@
             throw new IllegalArgumentException("mode is invalid");
         }
         if (ENABLE_PER_WINDOW_INPUT_ROTATION) {
-            if (event instanceof MotionEvent) {
-                final Context dispCtx = getContextForDisplay(event.getDisplayId());
-                final Display display = dispCtx.getDisplay();
+            // Motion events that are pointer events or relative mouse events will need to have the
+            // inverse display rotation applied to them.
+            if (event instanceof MotionEvent
+                    && (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
+                    || event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE))) {
+                Context displayContext = getContextForDisplay(event.getDisplayId());
+                if (displayContext == null) {
+                    displayContext = Objects.requireNonNull(
+                            getContextForDisplay(Display.DEFAULT_DISPLAY));
+                }
+                final Display display = displayContext.getDisplay();
                 final int rotation = display.getRotation();
                 if (rotation != ROTATION_0) {
                     final MotionEvent motion = (MotionEvent) event;
                     // Injections are currently expected to be in the space of the injector (ie.
-                    // usually assumed to be post-rotated). Thus we need to unrotate into raw
+                    // usually assumed to be post-rotated). Thus we need to un-rotate into raw
                     // input coordinates for dispatch.
                     final Point sz = new Point();
-                    display.getRealSize(sz);
-                    if ((rotation % 2) != 0) {
-                        final int tmpX = sz.x;
-                        sz.x = sz.y;
-                        sz.y = tmpX;
+                    if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+                        display.getRealSize(sz);
+                        if ((rotation % 2) != 0) {
+                            final int tmpX = sz.x;
+                            sz.x = sz.y;
+                            sz.y = tmpX;
+                        }
                     }
                     motion.applyTransform(MotionEvent.createRotateMatrix(
                             (4 - rotation), sz.x, sz.y));
@@ -890,6 +900,7 @@
 
     @Override // Binder call
     public VerifiedInputEvent verifyInputEvent(InputEvent event) {
+        Objects.requireNonNull(event, "event must not be null");
         return nativeVerifyInputEvent(mPtr, event);
     }
 
@@ -1742,6 +1753,11 @@
 
     /** Clean up input window handles of the given display. */
     public void onDisplayRemoved(int displayId) {
+        if (mPointerIconDisplayContext != null
+                && mPointerIconDisplayContext.getDisplay().getDisplayId() == displayId) {
+            mPointerIconDisplayContext = null;
+        }
+
         nativeDisplayRemoved(mPtr, displayId);
     }
 
@@ -2969,24 +2985,43 @@
 
     // Native callback.
     private PointerIcon getPointerIcon(int displayId) {
-        return PointerIcon.getDefaultIcon(getContextForDisplay(displayId));
+        return PointerIcon.getDefaultIcon(getContextForPointerIcon(displayId));
     }
 
-    private Context getContextForDisplay(int displayId) {
-        if (mDisplayContext != null && mDisplayContext.getDisplay().getDisplayId() == displayId) {
-            return mDisplayContext;
-        }
-
-        if (mContext.getDisplay().getDisplayId() == displayId) {
-            mDisplayContext = mContext;
-            return mDisplayContext;
+    @NonNull
+    private Context getContextForPointerIcon(int displayId) {
+        if (mPointerIconDisplayContext != null
+                && mPointerIconDisplayContext.getDisplay().getDisplayId() == displayId) {
+            return mPointerIconDisplayContext;
         }
 
         // Create and cache context for non-default display.
-        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        mPointerIconDisplayContext = getContextForDisplay(displayId);
+
+        // Fall back to default display if the requested displayId does not exist.
+        if (mPointerIconDisplayContext == null) {
+            mPointerIconDisplayContext = getContextForDisplay(Display.DEFAULT_DISPLAY);
+        }
+        return mPointerIconDisplayContext;
+    }
+
+    @Nullable
+    private Context getContextForDisplay(int displayId) {
+        if (displayId == Display.INVALID_DISPLAY) {
+            return null;
+        }
+        if (mContext.getDisplay().getDisplayId() == displayId) {
+            return mContext;
+        }
+
+        final DisplayManager displayManager = Objects.requireNonNull(
+                mContext.getSystemService(DisplayManager.class));
         final Display display = displayManager.getDisplay(displayId);
-        mDisplayContext = mContext.createDisplayContext(display);
-        return mDisplayContext;
+        if (display == null) {
+            return null;
+        }
+
+        return mContext.createDisplayContext(display);
     }
 
     // Native callback.
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 3e52f5e..0e82c2a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2528,9 +2528,8 @@
                 }
                 if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
                 // Dispatch display id for InputMethodService to update context display.
-                executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOOO(
-                        MSG_INITIALIZE_IME, mCurTokenDisplayId, mCurMethod, mCurToken,
-                        mMethodMap.get(mCurMethodId).getConfigChanges()));
+                executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(MSG_INITIALIZE_IME,
+                        mMethodMap.get(mCurMethodId).getConfigChanges(), mCurMethod, mCurToken));
                 scheduleNotifyImeUidToAudioService(mCurMethodUid);
                 if (mCurClient != null) {
                     clearClientSessionLocked(mCurClient);
@@ -4121,6 +4120,18 @@
         }
     }
 
+    /** Called right after {@link IInputMethod#showSoftInput}. */
+    private void onShowHideSoftInputRequested(boolean show, IBinder requestToken,
+            @SoftInputShowHideReason int reason) {
+        final WindowManagerInternal.ImeTargetInfo info =
+                mWindowManagerInternal.onToggleImeRequested(
+                        show, mCurFocusedWindow, requestToken, mCurTokenDisplayId);
+        mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry(
+                mCurFocusedWindowClient, mCurAttribute, info.focusedWindowName,
+                mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode,
+                info.requestWindowName, info.imeControlTargetName, info.imeLayerTargetName));
+    }
+
     @BinderThread
     private void hideMySoftInput(@NonNull IBinder token, int flags) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput");
@@ -4239,18 +4250,11 @@
                     if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".showSoftInput("
                             + args.arg3 + ", " + msg.arg1 + ", " + args.arg2 + ") for reason: "
                             + InputMethodDebug.softInputDisplayReasonToString(reason));
+                    final IBinder token = (IBinder) args.arg3;
                     ((IInputMethod) args.arg1).showSoftInput(
-                            (IBinder) args.arg3, msg.arg1, (ResultReceiver) args.arg2);
-                    mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry(
-                            mCurFocusedWindowClient, mCurAttribute,
-                            mWindowManagerInternal.getWindowName(mCurFocusedWindow),
-                            mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode,
-                            mWindowManagerInternal.getWindowName(
-                                    mShowRequestWindowMap.get(args.arg3)),
-                            mWindowManagerInternal.getImeControlTargetNameForLogging(
-                                    mCurTokenDisplayId),
-                            mWindowManagerInternal.getImeTargetNameForLogging(
-                                    mCurTokenDisplayId)));
+                            token, msg.arg1 /* flags */, (ResultReceiver) args.arg2);
+                    final IBinder requestToken = mShowRequestWindowMap.get(token);
+                    onShowHideSoftInputRequested(true /* show */, requestToken, reason);
                 } catch (RemoteException e) {
                 }
                 args.recycle();
@@ -4262,18 +4266,11 @@
                     if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".hideSoftInput(0, "
                             + args.arg3 + ", " + args.arg2 + ") for reason: "
                             + InputMethodDebug.softInputDisplayReasonToString(reason));
+                    final IBinder token = (IBinder) args.arg3;
                     ((IInputMethod)args.arg1).hideSoftInput(
-                            (IBinder) args.arg3, 0, (ResultReceiver)args.arg2);
-                    mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry(
-                            mCurFocusedWindowClient, mCurAttribute,
-                            mWindowManagerInternal.getWindowName(mCurFocusedWindow),
-                            mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode,
-                            mWindowManagerInternal.getWindowName(
-                                    mHideRequestWindowMap.get(args.arg3)),
-                            mWindowManagerInternal.getImeControlTargetNameForLogging(
-                                    mCurTokenDisplayId),
-                            mWindowManagerInternal.getImeTargetNameForLogging(
-                                    mCurTokenDisplayId)));
+                            token, 0 /* flags */, (ResultReceiver) args.arg2);
+                    final IBinder requestToken = mHideRequestWindowMap.get(token);
+                    onShowHideSoftInputRequested(false /* show */, requestToken, reason);
                 } catch (RemoteException e) {
                 }
                 args.recycle();
@@ -4290,12 +4287,11 @@
                 try {
                     if (DEBUG) {
                         Slog.v(TAG, "Sending attach of token: " + args.arg2 + " for display: "
-                                + msg.arg1);
+                                + mCurTokenDisplayId);
                     }
                     final IBinder token = (IBinder) args.arg2;
-                    ((IInputMethod) args.arg1).initializeInternal(token, msg.arg1,
-                            new InputMethodPrivilegedOperationsImpl(this, token),
-                            (int) args.arg3);
+                    ((IInputMethod) args.arg1).initializeInternal(token,
+                            new InputMethodPrivilegedOperationsImpl(this, token), msg.arg1);
                 } catch (RemoteException e) {
                 }
                 args.recycle();
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 7502afc..9be618c 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -922,8 +922,26 @@
             if (intent.getAction().equals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
                 BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                 synchronized (mLock) {
+                    boolean wasA2dpOn = mGlobalBluetoothA2dpOn;
                     mActiveBluetoothDevice = btDevice;
                     mGlobalBluetoothA2dpOn = btDevice != null;
+                    if (wasA2dpOn != mGlobalBluetoothA2dpOn) {
+                        Slog.d(TAG, "GlobalBluetoothA2dpOn is changed to '"
+                                + mGlobalBluetoothA2dpOn + "'");
+                        UserRecord userRecord = mUserRecords.get(mCurrentUserId);
+                        if (userRecord != null) {
+                            for (ClientRecord cr : userRecord.mClientRecords) {
+                                // mSelectedRouteId will be null for BT and phone speaker.
+                                if (cr.mSelectedRouteId == null) {
+                                    try {
+                                        cr.mClient.onGlobalA2dpChanged(mGlobalBluetoothA2dpOn);
+                                    } catch (RemoteException e) {
+                                        // Ignore exception
+                                    }
+                                }
+                            }
+                        }
+                    }
                 }
             }
         }
diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
index 2519bbf..8e7c4ff 100644
--- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
+++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.media.MediaMetrics;
 import android.media.metrics.IMediaMetricsManager;
 import android.media.metrics.NetworkEvent;
 import android.media.metrics.PlaybackErrorEvent;
@@ -65,6 +66,8 @@
     private static final int LOGGING_LEVEL_NO_UID = 1000;
     private static final int LOGGING_LEVEL_BLOCKED = 99999;
 
+    private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER;
+
     private static final String FAILED_TO_GET = "failed_to_get";
     private final SecureRandom mSecureRandom;
     @GuardedBy("mLock")
@@ -199,6 +202,12 @@
             mSecureRandom.nextBytes(byteId);
             String id = Base64.encodeToString(
                     byteId, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE);
+
+            // Authorize these session ids in the native mediametrics service.
+            new MediaMetrics.Item(mMetricsId)
+                    .set(MediaMetrics.Property.EVENT, "create")
+                    .set(MediaMetrics.Property.LOG_SESSION_ID, id)
+                    .record();
             return id;
         }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 7ba0f04..2f550c6 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -7076,7 +7076,6 @@
                         r.isUpdate = true;
                         final boolean isInterruptive = isVisuallyInterruptive(old, r);
                         r.setTextChanged(isInterruptive);
-                        r.setInterruptive(isInterruptive);
                     }
 
                     mNotificationsByKey.put(n.getKey(), r);
@@ -9424,7 +9423,7 @@
                     record.getSystemGeneratedSmartActions(),
                     record.getSmartReplies(),
                     record.canBubble(),
-                    record.isInterruptive(),
+                    record.isTextChanged(),
                     record.isConversation(),
                     record.getShortcutInfo(),
                     record.getRankingScore() == 0
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index b4ca511..b6b54fc 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -1143,6 +1143,10 @@
         return mIsInterruptive;
     }
 
+    public boolean isTextChanged() {
+        return mTextChanged;
+    }
+
     /** Returns the time the notification audibly alerted the user. */
     public long getLastAudiblyAlertedMs() {
         return mLastAudiblyAlertedMs;
diff --git a/services/core/java/com/android/server/notification/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java
index f47aa48..0a69aec 100644
--- a/services/core/java/com/android/server/notification/VibratorHelper.java
+++ b/services/core/java/com/android/server/notification/VibratorHelper.java
@@ -39,6 +39,9 @@
 
     private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
     private static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps
+    private static final int CHIRP_LEVEL_DURATION_MILLIS = 100;
+    private static final int DEFAULT_CHIRP_RAMP_DURATION_MILLIS = 100;
+    private static final int FALLBACK_CHIRP_RAMP_DURATION_MILLIS = 50;
 
     private final Vibrator mVibrator;
     private final long[] mDefaultPattern;
@@ -102,6 +105,9 @@
      * @param insistent {@code true} if the vibration should loop until it is cancelled.
      */
     public VibrationEffect createFallbackVibration(boolean insistent) {
+        if (mVibrator.hasFrequencyControl()) {
+            return createChirpVibration(FALLBACK_CHIRP_RAMP_DURATION_MILLIS, insistent);
+        }
         return createWaveformVibration(mFallbackPattern, insistent);
     }
 
@@ -111,9 +117,32 @@
      * @param insistent {@code true} if the vibration should loop until it is cancelled.
      */
     public VibrationEffect createDefaultVibration(boolean insistent) {
+        if (mVibrator.hasFrequencyControl()) {
+            return createChirpVibration(DEFAULT_CHIRP_RAMP_DURATION_MILLIS, insistent);
+        }
         return createWaveformVibration(mDefaultPattern, insistent);
     }
 
+    private static VibrationEffect createChirpVibration(int rampDuration, boolean insistent) {
+        VibrationEffect.WaveformBuilder waveformBuilder = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0, /* frequency= */ -0.85f, /* duration= */ 0)
+                .addRamp(/* amplitude= */ 1, /* frequency= */ -0.25f, rampDuration)
+                .addStep(/* amplitude= */ 1, /* frequency= */ -0.25f, CHIRP_LEVEL_DURATION_MILLIS)
+                .addRamp(/* amplitude= */ 0, /* frequency= */ -0.85f, rampDuration);
+
+        if (insistent) {
+            return waveformBuilder
+                    .addStep(/* amplitude= */ 0, CHIRP_LEVEL_DURATION_MILLIS)
+                    .build(/* repeat= */ 0);
+        }
+
+        VibrationEffect singleBeat = waveformBuilder.build();
+        return VibrationEffect.startComposition()
+                .addEffect(singleBeat)
+                .addEffect(singleBeat, /* delay= */ CHIRP_LEVEL_DURATION_MILLIS)
+                .compose();
+    }
+
     private static long[] getLongArray(Resources resources, int resId, int maxLength, long[] def) {
         int[] ar = resources.getIntArray(resId);
         if (ar == null) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index cb72f0f..ce28c13 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -10576,7 +10576,7 @@
                 userId);
         // Find any earlier preferred or last chosen entries and nuke them
         findPreferredActivityNotLocked(
-                intent, resolvedType, flags, query, false, true, false, userId);
+                intent, resolvedType, flags, query, 0, false, true, false, userId);
         // Add the new activity as the last chosen for this filter
         addPreferredActivity(filter, match, null, activity, false, userId,
                 "Setting last chosen", false);
@@ -10592,7 +10592,7 @@
         final List<ResolveInfo> query = queryIntentActivitiesInternal(intent, resolvedType, flags,
                 userId);
         return findPreferredActivityNotLocked(
-                intent, resolvedType, flags, query, false, false, false, userId);
+                intent, resolvedType, flags, query, 0, false, false, false, userId);
     }
 
     private void requestInstantAppResolutionPhaseTwo(AuxiliaryResolveInfo responseObj,
@@ -10634,7 +10634,7 @@
                 // If we have saved a preference for a preferred activity for
                 // this Intent, use that.
                 ResolveInfo ri = findPreferredActivityNotLocked(intent, resolvedType,
-                        flags, query, true, false, debug, userId, queryMayBeFiltered);
+                        flags, query, r0.priority, true, false, debug, userId, queryMayBeFiltered);
                 if (ri != null) {
                     return ri;
                 }
@@ -10780,17 +10780,17 @@
     }
 
     ResolveInfo findPreferredActivityNotLocked(Intent intent, String resolvedType, int flags,
-            List<ResolveInfo> query, boolean always,
+            List<ResolveInfo> query, int priority, boolean always,
             boolean removeMatches, boolean debug, int userId) {
         return findPreferredActivityNotLocked(
-                intent, resolvedType, flags, query, always, removeMatches, debug, userId,
+                intent, resolvedType, flags, query, priority, always, removeMatches, debug, userId,
                 UserHandle.getAppId(Binder.getCallingUid()) >= Process.FIRST_APPLICATION_UID);
     }
 
     // TODO: handle preferred activities missing while user has amnesia
     /** <b>must not hold {@link #mLock}</b> */
     ResolveInfo findPreferredActivityNotLocked(Intent intent, String resolvedType, int flags,
-            List<ResolveInfo> query, boolean always,
+            List<ResolveInfo> query, int priority, boolean always,
             boolean removeMatches, boolean debug, int userId, boolean queryMayBeFiltered) {
         if (Thread.holdsLock(mLock)) {
             Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName()
@@ -23601,7 +23601,7 @@
         final List<ResolveInfo> resolveInfos = queryIntentActivitiesInternal(intent, null,
                 MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, userId);
         final ResolveInfo preferredResolveInfo = findPreferredActivityNotLocked(
-                intent, null, 0, resolveInfos, true, false, false, userId);
+                intent, null, 0, resolveInfos, 0, true, false, false, userId);
         final String packageName = preferredResolveInfo != null
                 && preferredResolveInfo.activityInfo != null
                 ? preferredResolveInfo.activityInfo.packageName : null;
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 3019146..7096f6f 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -913,6 +913,11 @@
         }
         grantPermissionsToSystemPackage(pm, dialerPackage, userId,
                 CONTACTS_PERMISSIONS, SMS_PERMISSIONS, MICROPHONE_PERMISSIONS, CAMERA_PERMISSIONS);
+        boolean isAndroidAutomotive =
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0);
+        if (isAndroidAutomotive) {
+            grantPermissionsToSystemPackage(pm, dialerPackage, userId, NEARBY_DEVICES_PERMISSIONS);
+        }
     }
 
     private void grantDefaultPermissionsToDefaultSystemSmsApp(PackageManagerWrapper pm,
diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java
index 8b46906..18c45e4 100644
--- a/services/core/java/com/android/server/policy/AppOpsPolicy.java
+++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java
@@ -196,8 +196,9 @@
     }
 
     private static boolean isHotwordDetectionServiceRequired(PackageManager pm) {
-        // Usage of the HotwordDetectionService won't be enforced until a later release.
-        return false;
+        // The HotwordDetectionService APIs aren't ready yet for Auto or TV.
+        return !(pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+                || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index edd5f5f..27a16e9 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -41,6 +41,7 @@
 import com.android.server.devicestate.DeviceStateProvider;
 import com.android.server.policy.devicestate.config.Conditions;
 import com.android.server.policy.devicestate.config.DeviceStateConfig;
+import com.android.server.policy.devicestate.config.Flags;
 import com.android.server.policy.devicestate.config.LidSwitchCondition;
 import com.android.server.policy.devicestate.config.NumericRange;
 import com.android.server.policy.devicestate.config.SensorCondition;
@@ -81,13 +82,14 @@
 public final class DeviceStateProviderImpl implements DeviceStateProvider,
         InputManagerInternal.LidSwitchCallback, SensorEventListener {
     private static final String TAG = "DeviceStateProviderImpl";
+    private static final boolean DEBUG = false;
 
     private static final BooleanSupplier TRUE_BOOLEAN_SUPPLIER = () -> true;
     private static final BooleanSupplier FALSE_BOOLEAN_SUPPLIER = () -> false;
 
     @VisibleForTesting
     static final DeviceState DEFAULT_DEVICE_STATE = new DeviceState(MINIMUM_DEVICE_STATE,
-            "DEFAULT");
+            "DEFAULT", 0 /* flags */);
 
     private static final String VENDOR_CONFIG_FILE_PATH = "etc/devicestate/";
     private static final String DATA_CONFIG_FILE_PATH = "system/devicestate/";
@@ -131,7 +133,26 @@
                         config.getDeviceState()) {
                     final int state = stateConfig.getIdentifier().intValue();
                     final String name = stateConfig.getName() == null ? "" : stateConfig.getName();
-                    deviceStateList.add(new DeviceState(state, name));
+
+                    int flags = 0;
+                    final Flags configFlags = stateConfig.getFlags();
+                    if (configFlags != null) {
+                        List<String> configFlagStrings = configFlags.getFlag();
+                        for (int i = 0; i < configFlagStrings.size(); i++) {
+                            final String configFlagString = configFlagStrings.get(i);
+                            switch (configFlagString) {
+                                case "FLAG_CANCEL_STICKY_REQUESTS":
+                                    flags |= DeviceState.FLAG_CANCEL_STICKY_REQUESTS;
+                                    break;
+                                default:
+                                    Slog.w(TAG, "Parsed unknown flag with name: "
+                                            + configFlagString);
+                                    break;
+                            }
+                        }
+                    }
+
+                    deviceStateList.add(new DeviceState(state, name, flags));
 
                     final Conditions condition = stateConfig.getConditions();
                     conditionsList.add(condition);
@@ -193,6 +214,10 @@
 
         for (int i = 0; i < stateConditions.size(); i++) {
             final int state = deviceStates.get(i).getIdentifier();
+            if (DEBUG) {
+                Slog.d(TAG, "Evaluating conditions for device state " + state
+                        + " (" + deviceStates.get(i).getName() + ")");
+            }
             final Conditions conditions = stateConditions.get(i);
             if (conditions == null) {
                 mStateConditions.put(state, TRUE_BOOLEAN_SUPPLIER);
@@ -213,6 +238,9 @@
             if (lidSwitchCondition != null) {
                 suppliers.add(new LidSwitchBooleanSupplier(lidSwitchCondition.getOpen()));
                 lidSwitchRequired = true;
+                if (DEBUG) {
+                    Slog.d(TAG, "Lid switch required");
+                }
             }
 
             List<SensorCondition> sensorConditions = conditions.getSensor();
@@ -229,6 +257,11 @@
                     break;
                 }
 
+                if (DEBUG) {
+                    Slog.d(TAG, "Found sensor with type: " + expectedSensorType
+                            + " (" + expectedSensorName + ")");
+                }
+
                 suppliers.add(new SensorBooleanSupplier(foundSensor, sensorCondition.getValue()));
                 sensorsRequired.add(foundSensor);
             }
@@ -323,6 +356,10 @@
             int newState = mOrderedStates[0].getIdentifier();
             for (int i = 0; i < mOrderedStates.length; i++) {
                 int state = mOrderedStates[i].getIdentifier();
+                if (DEBUG) {
+                    Slog.d(TAG, "Checking conditions for " + mOrderedStates[i].getName() + "("
+                            + i + ")");
+                }
                 boolean conditionSatisfied;
                 try {
                     conditionSatisfied = mStateConditions.get(state).getAsBoolean();
@@ -330,10 +367,16 @@
                     // Failed to compute the current state based on current available data. Return
                     // with the expectation that notifyDeviceStateChangedIfNeeded() will be called
                     // when a callback with the missing data is triggered.
+                    if (DEBUG) {
+                        Slog.d(TAG, "Unable to check current state", e);
+                    }
                     return;
                 }
 
                 if (conditionSatisfied) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Device State conditions satisfied, transition to " + state);
+                    }
                     newState = state;
                     break;
                 }
@@ -355,6 +398,9 @@
         synchronized (mLock) {
             mIsLidOpen = lidOpen;
         }
+        if (DEBUG) {
+            Slog.d(TAG, "Lid switch state: " + (lidOpen ? "open" : "closed"));
+        }
         notifyDeviceStateChangedIfNeeded();
     }
 
@@ -440,6 +486,9 @@
         private boolean adheresToRange(float value, @NonNull NumericRange range) {
             final BigDecimal min = range.getMin_optional();
             if (min != null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "value: " + value + ", constraint min: " + min.floatValue());
+                }
                 if (value <= min.floatValue()) {
                     return false;
                 }
@@ -447,6 +496,10 @@
 
             final BigDecimal minInclusive = range.getMinInclusive_optional();
             if (minInclusive != null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "value: " + value + ", constraint min-inclusive: "
+                            + minInclusive.floatValue());
+                }
                 if (value < minInclusive.floatValue()) {
                     return false;
                 }
@@ -454,6 +507,9 @@
 
             final BigDecimal max = range.getMax_optional();
             if (max != null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "value: " + value + ", constraint max: " + max.floatValue());
+                }
                 if (value >= max.floatValue()) {
                     return false;
                 }
@@ -461,6 +517,10 @@
 
             final BigDecimal maxInclusive = range.getMaxInclusive_optional();
             if (maxInclusive != null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "value: " + value + ", constraint max-inclusive: "
+                            + maxInclusive.floatValue());
+                }
                 if (value > maxInclusive.floatValue()) {
                     return false;
                 }
diff --git a/services/core/java/com/android/server/policy/DisplayFoldController.java b/services/core/java/com/android/server/policy/DisplayFoldController.java
index 3c9b106..04b5005 100644
--- a/services/core/java/com/android/server/policy/DisplayFoldController.java
+++ b/services/core/java/com/android/server/policy/DisplayFoldController.java
@@ -16,10 +16,8 @@
 
 package com.android.server.policy;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Rect;
-import android.hardware.ICameraService;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.hardware.display.DisplayManagerInternal;
@@ -27,13 +25,11 @@
 import android.os.HandlerExecutor;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
-import android.util.Slog;
 import android.view.DisplayInfo;
 import android.view.IDisplayFoldListener;
 
 import com.android.server.DisplayThread;
 import com.android.server.LocalServices;
-import com.android.server.camera.CameraServiceProxy;
 import com.android.server.wm.WindowManagerInternal;
 
 /**
@@ -41,13 +37,8 @@
  * TODO(b/126160895): Move DisplayFoldController from PhoneWindowManager to DisplayPolicy.
  */
 class DisplayFoldController {
-    private static final String TAG = "DisplayFoldController";
-
     private final WindowManagerInternal mWindowManagerInternal;
     private final DisplayManagerInternal mDisplayManagerInternal;
-    // Camera service proxy can be disabled through a config.
-    @Nullable
-    private final CameraServiceProxy mCameraServiceProxy;
     private final int mDisplayId;
     private final Handler mHandler;
 
@@ -64,12 +55,10 @@
 
     DisplayFoldController(
             Context context, WindowManagerInternal windowManagerInternal,
-            DisplayManagerInternal displayManagerInternal,
-            @Nullable CameraServiceProxy cameraServiceProxy, int displayId, Rect foldedArea,
+            DisplayManagerInternal displayManagerInternal, int displayId, Rect foldedArea,
             Handler handler) {
         mWindowManagerInternal = windowManagerInternal;
         mDisplayManagerInternal = displayManagerInternal;
-        mCameraServiceProxy = cameraServiceProxy;
         mDisplayId = displayId;
         mFoldedArea = new Rect(foldedArea);
         mHandler = handler;
@@ -124,16 +113,6 @@
             }
         }
 
-        if (mCameraServiceProxy != null) {
-            if (folded) {
-                mCameraServiceProxy.setDeviceStateFlags(ICameraService.DEVICE_STATE_FOLDED);
-            } else {
-                mCameraServiceProxy.clearDeviceStateFlags(ICameraService.DEVICE_STATE_FOLDED);
-            }
-        } else {
-            Slog.w(TAG, "Camera service unavailable to toggle folded state.");
-        }
-
         mDurationLogger.setDeviceFolded(folded);
         mDurationLogger.logFocusedAppWithFoldState(folded, mFocusedApp);
         mFolded = folded;
@@ -188,8 +167,6 @@
                 LocalServices.getService(WindowManagerInternal.class);
         final DisplayManagerInternal displayService =
                 LocalServices.getService(DisplayManagerInternal.class);
-        final CameraServiceProxy cameraServiceProxy =
-                LocalServices.getService(CameraServiceProxy.class);
 
         final String configFoldedArea = context.getResources().getString(
                 com.android.internal.R.string.config_foldedArea);
@@ -201,6 +178,6 @@
         }
 
         return new DisplayFoldController(context, windowManagerService, displayService,
-                cameraServiceProxy, displayId, foldedArea, DisplayThread.getHandler());
+                displayId, foldedArea, DisplayThread.getHandler());
     }
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index b6ca67d..83b7489 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -47,7 +47,6 @@
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW;
-import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
@@ -508,7 +507,6 @@
     private boolean mKeyguardOccludedChanged;
 
     private ActivityTaskManagerInternal.SleepTokenAcquirer mScreenOffSleepTokenAcquirer;
-    volatile boolean mKeyguardOccluded;
     Intent mHomeIntent;
     Intent mCarDockIntent;
     Intent mDeskDockIntent;
@@ -1815,14 +1813,22 @@
 
         mWindowManagerInternal.registerAppTransitionListener(new AppTransitionListener() {
             @Override
-            public int onAppTransitionStartingLocked(boolean keyguardGoingAway, long duration,
-                    long statusBarAnimationStartTime, long statusBarAnimationDuration) {
-                return handleStartTransitionForKeyguardLw(keyguardGoingAway, duration);
+            public int onAppTransitionStartingLocked(boolean keyguardGoingAway,
+                    boolean keyguardOccluding, long duration, long statusBarAnimationStartTime,
+                    long statusBarAnimationDuration) {
+                // When remote animation is enabled for KEYGUARD_GOING_AWAY transition, SysUI
+                // receives IRemoteAnimationRunner#onAnimationStart to start animation, so we don't
+                // need to call IKeyguardService#keyguardGoingAway here.
+                return handleStartTransitionForKeyguardLw(keyguardGoingAway
+                        && !WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation,
+                        keyguardOccluding, duration);
             }
 
             @Override
             public void onAppTransitionCancelledLocked(boolean keyguardGoingAway) {
-                handleStartTransitionForKeyguardLw(keyguardGoingAway, 0 /* duration */);
+                handleStartTransitionForKeyguardLw(
+                        keyguardGoingAway, false /* keyguardOccludingStarted */,
+                        0 /* duration */);
             }
         });
 
@@ -2399,7 +2405,7 @@
                 // the keyguard is being hidden. This is okay because starting windows never show
                 // secret information.
                 // TODO(b/113840485): Occluded may not only happen on default display
-                if (displayId == DEFAULT_DISPLAY && mKeyguardOccluded) {
+                if (displayId == DEFAULT_DISPLAY && isKeyguardOccluded()) {
                     windowFlags |= FLAG_SHOW_WHEN_LOCKED;
                 }
             }
@@ -3040,31 +3046,35 @@
 
     @Override
     public void onKeyguardOccludedChangedLw(boolean occluded) {
-        if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()) {
+        if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()
+                && !WindowManagerService.sEnableShellTransitions) {
             mPendingKeyguardOccluded = occluded;
             mKeyguardOccludedChanged = true;
         } else {
-            setKeyguardOccludedLw(occluded, false /* force */);
+            setKeyguardOccludedLw(occluded, false /* force */,
+                    false /* transitionStarted */);
         }
     }
 
     @Override
-    public int applyKeyguardOcclusionChange() {
+    public int applyKeyguardOcclusionChange(boolean transitionStarted) {
         if (mKeyguardOccludedChanged) {
             if (DEBUG_KEYGUARD) Slog.d(TAG, "transition/occluded changed occluded="
                     + mPendingKeyguardOccluded);
             mKeyguardOccludedChanged = false;
-            if (setKeyguardOccludedLw(mPendingKeyguardOccluded, false /* force */)) {
+            if (setKeyguardOccludedLw(mPendingKeyguardOccluded, false /* force */,
+                    transitionStarted)) {
                 return FINISH_LAYOUT_REDO_LAYOUT | FINISH_LAYOUT_REDO_WALLPAPER;
             }
         }
         return 0;
     }
 
-    private int handleStartTransitionForKeyguardLw(boolean keyguardGoingAway, long duration) {
-        final int res = applyKeyguardOcclusionChange();
-        if (res != 0) return res;
-        if (!WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation && keyguardGoingAway) {
+    private int handleStartTransitionForKeyguardLw(boolean keyguardGoingAway,
+            boolean keyguardOccluding, long duration) {
+        final int redoLayout = applyKeyguardOcclusionChange(keyguardOccluding);
+        if (redoLayout != 0) return redoLayout;
+        if (keyguardGoingAway) {
             if (DEBUG_KEYGUARD) Slog.d(TAG, "Starting keyguard exit animation");
             startKeyguardExitAnimation(SystemClock.uptimeMillis(), duration);
         }
@@ -3214,7 +3224,7 @@
                 return;
             }
 
-            if (!mKeyguardOccluded && mKeyguardDelegate.isInputRestricted()) {
+            if (!isKeyguardOccluded() && mKeyguardDelegate.isInputRestricted()) {
                 // when in keyguard restricted mode, must first verify unlock
                 // before launching home
                 mKeyguardDelegate.verifyUnlock(new OnKeyguardExitResult() {
@@ -3265,42 +3275,35 @@
     @Override
     public void setKeyguardCandidateLw(WindowState win) {
         mKeyguardCandidate = win;
-        setKeyguardOccludedLw(mKeyguardOccluded, true /* force */);
+        setKeyguardOccludedLw(isKeyguardOccluded(), true /* force */,
+                false /* keyguardOccludingStarted */);
     }
 
     /**
      * Updates the occluded state of the Keyguard.
      *
+     * @param isOccluded Whether the Keyguard is occluded by another window.
+     * @param force notify the occluded status to KeyguardService and update flags even though
+     *             occlude status doesn't change.
+     * @param transitionStarted {@code true} if keyguard (un)occluded transition started.
      * @return Whether the flags have changed and we have to redo the layout.
      */
-    private boolean setKeyguardOccludedLw(boolean isOccluded, boolean force) {
+    private boolean setKeyguardOccludedLw(boolean isOccluded, boolean force,
+            boolean transitionStarted) {
         if (DEBUG_KEYGUARD) Slog.d(TAG, "setKeyguardOccluded occluded=" + isOccluded);
-        final boolean wasOccluded = mKeyguardOccluded;
-        final boolean showing = mKeyguardDelegate.isShowing();
-        final boolean changed = wasOccluded != isOccluded || force;
-        if (!isOccluded && changed && showing) {
-            mKeyguardOccluded = false;
-            mKeyguardDelegate.setOccluded(false, true /* animate */);
-            if (mKeyguardCandidate != null) {
-                if (!mKeyguardDelegate.hasLockscreenWallpaper()) {
-                    mKeyguardCandidate.getAttrs().flags |= FLAG_SHOW_WALLPAPER;
-                }
-            }
-            return true;
-        } else if (isOccluded && changed && showing) {
-            mKeyguardOccluded = true;
-            mKeyguardDelegate.setOccluded(true, false /* animate */);
-            if (mKeyguardCandidate != null) {
-                mKeyguardCandidate.getAttrs().flags &= ~FLAG_SHOW_WALLPAPER;
-            }
-            return true;
-        } else if (changed) {
-            mKeyguardOccluded = isOccluded;
-            mKeyguardDelegate.setOccluded(isOccluded, false /* animate */);
-            return false;
-        } else {
+        if (isKeyguardOccluded() == isOccluded && !force) {
             return false;
         }
+
+        final boolean showing = mKeyguardDelegate.isShowing();
+        final boolean animate = showing && !isOccluded;
+        // When remote animation is enabled for keyguard (un)occlude transition, KeyguardService
+        // uses remote animation start as a signal to update its occlusion status ,so we don't need
+        // to notify here.
+        final boolean notify = !WindowManagerService.sEnableRemoteKeyguardOccludeAnimation
+                || !transitionStarted;
+        mKeyguardDelegate.setOccluded(isOccluded, animate, notify);
+        return showing;
     }
 
     /** {@inheritDoc} */
@@ -4270,6 +4273,7 @@
                                     pmWakeReason)) + ")");
         }
 
+        mActivityTaskManagerInternal.notifyWakingUp();
         mDefaultDisplayPolicy.setAwake(true);
 
         // Since goToSleep performs these functions synchronously, we must
@@ -4594,7 +4598,7 @@
     @Override
     public boolean isKeyguardShowingAndNotOccluded() {
         if (mKeyguardDelegate == null) return false;
-        return mKeyguardDelegate.isShowing() && !mKeyguardOccluded;
+        return mKeyguardDelegate.isShowing() && !isKeyguardOccluded();
     }
 
     @Override
@@ -4620,7 +4624,7 @@
     @Override
     public boolean isKeyguardOccluded() {
         if (mKeyguardDelegate == null) return false;
-        return mKeyguardOccluded;
+        return mKeyguardDelegate.isOccluded();
     }
 
     /** {@inheritDoc} */
@@ -5306,7 +5310,7 @@
         proto.write(KEYGUARD_DRAW_COMPLETE, mDefaultDisplayPolicy.isKeyguardDrawComplete());
         proto.write(WINDOW_MANAGER_DRAW_COMPLETE,
                 mDefaultDisplayPolicy.isWindowManagerDrawComplete());
-        proto.write(KEYGUARD_OCCLUDED, mKeyguardOccluded);
+        proto.write(KEYGUARD_OCCLUDED, isKeyguardOccluded());
         proto.write(KEYGUARD_OCCLUDED_CHANGED, mKeyguardOccludedChanged);
         proto.write(KEYGUARD_OCCLUDED_PENDING, mPendingKeyguardOccluded);
         if (mKeyguardDelegate != null) {
@@ -5391,7 +5395,7 @@
             final int key = mDisplayHomeButtonHandlers.keyAt(i);
             pw.println(mDisplayHomeButtonHandlers.get(key));
         }
-        pw.print(prefix); pw.print("mKeyguardOccluded="); pw.print(mKeyguardOccluded);
+        pw.print(prefix); pw.print("mKeyguardOccluded="); pw.print(isKeyguardOccluded());
                 pw.print(" mKeyguardOccludedChanged="); pw.print(mKeyguardOccludedChanged);
                 pw.print(" mPendingKeyguardOccluded="); pw.println(mPendingKeyguardOccluded);
         pw.print(prefix); pw.print("mAllowLockscreenWhenOnDisplays=");
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 78b03b2..c3ec6a4 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -173,8 +173,11 @@
      */
     void onKeyguardOccludedChangedLw(boolean occluded);
 
-    /** Applies a keyguard occlusion change if one happened. */
-    int applyKeyguardOcclusionChange();
+    /**
+     * Applies a keyguard occlusion change if one happened.
+     * @param transitionStarted Whether keyguard (un)occlude transition is starting or not.
+     */
+    int applyKeyguardOcclusionChange(boolean transitionStarted);
 
     /**
      * Interface to the Window Manager state associated with a particular
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
index 0535af5..0080ec6 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
@@ -29,7 +29,6 @@
 import com.android.internal.policy.IKeyguardService;
 import com.android.server.UiThread;
 import com.android.server.policy.WindowManagerPolicy.OnKeyguardExitResult;
-import com.android.server.wm.WindowManagerService;
 
 import java.io.PrintWriter;
 
@@ -67,7 +66,7 @@
         boolean showing;
         boolean showingAndNotOccluded;
         boolean inputRestricted;
-        boolean occluded;
+        volatile boolean occluded;
         boolean secure;
         boolean dreaming;
         boolean systemIsReady;
@@ -235,13 +234,6 @@
         return false;
     }
 
-    public boolean hasLockscreenWallpaper() {
-        if (mKeyguardService != null) {
-            return mKeyguardService.hasLockscreenWallpaper();
-        }
-        return false;
-    }
-
     public boolean hasKeyguard() {
         return mKeyguardState.deviceHasKeyguard;
     }
@@ -259,19 +251,18 @@
         }
     }
 
-    /**
-     * @deprecated Notify occlude status change via remote animation.
-     */
-    @Deprecated
-    public void setOccluded(boolean isOccluded, boolean animate) {
-        if (!WindowManagerService.sEnableRemoteKeyguardOccludeAnimation
-                && mKeyguardService != null) {
+    public void setOccluded(boolean isOccluded, boolean animate, boolean notify) {
+        if (mKeyguardService != null && notify) {
             if (DEBUG) Log.v(TAG, "setOccluded(" + isOccluded + ") animate=" + animate);
             mKeyguardService.setOccluded(isOccluded, animate);
         }
         mKeyguardState.occluded = isOccluded;
     }
 
+    public boolean isOccluded() {
+        return mKeyguardState.occluded;
+    }
+
     public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
         if (mKeyguardService != null) {
             mKeyguardService.dismiss(callback, message);
@@ -406,8 +397,7 @@
     }
 
     public void startKeyguardExitAnimation(long startTime, long fadeoutDuration) {
-        if (!WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation
-                && mKeyguardService != null) {
+        if (mKeyguardService != null) {
             mKeyguardService.startKeyguardExitAnimation(startTime, fadeoutDuration);
         }
     }
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
index 855a1cc..ac650ec 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
@@ -261,10 +261,6 @@
         return mKeyguardStateMonitor.isTrusted();
     }
 
-    public boolean hasLockscreenWallpaper() {
-        return mKeyguardStateMonitor.hasLockscreenWallpaper();
-    }
-
     public boolean isSecure(int userId) {
         return mKeyguardStateMonitor.isSecure(userId);
     }
@@ -276,4 +272,4 @@
     public void dump(String prefix, PrintWriter pw) {
         mKeyguardStateMonitor.dump(prefix, pw);
     }
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java b/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
index add0b01..e651137 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
@@ -44,7 +44,6 @@
     private volatile boolean mSimSecure = true;
     private volatile boolean mInputRestricted = true;
     private volatile boolean mTrusted = false;
-    private volatile boolean mHasLockscreenWallpaper = false;
 
     private int mCurrentUserId;
 
@@ -79,10 +78,6 @@
         return mTrusted;
     }
 
-    public boolean hasLockscreenWallpaper() {
-        return mHasLockscreenWallpaper;
-    }
-
     @Override // Binder interface
     public void onShowingStateChanged(boolean showing) {
         mIsShowing = showing;
@@ -110,11 +105,6 @@
         mCallback.onTrustedChanged();
     }
 
-    @Override // Binder interface
-    public void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) {
-        mHasLockscreenWallpaper = hasLockscreenWallpaper;
-    }
-
     public interface StateCallback {
         void onTrustedChanged();
         void onShowingChanged();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index e7e0425..9784756 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -31,6 +31,8 @@
 import static android.os.PowerManagerInternal.WAKEFULNESS_DREAMING;
 import static android.os.PowerManagerInternal.wakefulnessToString;
 
+import static com.android.internal.util.LatencyTracker.ACTION_TURN_ON_SCREEN;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -104,6 +106,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.LatencyTracker;
 import com.android.internal.util.Preconditions;
 import com.android.server.EventLogTags;
 import com.android.server.LockGuard;
@@ -1842,6 +1845,9 @@
                     + ", details=" + details
                     + ")...");
             Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, TRACE_SCREEN_ON, groupId);
+            // The instrument will be timed out automatically after 2 seconds.
+            LatencyTracker.getInstance(mContext)
+                    .onActionStart(ACTION_TURN_ON_SCREEN, String.valueOf(groupId));
 
             setWakefulnessLocked(groupId, WAKEFULNESS_AWAKE, eventTime, uid, reason, opUid,
                     opPackageName, details);
@@ -3225,6 +3231,7 @@
                         && mDisplayGroupPowerStateMapper.getWakefulnessLocked(
                         groupId) == WAKEFULNESS_AWAKE) {
                     mDisplayGroupPowerStateMapper.setPoweringOnLocked(groupId, false);
+                    LatencyTracker.getInstance(mContext).onActionEnd(ACTION_TURN_ON_SCREEN);
                     Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, TRACE_SCREEN_ON, groupId);
                     final int latencyMs = (int) (mClock.uptimeMillis()
                             - mDisplayGroupPowerStateMapper.getLastPowerOnTimeLocked(groupId));
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
index 6366280..7ffff93 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
@@ -429,15 +429,7 @@
     private static @NonNull
     HidlMemory parcelFileDescriptorToHidlMemory(@Nullable ParcelFileDescriptor data, int dataSize) {
         if (dataSize > 0) {
-            // Extract a dup of the underlying FileDescriptor out of data.
-            FileDescriptor fd = new FileDescriptor();
-            try {
-                ParcelFileDescriptor dup = data.dup();
-                fd.setInt$(dup.detachFd());
-                return HidlMemoryUtil.fileDescriptorToHidlMemory(fd, dataSize);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
+            return HidlMemoryUtil.fileDescriptorToHidlMemory(data.getFileDescriptor(), dataSize);
         } else {
             return HidlMemoryUtil.fileDescriptorToHidlMemory(null, 0);
         }
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
index 2b03fe8..9999aff 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
@@ -125,25 +125,16 @@
      * originator temporarily doesn't have the right permissions to use this service.
      */
     private void enforcePermissionsForPreflight(@NonNull Identity identity) {
-        enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO,
-                /* allowSoftDenial= */ true);
-        enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD,
-                /* allowSoftDenial= */ true);
+        enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO);
+        enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD);
     }
 
     /**
      * Throws a {@link SecurityException} iff the originator has permission to receive data.
      */
     void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) {
-        // SoundTrigger data is treated the same as Hotword-source audio. This should incur the
-        // HOTWORD op instead of the RECORD_AUDIO op. The RECORD_AUDIO permission is still required,
-        // and since this is a data delivery check, soft denials aren't accepted.
-        enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO,
-                /* allowSoftDenial= */ false);
-        int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
-        mContext.getSystemService(AppOpsManager.class).noteOpNoThrow(hotwordOp, identity.uid,
-                identity.packageName, identity.attributionTag, reason);
-
+        enforcePermissionForDataDelivery(mContext, identity, RECORD_AUDIO,
+                reason);
         enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD,
                 reason);
     }
@@ -172,25 +163,20 @@
     /**
      * Throws a {@link SecurityException} if originator permanently doesn't have the given
      * permission.
+     * Soft (temporary) denials are considered OK for preflight purposes.
      *
-     * @param context         A {@link Context}, used for permission checks.
-     * @param identity        The identity to check.
-     * @param permission      The identifier of the permission we want to check.
-     * @param allowSoftDenial If true, the operation succeeds even for soft (temporary) denials.
+     * @param context    A {@link Context}, used for permission checks.
+     * @param identity   The identity to check.
+     * @param permission The identifier of the permission we want to check.
      */
-    // TODO: Consider splitting up this method instead of using `allowSoftDenial`, to make it
-    // clearer when soft denials are not allowed.
     private static void enforcePermissionForPreflight(@NonNull Context context,
-            @NonNull Identity identity, @NonNull String permission, boolean allowSoftDenial) {
+            @NonNull Identity identity, @NonNull String permission) {
         final int status = PermissionUtil.checkPermissionForPreflight(context, identity,
                 permission);
         switch (status) {
             case PermissionChecker.PERMISSION_GRANTED:
-                return;
             case PermissionChecker.PERMISSION_SOFT_DENIED:
-                if (allowSoftDenial) {
-                    return;
-                } // else fall through
+                return;
             case PermissionChecker.PERMISSION_HARD_DENIED:
                 throw new SecurityException(
                         String.format("Failed to obtain permission %s for identity %s", permission,
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 61770ea..106cff1 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -67,6 +67,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.HistoricalOp;
@@ -82,6 +83,7 @@
 import android.bluetooth.BluetoothActivityEnergyInfo;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.UidTraffic;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
@@ -734,6 +736,10 @@
                     case FrameworkStatsLog.RKP_ERROR_STATS:
                     case FrameworkStatsLog.KEYSTORE2_CRASH_STATS:
                         return pullKeystoreAtoms(atomTag, data);
+                    case FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_STATS:
+                        return pullAccessibilityShortcutStatsLocked(atomTag, data);
+                    case FrameworkStatsLog.ACCESSIBILITY_FLOATING_MENU_STATS:
+                        return pullAccessibilityFloatingMenuStatsLocked(atomTag, data);
                     default:
                         throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
                 }
@@ -930,6 +936,8 @@
         registerKeystoreKeyOperationWithGeneralInfo();
         registerRkpErrorStats();
         registerKeystoreCrashStats();
+        registerAccessibilityShortcutStats();
+        registerAccessibilityFloatingMenuStats();
     }
 
     private void initAndRegisterNetworkStatsPullers() {
@@ -4150,6 +4158,26 @@
                 mStatsCallbackImpl);
     }
 
+    private void registerAccessibilityShortcutStats() {
+        int tagId = FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_STATS;
+        mStatsManager.setPullAtomCallback(
+                tagId,
+                null, // use default PullAtomMetadata values
+                DIRECT_EXECUTOR,
+                mStatsCallbackImpl
+        );
+    }
+
+    private void registerAccessibilityFloatingMenuStats() {
+        int tagId = FrameworkStatsLog.ACCESSIBILITY_FLOATING_MENU_STATS;
+        mStatsManager.setPullAtomCallback(
+                tagId,
+                null, // use default PullAtomMetadata values
+                DIRECT_EXECUTOR,
+                mStatsCallbackImpl
+        );
+    }
+
     int parseKeystoreStorageStats(KeystoreAtom[] atoms, List<StatsEvent> pulledData) {
         for (KeystoreAtom atomWrapper : atoms) {
             if (atomWrapper.payload.getTag() != KeystoreAtomPayload.storageStats) {
@@ -4341,6 +4369,144 @@
         }
     }
 
+    int pullAccessibilityShortcutStatsLocked(int atomTag, List<StatsEvent> pulledData) {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        if (userManager == null) {
+            return StatsManager.PULL_SKIP;
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final ContentResolver resolver = mContext.getContentResolver();
+            final int hardware_shortcut_type =
+                    FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__VOLUME_KEY;
+            final int triple_tap_shortcut =
+                    FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__TRIPLE_TAP;
+            for (UserInfo userInfo : userManager.getUsers()) {
+                final int userId = userInfo.getUserHandle().getIdentifier();
+
+                if (isAccessibilityShortcutUser(mContext, userId)) {
+                    final int software_shortcut_type = Settings.Secure.getIntForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 0, userId);
+                    final String software_shortcut_list = Settings.Secure.getStringForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, userId);
+                    final int software_shortcut_service_num = countAccessibilityServices(
+                            software_shortcut_list);
+
+                    final String hardware_shortcut_list = Settings.Secure.getStringForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userId);
+                    final int hardware_shortcut_service_num = countAccessibilityServices(
+                            hardware_shortcut_list);
+
+                    // only allow magnification to use it for now
+                    final int triple_tap_service_num = Settings.Secure.getIntForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, 0, userId);
+
+                    pulledData.add(
+                            FrameworkStatsLog.buildStatsEvent(atomTag,
+                                    software_shortcut_type, software_shortcut_service_num,
+                                    hardware_shortcut_type, hardware_shortcut_service_num,
+                                    triple_tap_shortcut, triple_tap_service_num));
+                }
+            }
+        } catch (RuntimeException e) {
+            Slog.e(TAG, "pulling accessibility shortcuts stats failed at getUsers", e);
+            return StatsManager.PULL_SKIP;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        return StatsManager.PULL_SUCCESS;
+    }
+
+    int pullAccessibilityFloatingMenuStatsLocked(int atomTag, List<StatsEvent> pulledData) {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        if (userManager == null) {
+            return StatsManager.PULL_SKIP;
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final ContentResolver resolver = mContext.getContentResolver();
+            final int defaultSize = 0;
+            final int defaultIconType = 0;
+            final int defaultFadeEnabled = 1;
+            final float defaultOpacity = 0.55f;
+
+            for (UserInfo userInfo : userManager.getUsers()) {
+                final int userId = userInfo.getUserHandle().getIdentifier();
+
+                if (isAccessibilityFloatingMenuUser(mContext, userId)) {
+                    final int size = Settings.Secure.getIntForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE, defaultSize, userId);
+                    final int type = Settings.Secure.getIntForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
+                            defaultIconType, userId);
+                    final boolean fadeEnabled = (Settings.Secure.getIntForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
+                            defaultFadeEnabled, userId)) == 1;
+                    final float opacity = Settings.Secure.getFloatForUser(resolver,
+                            Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY,
+                            defaultOpacity, userId);
+
+                    pulledData.add(
+                            FrameworkStatsLog.buildStatsEvent(atomTag, size, type, fadeEnabled,
+                                    opacity));
+                }
+            }
+        }  catch (RuntimeException e) {
+            Slog.e(TAG, "pulling accessibility floating menu stats failed at getUsers", e);
+            return StatsManager.PULL_SKIP;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        return StatsManager.PULL_SUCCESS;
+    }
+
+    /**
+     * Counts how many accessibility services (including features) there are in the colon-separated
+     * string list.
+     *
+     * @param semicolonList colon-separated string, it should be
+     *                        {@link Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS} or
+     *                        {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE}.
+     * @return The number of accessibility services
+     */
+    private int countAccessibilityServices(String semicolonList) {
+        if (TextUtils.isEmpty(semicolonList)) {
+            return 0;
+        }
+        final int semiColonNums = (int) semicolonList.chars().filter(ch -> ch == ':').count();
+        return TextUtils.isEmpty(semicolonList) ? 0 : semiColonNums + 1;
+    }
+
+    private boolean isAccessibilityShortcutUser(Context context, @UserIdInt int userId) {
+        final ContentResolver resolver = context.getContentResolver();
+
+        final String software_shortcut_list = Settings.Secure.getStringForUser(resolver,
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, userId);
+        final String hardware_shortcut_list = Settings.Secure.getStringForUser(resolver,
+                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userId);
+        final boolean hardware_shortcut_dialog_shown = Settings.Secure.getIntForUser(resolver,
+                Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId) == 1;
+        final boolean software_shortcut_enabled = !TextUtils.isEmpty(software_shortcut_list);
+        final boolean hardware_shortcut_enabled =
+                hardware_shortcut_dialog_shown && !TextUtils.isEmpty(hardware_shortcut_list);
+        final boolean triple_tap_shortcut_enabled = Settings.Secure.getIntForUser(resolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, 0, userId) == 1;
+
+        return software_shortcut_enabled || hardware_shortcut_enabled
+                || triple_tap_shortcut_enabled;
+    }
+
+    private boolean isAccessibilityFloatingMenuUser(Context context, @UserIdInt int userId) {
+        final ContentResolver resolver = context.getContentResolver();
+        final int mode = Settings.Secure.getIntForUser(resolver,
+                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 0, userId);
+        final String software_string = Settings.Secure.getStringForUser(resolver,
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, userId);
+
+        return (mode == Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU)
+                && !TextUtils.isEmpty(software_string);
+    }
+
     // Thermal event received from vendor thermal management subsystem
     private static final class ThermalEventListener extends IThermalEventListener.Stub {
         @Override
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index a436e6b..cdab91b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -23,6 +23,7 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -132,7 +133,7 @@
     /** @see com.android.internal.statusbar.IStatusBar#onSystemBarAttributesChanged */
     void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, boolean isFullscreen);
+            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName);
 
     /** @see com.android.internal.statusbar.IStatusBar#showTransient */
     void showTransient(int displayId, @InternalInsetsType int[] types);
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index abe81e1..b58ca1f 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -60,6 +60,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -526,13 +527,14 @@
         @Override
         public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, boolean isFullscreen) {
+                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                String packageName) {
             getUiState(displayId).setBarAttributes(appearance, appearanceRegions,
-                    navbarColorManagedByIme, behavior, isFullscreen);
+                    navbarColorManagedByIme, behavior, requestedVisibilities, packageName);
             if (mBar != null) {
                 try {
                     mBar.onSystemBarAttributesChanged(displayId, appearance, appearanceRegions,
-                            navbarColorManagedByIme, behavior, isFullscreen);
+                            navbarColorManagedByIme, behavior, requestedVisibilities, packageName);
                 } catch (RemoteException ex) { }
             }
         }
@@ -1104,13 +1106,14 @@
         return state;
     }
 
-    private class UiState {
+    private static class UiState {
         private @Appearance int mAppearance = 0;
         private AppearanceRegion[] mAppearanceRegions = new AppearanceRegion[0];
-        private ArraySet<Integer> mTransientBarTypes = new ArraySet<>();
+        private final ArraySet<Integer> mTransientBarTypes = new ArraySet<>();
         private boolean mNavbarColorManagedByIme = false;
         private @Behavior int mBehavior;
-        private boolean mFullscreen = false;
+        private InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+        private String mPackageName = "none";
         private int mDisabled1 = 0;
         private int mDisabled2 = 0;
         private int mImeWindowVis = 0;
@@ -1120,12 +1123,14 @@
 
         private void setBarAttributes(@Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, boolean isFullscreen) {
+                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                String packageName) {
             mAppearance = appearance;
             mAppearanceRegions = appearanceRegions;
             mNavbarColorManagedByIme = navbarColorManagedByIme;
             mBehavior = behavior;
-            mFullscreen = isFullscreen;
+            mRequestedVisibilities = requestedVisibilities;
+            mPackageName = packageName;
         }
 
         private void showTransient(@InternalInsetsType int[] types) {
@@ -1245,8 +1250,8 @@
                     state.mAppearance, state.mAppearanceRegions, state.mImeWindowVis,
                     state.mImeBackDisposition, state.mShowImeSwitcher,
                     gatherDisableActionsLocked(mCurrentUserId, 2), state.mImeToken,
-                    state.mNavbarColorManagedByIme, state.mBehavior, state.mFullscreen,
-                    transientBarTypes);
+                    state.mNavbarColorManagedByIme, state.mBehavior, state.mRequestedVisibilities,
+                    state.mPackageName, transientBarTypes);
         }
     }
 
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 2a47512..7307662 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -1319,24 +1319,18 @@
                 return IExternalVibratorService.SCALE_MUTE;
             }
 
-            int mode = checkAppOpModeLocked(vib.getUid(), vib.getPackage(),
-                    vib.getVibrationAttributes());
-            if (mode != AppOpsManager.MODE_ALLOWED) {
-                ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib);
-                vibHolder.scale = IExternalVibratorService.SCALE_MUTE;
-                if (mode == AppOpsManager.MODE_ERRORED) {
-                    Slog.w(TAG, "Would be an error: external vibrate from uid " + vib.getUid());
-                    endVibrationLocked(vibHolder, Vibration.Status.IGNORED_ERROR_APP_OPS);
-                } else {
-                    endVibrationLocked(vibHolder, Vibration.Status.IGNORED_APP_OPS);
-                }
-                return vibHolder.scale;
-            }
-
             ExternalVibrationHolder cancelingExternalVibration = null;
             VibrationThread cancelingVibration = null;
             int scale;
             synchronized (mLock) {
+                Vibration.Status ignoreStatus = shouldIgnoreVibrationLocked(
+                        vib.getUid(), vib.getPackage(), vib.getVibrationAttributes());
+                if (ignoreStatus != null) {
+                    ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib);
+                    vibHolder.scale = IExternalVibratorService.SCALE_MUTE;
+                    endVibrationLocked(vibHolder, ignoreStatus);
+                    return vibHolder.scale;
+                }
                 if (mCurrentExternalVibration != null
                         && mCurrentExternalVibration.externalVibration.equals(vib)) {
                     // We are already playing this external vibration, so we can return the same
diff --git a/services/core/java/com/android/server/vr/Vr2dDisplay.java b/services/core/java/com/android/server/vr/Vr2dDisplay.java
index a713e5b..39d7a15 100644
--- a/services/core/java/com/android/server/vr/Vr2dDisplay.java
+++ b/services/core/java/com/android/server/vr/Vr2dDisplay.java
@@ -302,7 +302,8 @@
             builder.setUniqueId(UNIQUE_DISPLAY_ID);
             builder.setFlags(flags);
             mVirtualDisplay = mDisplayManager.createVirtualDisplay(null /* projection */,
-                    builder.build(), null /* callback */, null /* handler */);
+                    builder.build(), null /* callback */, null /* handler */,
+                    null /* windowContext */);
 
             if (mVirtualDisplay != null) {
                 updateDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index a24319f..cf9783f 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -16,6 +16,8 @@
 
 package com.android.server.wm;
 
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_MAGNIFICATION_CALLBACK;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK;
 import static android.os.Build.IS_USER;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
@@ -34,17 +36,21 @@
 import static com.android.server.accessibility.AccessibilityTraceProto.CALLING_PKG;
 import static com.android.server.accessibility.AccessibilityTraceProto.CALLING_STACKS;
 import static com.android.server.accessibility.AccessibilityTraceProto.ELAPSED_REALTIME_NANOS;
+import static com.android.server.accessibility.AccessibilityTraceProto.LOGGING_TYPE;
 import static com.android.server.accessibility.AccessibilityTraceProto.PROCESS_NAME;
 import static com.android.server.accessibility.AccessibilityTraceProto.THREAD_ID_NAME;
 import static com.android.server.accessibility.AccessibilityTraceProto.WHERE;
 import static com.android.server.accessibility.AccessibilityTraceProto.WINDOW_MANAGER_SERVICE;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+import static com.android.server.wm.WindowTracing.WINSCOPE_EXT;
 import static com.android.server.wm.utils.RegionUtils.forEachRect;
 
+import android.accessibilityservice.AccessibilityTrace;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Application;
 import android.content.Context;
 import android.content.pm.PackageManagerInternal;
@@ -84,12 +90,14 @@
 import android.view.ViewConfiguration;
 import android.view.WindowInfo;
 import android.view.WindowManager;
+import android.view.WindowManagerPolicyConstants;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
 import com.android.internal.R;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.TraceBuffer;
+import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.wm.WindowManagerInternal.AccessibilityControllerInternal;
@@ -99,8 +107,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.nio.file.Files;
-import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -116,32 +122,36 @@
     private static final String TAG = AccessibilityController.class.getSimpleName();
 
     private static final Object STATIC_LOCK = new Object();
-    static AccessibilityControllerInternal
+    static AccessibilityControllerInternalImpl
             getAccessibilityControllerInternal(WindowManagerService service) {
         return AccessibilityControllerInternalImpl.getInstance(service);
     }
 
-    private final AccessibilityTracing mAccessibilityTracing;
+    private final AccessibilityControllerInternalImpl mAccessibilityTracing;
     private final WindowManagerService mService;
     private static final Rect EMPTY_RECT = new Rect();
     private static final float[] sTempFloats = new float[9];
 
-    AccessibilityController(WindowManagerService service) {
-        mService = service;
-        mAccessibilityTracing = AccessibilityTracing.getInstance(service);
-    }
-
     private SparseArray<DisplayMagnifier> mDisplayMagnifiers = new SparseArray<>();
     private SparseArray<WindowsForAccessibilityObserver> mWindowsForAccessibilityObserver =
             new SparseArray<>();
+    private SparseArray<IBinder> mFocusedWindow = new SparseArray<>();
+    private int mFocusedDisplay = -1;
 
     // Set to true if initializing window population complete.
     private boolean mAllObserversInitialized = true;
 
+    AccessibilityController(WindowManagerService service) {
+        mService = service;
+        mAccessibilityTracing =
+                AccessibilityController.getAccessibilityControllerInternal(service);
+    }
+
     boolean setMagnificationCallbacks(int displayId, MagnificationCallbacks callbacks) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(
                     TAG + ".setMagnificationCallbacks",
+                    FLAGS_MAGNIFICATION_CALLBACK,
                     "displayId=" + displayId + "; callbacks={" + callbacks + "}");
         }
         boolean result = false;
@@ -172,25 +182,31 @@
 
     /**
      * Sets a callback for observing which windows are touchable for the purposes
-     * of accessibility on specified display.
+     * of accessibility on specified display. When a display is reparented and becomes
+     * an embedded one, the {@link WindowsForAccessibilityCallback#onDisplayReparented(int)}
+     * will notify the accessibility framework to remove the un-used window observer of
+     * this embedded display.
      *
      * @param displayId The logical display id.
      * @param callback The callback.
-     * @return {@code false} if display id is not valid or an embedded display.
+     * @return {@code false} if display id is not valid or an embedded display when the callback
+     * isn't null.
      */
     boolean setWindowsForAccessibilityCallback(int displayId,
             WindowsForAccessibilityCallback callback) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(
                     TAG + ".setWindowsForAccessibilityCallback",
+                    FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
                     "displayId=" + displayId + "; callback={" + callback + "}");
         }
-        final DisplayContent dc = mService.mRoot.getDisplayContentOrCreate(displayId);
-        if (dc == null) {
-            return false;
-        }
 
         if (callback != null) {
+            final DisplayContent dc = mService.mRoot.getDisplayContentOrCreate(displayId);
+            if (dc == null) {
+                return false;
+            }
+
             WindowsForAccessibilityObserver observer =
                     mWindowsForAccessibilityObserver.get(displayId);
             if (isEmbeddedDisplay(dc)) {
@@ -209,21 +225,13 @@
                 if (Build.IS_DEBUGGABLE) {
                     throw new IllegalStateException(errorMessage);
                 }
-                removeObserverOfEmbeddedDisplay(observer);
+                removeObserversForEmbeddedChildDisplays(observer);
                 mWindowsForAccessibilityObserver.remove(displayId);
             }
             observer = new WindowsForAccessibilityObserver(mService, displayId, callback);
             mWindowsForAccessibilityObserver.put(displayId, observer);
             mAllObserversInitialized &= observer.mInitialized;
         } else {
-            if (isEmbeddedDisplay(dc)) {
-                // If this display is an embedded one, its window observer should be removed along
-                // with the window observer of its parent display removed because the window
-                // observer of the embedded display and its parent display is the same, and would
-                // be removed together when stopping the window tracking of its parent display. So
-                // here don't need to do removing window observer of the embedded display again.
-                return true;
-            }
             final WindowsForAccessibilityObserver windowsForA11yObserver =
                     mWindowsForAccessibilityObserver.get(displayId);
             if (windowsForA11yObserver == null) {
@@ -234,16 +242,17 @@
                     throw new IllegalStateException(errorMessage);
                 }
             }
-            removeObserverOfEmbeddedDisplay(windowsForA11yObserver);
+            removeObserversForEmbeddedChildDisplays(windowsForA11yObserver);
             mWindowsForAccessibilityObserver.remove(displayId);
         }
         return true;
     }
 
     void performComputeChangedWindowsNot(int displayId, boolean forceSend) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(
                     TAG + ".performComputeChangedWindowsNot",
+                    FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
                     "displayId=" + displayId + "; forceSend=" + forceSend);
         }
         WindowsForAccessibilityObserver observer = null;
@@ -260,8 +269,10 @@
     }
 
     void setMagnificationSpec(int displayId, MagnificationSpec spec) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".setMagnificationSpec",
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK
+                | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".setMagnificationSpec",
+                    FLAGS_MAGNIFICATION_CALLBACK | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
                     "displayId=" + displayId + "; spec={" + spec + "}");
         }
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
@@ -276,8 +287,9 @@
     }
 
     void getMagnificationRegion(int displayId, Region outMagnificationRegion) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".getMagnificationRegion",
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".getMagnificationRegion",
+                    FLAGS_MAGNIFICATION_CALLBACK,
                     "displayId=" + displayId + "; outMagnificationRegion={" + outMagnificationRegion
                             + "}");
         }
@@ -288,9 +300,10 @@
     }
 
     void onRectangleOnScreenRequested(int displayId, Rect rectangle) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(
                     TAG + ".onRectangleOnScreenRequested",
+                    FLAGS_MAGNIFICATION_CALLBACK,
                     "displayId=" + displayId + "; rectangle={" + rectangle + "}");
         }
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
@@ -301,9 +314,11 @@
     }
 
     void onWindowLayersChanged(int displayId) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
-                    TAG + ".onWindowLayersChanged", "displayId=" + displayId);
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK
+                | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".onWindowLayersChanged",
+                    FLAGS_MAGNIFICATION_CALLBACK | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
+                    "displayId=" + displayId);
         }
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
         if (displayMagnifier != null) {
@@ -316,15 +331,18 @@
         }
     }
 
-    void onRotationChanged(DisplayContent displayContent) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".onRotationChanged",
+    void onDisplaySizeChanged(DisplayContent displayContent) {
+
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK
+                | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".onRotationChanged",
+                    FLAGS_MAGNIFICATION_CALLBACK | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
                     "displayContent={" + displayContent + "}");
         }
         final int displayId = displayContent.getDisplayId();
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
         if (displayMagnifier != null) {
-            displayMagnifier.onRotationChanged(displayContent);
+            displayMagnifier.onDisplaySizeChanged(displayContent);
         }
         final WindowsForAccessibilityObserver windowsForA11yObserver =
                 mWindowsForAccessibilityObserver.get(displayId);
@@ -334,8 +352,9 @@
     }
 
     void onAppWindowTransition(int displayId, int transition) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".onAppWindowTransition",
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".onAppWindowTransition",
+                    FLAGS_MAGNIFICATION_CALLBACK,
                     "displayId=" + displayId + "; transition=" + transition);
         }
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
@@ -346,8 +365,10 @@
     }
 
     void onWindowTransition(WindowState windowState, int transition) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".onWindowTransition",
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK
+                | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".onWindowTransition",
+                    FLAGS_MAGNIFICATION_CALLBACK | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
                     "windowState={" + windowState + "}; transition=" + transition);
         }
         final int displayId = windowState.getDisplayId();
@@ -364,9 +385,9 @@
 
     void onWindowFocusChangedNot(int displayId) {
         // Not relevant for the display magnifier.
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
-                    TAG + ".onWindowFocusChangedNot", "displayId=" + displayId);
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".onWindowFocusChangedNot",
+                    FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK, "displayId=" + displayId);
         }
         WindowsForAccessibilityObserver observer = null;
         synchronized (mService.mGlobalLock) {
@@ -426,12 +447,10 @@
     }
 
     void onSomeWindowResizedOrMovedWithCallingUid(int callingUid, int... displayIds) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
-                    TAG + ".onSomeWindowResizedOrMoved",
-                    "displayIds={" + displayIds.toString() + "}",
-                    "".getBytes(),
-                    callingUid);
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".onSomeWindowResizedOrMoved",
+                    FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
+                    "displayIds={" + displayIds.toString() + "}", "".getBytes(), callingUid);
         }
         // Not relevant for the display magnifier.
         for (int i = 0; i < displayIds.length; i++) {
@@ -444,9 +463,10 @@
     }
 
     void drawMagnifiedRegionBorderIfNeeded(int displayId, SurfaceControl.Transaction t) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(
                     TAG + ".drawMagnifiedRegionBorderIfNeeded",
+                    FLAGS_MAGNIFICATION_CALLBACK,
                     "displayId=" + displayId + "; transaction={" + t + "}");
         }
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
@@ -457,8 +477,9 @@
     }
 
     MagnificationSpec getMagnificationSpecForWindow(WindowState windowState) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".getMagnificationSpecForWindow",
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".getMagnificationSpecForWindow",
+                    FLAGS_MAGNIFICATION_CALLBACK,
                     "windowState={" + windowState + "}");
         }
         final int displayId = windowState.getDisplayId();
@@ -470,17 +491,19 @@
     }
 
     boolean hasCallbacks() {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".hasCallbacks");
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK
+                | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".hasCallbacks",
+                    FLAGS_MAGNIFICATION_CALLBACK | FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK);
         }
         return (mDisplayMagnifiers.size() > 0
                 || mWindowsForAccessibilityObserver.size() > 0);
     }
 
     void setForceShowMagnifiableBounds(int displayId, boolean show) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".setForceShowMagnifiableBounds",
-                    "displayId=" + displayId + "; show=" + show);
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".setForceShowMagnifiableBounds",
+                    FLAGS_MAGNIFICATION_CALLBACK, "displayId=" + displayId + "; show=" + show);
         }
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
         if (displayMagnifier != null) {
@@ -497,39 +520,50 @@
 
     void handleWindowObserverOfEmbeddedDisplay(
             int embeddedDisplayId, WindowState parentWindow, int callingUid) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".handleWindowObserverOfEmbeddedDisplay",
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".handleWindowObserverOfEmbeddedDisplay",
+                    FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK,
                     "embeddedDisplayId=" + embeddedDisplayId + "; parentWindowState={"
-                    + parentWindow + "}",
-                    "".getBytes(),
-                    callingUid);
+                    + parentWindow + "}", "".getBytes(), callingUid);
         }
         if (embeddedDisplayId == Display.DEFAULT_DISPLAY || parentWindow == null) {
             return;
         }
-        // Finds the parent display of this embedded display
-        final int parentDisplayId;
-        WindowState candidate = parentWindow;
-        while (candidate != null) {
-            parentWindow = candidate;
-            candidate = parentWindow.getDisplayContent().getParentWindow();
+        mService.mH.sendMessage(PooledLambda.obtainMessage(
+                AccessibilityController::updateWindowObserverOfEmbeddedDisplay,
+                this, embeddedDisplayId, parentWindow));
+    }
+
+    private void updateWindowObserverOfEmbeddedDisplay(int embeddedDisplayId,
+            WindowState parentWindow) {
+        final WindowsForAccessibilityObserver windowsForA11yObserver;
+
+        synchronized (mService.mGlobalLock) {
+            // Finds the parent display of this embedded display
+            WindowState candidate = parentWindow;
+            while (candidate != null) {
+                parentWindow = candidate;
+                candidate = parentWindow.getDisplayContent().getParentWindow();
+            }
+            final int parentDisplayId = parentWindow.getDisplayId();
+            // Uses the observer of parent display
+            windowsForA11yObserver = mWindowsForAccessibilityObserver.get(parentDisplayId);
         }
-        parentDisplayId = parentWindow.getDisplayId();
-        // Uses the observer of parent display
-        final WindowsForAccessibilityObserver windowsForA11yObserver =
-                mWindowsForAccessibilityObserver.get(parentDisplayId);
 
         if (windowsForA11yObserver != null) {
+            windowsForA11yObserver.notifyDisplayReparented(embeddedDisplayId);
             windowsForA11yObserver.addEmbeddedDisplay(embeddedDisplayId);
-            // Replaces the observer of embedded display to the one of parent display
-            mWindowsForAccessibilityObserver.put(embeddedDisplayId, windowsForA11yObserver);
+            synchronized (mService.mGlobalLock) {
+                // Replaces the observer of embedded display to the one of parent display
+                mWindowsForAccessibilityObserver.put(embeddedDisplayId, windowsForA11yObserver);
+            }
         }
     }
 
     void onImeSurfaceShownChanged(WindowState windowState, boolean shown) {
-        if (mAccessibilityTracing.isEnabled()) {
-            mAccessibilityTracing.logState(TAG + ".onImeSurfaceShownChanged",
-                    "windowState=" + windowState + "; shown=" + shown);
+        if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+            mAccessibilityTracing.logTrace(TAG + ".onImeSurfaceShownChanged",
+                    FLAGS_MAGNIFICATION_CALLBACK, "windowState=" + windowState + ";shown=" + shown);
         }
         final int displayId = windowState.getDisplayId();
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
@@ -555,7 +589,7 @@
                 + "mWindowsForAccessibilityObserver=" + mWindowsForAccessibilityObserver);
     }
 
-    private void removeObserverOfEmbeddedDisplay(WindowsForAccessibilityObserver
+    private void removeObserversForEmbeddedChildDisplays(WindowsForAccessibilityObserver
             observerOfParentDisplay) {
         final IntArray embeddedDisplayIdList =
                 observerOfParentDisplay.getAndClearEmbeddedDisplayIdList();
@@ -572,6 +606,29 @@
         return display.getType() == Display.TYPE_VIRTUAL && dc.getParentWindow() != null;
     }
 
+    void onFocusChanged(InputTarget lastTarget, InputTarget newTarget) {
+        if (lastTarget != null) {
+            mFocusedWindow.remove(lastTarget.getDisplayId());
+        }
+        if (newTarget != null) {
+            int displayId = newTarget.getDisplayId();
+            IBinder clientBinder = newTarget.getIWindow().asBinder();
+            mFocusedWindow.put(displayId, clientBinder);
+        }
+    }
+
+    public void onDisplayRemoved(int displayId) {
+        mFocusedWindow.remove(displayId);
+    }
+
+    public void setFocusedDisplay(int focusedDisplayId) {
+        mFocusedDisplay = focusedDisplayId;
+    }
+
+    @Nullable IBinder getFocusedWindowToken() {
+        return mFocusedWindow.get(mFocusedDisplay);
+    }
+
     /**
      * This class encapsulates the functionality related to display magnification.
      */
@@ -580,7 +637,7 @@
         private static final String LOG_TAG = TAG_WITH_CLASS_NAME ? "DisplayMagnifier" : TAG_WM;
 
         private static final boolean DEBUG_WINDOW_TRANSITIONS = false;
-        private static final boolean DEBUG_ROTATION = false;
+        private static final boolean DEBUG_DISPLAY_SIZE = false;
         private static final boolean DEBUG_LAYERS = false;
         private static final boolean DEBUG_RECTANGLE_REQUESTED = false;
         private static final boolean DEBUG_VIEWPORT_WINDOW = false;
@@ -599,7 +656,7 @@
         private final Handler mHandler;
         private final DisplayContent mDisplayContent;
         private final Display mDisplay;
-        private final AccessibilityTracing mAccessibilityTracing;
+        private final AccessibilityControllerInternalImpl mAccessibilityTracing;
 
         private final MagnificationCallbacks mCallbacks;
 
@@ -618,11 +675,13 @@
             mDisplay = display;
             mHandler = new MyHandler(mService.mH.getLooper());
             mMagnifedViewport = new MagnifiedViewport();
-            mAccessibilityTracing = AccessibilityTracing.getInstance(mService);
+            mAccessibilityTracing =
+                    AccessibilityController.getAccessibilityControllerInternal(mService);
             mLongAnimationDuration = mDisplayContext.getResources().getInteger(
                     com.android.internal.R.integer.config_longAnimTime);
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".DisplayMagnifier.constructor",
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".DisplayMagnifier.constructor",
+                        FLAGS_MAGNIFICATION_CALLBACK,
                         "windowManagerService={" + windowManagerService + "}; displayContent={"
                                 + displayContent + "}; display={" + display + "}; callbacks={"
                                 + callbacks + "}");
@@ -630,9 +689,9 @@
         }
 
         void setMagnificationSpec(MagnificationSpec spec) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(
-                        LOG_TAG + ".setMagnificationSpec", "spec={" + spec + "}");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".setMagnificationSpec",
+                        FLAGS_MAGNIFICATION_CALLBACK, "spec={" + spec + "}");
             }
             mMagnifedViewport.updateMagnificationSpec(spec);
             mMagnifedViewport.recomputeBounds();
@@ -642,25 +701,26 @@
         }
 
         void setForceShowMagnifiableBounds(boolean show) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(
-                        LOG_TAG + ".setForceShowMagnifiableBounds", "show=" + show);
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".setForceShowMagnifiableBounds",
+                        FLAGS_MAGNIFICATION_CALLBACK, "show=" + show);
             }
             mForceShowMagnifiableBounds = show;
             mMagnifedViewport.setMagnifiedRegionBorderShown(show, true);
         }
 
         boolean isForceShowingMagnifiableBounds() {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".isForceShowingMagnifiableBounds");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".isForceShowingMagnifiableBounds",
+                        FLAGS_MAGNIFICATION_CALLBACK);
             }
             return mForceShowMagnifiableBounds;
         }
 
         void onRectangleOnScreenRequested(Rect rectangle) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(
-                        LOG_TAG + ".onRectangleOnScreenRequested", "rectangle={" + rectangle + "}");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".onRectangleOnScreenRequested",
+                        FLAGS_MAGNIFICATION_CALLBACK, "rectangle={" + rectangle + "}");
             }
             if (DEBUG_RECTANGLE_REQUESTED) {
                 Slog.i(LOG_TAG, "Rectangle on screen requested: " + rectangle);
@@ -683,8 +743,9 @@
         }
 
         void onWindowLayersChanged() {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".onWindowLayersChanged");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(
+                        LOG_TAG + ".onWindowLayersChanged", FLAGS_MAGNIFICATION_CALLBACK);
             }
             if (DEBUG_LAYERS) {
                 Slog.i(LOG_TAG, "Layers changed.");
@@ -693,23 +754,24 @@
             mService.scheduleAnimationLocked();
         }
 
-        void onRotationChanged(DisplayContent displayContent) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(
-                        LOG_TAG + ".onRotationChanged", "displayContent={" + displayContent + "}");
+        void onDisplaySizeChanged(DisplayContent displayContent) {
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".onDisplaySizeChanged",
+                        FLAGS_MAGNIFICATION_CALLBACK, "displayContent={" + displayContent + "}");
             }
-            if (DEBUG_ROTATION) {
+            if (DEBUG_DISPLAY_SIZE) {
                 final int rotation = displayContent.getRotation();
                 Slog.i(LOG_TAG, "Rotation: " + Surface.rotationToString(rotation)
                         + " displayId: " + displayContent.getDisplayId());
             }
-            mMagnifedViewport.onRotationChanged();
-            mHandler.sendEmptyMessage(MyHandler.MESSAGE_NOTIFY_ROTATION_CHANGED);
+            mMagnifedViewport.onDisplaySizeChanged();
+            mHandler.sendEmptyMessage(MyHandler.MESSAGE_NOTIFY_DISPLAY_SIZE_CHANGED);
         }
 
         void onAppWindowTransition(int displayId, int transition) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".onAppWindowTransition",
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".onAppWindowTransition",
+                        FLAGS_MAGNIFICATION_CALLBACK,
                         "displayId=" + displayId + "; transition=" + transition);
             }
             if (DEBUG_WINDOW_TRANSITIONS) {
@@ -721,6 +783,7 @@
             if (magnifying) {
                 switch (transition) {
                     case WindowManager.TRANSIT_OLD_ACTIVITY_OPEN:
+                    case WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN:
                     case WindowManager.TRANSIT_OLD_TASK_OPEN:
                     case WindowManager.TRANSIT_OLD_TASK_TO_FRONT:
                     case WindowManager.TRANSIT_OLD_WALLPAPER_OPEN:
@@ -733,8 +796,9 @@
         }
 
         void onWindowTransition(WindowState windowState, int transition) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".onWindowTransition",
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".onWindowTransition",
+                        FLAGS_MAGNIFICATION_CALLBACK,
                         "windowState={" + windowState + "}; transition=" + transition);
             }
             if (DEBUG_WINDOW_TRANSITIONS) {
@@ -791,18 +855,18 @@
         }
 
         void onImeSurfaceShownChanged(boolean shown) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(
-                        LOG_TAG + ".onImeSurfaceShownChanged", "shown=" + shown);
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".onImeSurfaceShownChanged",
+                        FLAGS_MAGNIFICATION_CALLBACK, "shown=" + shown);
             }
             mHandler.obtainMessage(MyHandler.MESSAGE_NOTIFY_IME_WINDOW_VISIBILITY_CHANGED,
                     shown ? 1 : 0, 0).sendToTarget();
         }
 
         MagnificationSpec getMagnificationSpecForWindow(WindowState windowState) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".getMagnificationSpecForWindow",
-                        "windowState={" + windowState + "}");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".getMagnificationSpecForWindow",
+                        FLAGS_MAGNIFICATION_CALLBACK, "windowState={" + windowState + "}");
             }
             MagnificationSpec spec = mMagnifedViewport.getMagnificationSpec();
             if (spec != null && !spec.isNop()) {
@@ -814,8 +878,9 @@
         }
 
         void getMagnificationRegion(Region outMagnificationRegion) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".getMagnificationRegion",
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".getMagnificationRegion",
+                        FLAGS_MAGNIFICATION_CALLBACK,
                         "outMagnificationRegion={" + outMagnificationRegion + "}");
             }
             // Make sure we're working with the most current bounds
@@ -824,25 +889,26 @@
         }
 
         void destroy() {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".destroy");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".destroy", FLAGS_MAGNIFICATION_CALLBACK);
             }
             mMagnifedViewport.destroyWindow();
         }
 
         // Can be called outside of a surface transaction
         void showMagnificationBoundsIfNeeded() {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".showMagnificationBoundsIfNeeded");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".showMagnificationBoundsIfNeeded",
+                        FLAGS_MAGNIFICATION_CALLBACK);
             }
             mHandler.obtainMessage(MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)
                     .sendToTarget();
         }
 
         void drawMagnifiedRegionBorderIfNeeded(SurfaceControl.Transaction t) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded",
-                        "transition={" + t + "}");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded",
+                        FLAGS_MAGNIFICATION_CALLBACK, "transition={" + t + "}");
             }
             mMagnifedViewport.drawWindowIfNeeded(t);
         }
@@ -887,7 +953,8 @@
 
                 if (mDisplayContext.getResources().getConfiguration().isScreenRound()) {
                     mCircularPath = new Path();
-                    mDisplay.getRealSize(mScreenSize);
+
+                    getDisplaySizeLocked(mScreenSize);
                     final int centerXY = mScreenSize.x / 2;
                     mCircularPath.addCircle(centerXY, centerXY, centerXY, Path.Direction.CW);
                 } else {
@@ -917,7 +984,7 @@
             }
 
             void recomputeBounds() {
-                mDisplay.getRealSize(mScreenSize);
+                getDisplaySizeLocked(mScreenSize);
                 final int screenWidth = mScreenSize.x;
                 final int screenHeight = mScreenSize.y;
 
@@ -1052,9 +1119,10 @@
                         || windowType == TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
             }
 
-            void onRotationChanged() {
+            void onDisplaySizeChanged() {
                 // If we are showing the magnification border, hide it immediately so
-                // the user does not see strange artifacts during rotation. The screenshot
+                // the user does not see strange artifacts during display size changed caused by
+                // rotation or folding/unfolding the device. In the rotation case, the screenshot
                 // used for rotation already has the border. After the rotation is complete
                 // we will show the border.
                 if (isMagnifying() || isForceShowingMagnifiableBounds()) {
@@ -1112,6 +1180,12 @@
                 }, false /* traverseTopToBottom */ );
             }
 
+            private void getDisplaySizeLocked(Point outSize) {
+                final Rect bounds =
+                        mDisplayContent.getConfiguration().windowConfiguration.getBounds();
+                outSize.set(bounds.width(), bounds.height());
+            }
+
             void dump(PrintWriter pw, String prefix) {
                 mWindow.dump(pw, prefix);
             }
@@ -1155,7 +1229,7 @@
                     final SurfaceControl.Transaction t = mService.mTransactionFactory.get();
                     final int layer =
                             mService.mPolicy.getWindowLayerFromTypeLw(TYPE_MAGNIFICATION_OVERLAY) *
-                                    WindowManagerService.TYPE_LAYER_MULTIPLIER;
+                                    WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER;
                     t.setLayer(mSurfaceControl, layer).setPosition(mSurfaceControl, 0, 0);
                     InputMonitor.setTrustedOverlayInputInfo(mSurfaceControl, t,
                             mDisplayContent.getDisplayId(), "Magnification Overlay");
@@ -1226,7 +1300,7 @@
 
                 void updateSize() {
                     synchronized (mService.mGlobalLock) {
-                        mDisplay.getRealSize(mScreenSize);
+                        getDisplaySizeLocked(mScreenSize);
                         mBlastBufferQueue.update(mSurfaceControl, mScreenSize.x, mScreenSize.y,
                                 PixelFormat.RGBA_8888);
                         invalidate(mDirtyRect);
@@ -1365,7 +1439,7 @@
             public static final int MESSAGE_NOTIFY_MAGNIFICATION_REGION_CHANGED = 1;
             public static final int MESSAGE_NOTIFY_RECTANGLE_ON_SCREEN_REQUESTED = 2;
             public static final int MESSAGE_NOTIFY_USER_CONTEXT_CHANGED = 3;
-            public static final int MESSAGE_NOTIFY_ROTATION_CHANGED = 4;
+            public static final int MESSAGE_NOTIFY_DISPLAY_SIZE_CHANGED = 4;
             public static final int MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED = 5;
             public static final int MESSAGE_NOTIFY_IME_WINDOW_VISIBILITY_CHANGED = 6;
 
@@ -1397,9 +1471,8 @@
                         mCallbacks.onUserContextChanged();
                     } break;
 
-                    case MESSAGE_NOTIFY_ROTATION_CHANGED: {
-                        final int rotation = message.arg1;
-                        mCallbacks.onRotationChanged(rotation);
+                    case MESSAGE_NOTIFY_DISPLAY_SIZE_CHANGED: {
+                        mCallbacks.onDisplaySizeChanged();
                     } break;
 
                     case MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED : {
@@ -1482,7 +1555,7 @@
 
         private final Handler mHandler;
 
-        private final AccessibilityTracing mAccessibilityTracing;
+        private final AccessibilityControllerInternalImpl mAccessibilityTracing;
 
         private final WindowsForAccessibilityCallback mCallback;
 
@@ -1502,24 +1575,26 @@
             mCallback = callback;
             mDisplayId = displayId;
             mHandler = new MyHandler(mService.mH.getLooper());
-            mAccessibilityTracing = AccessibilityTracing.getInstance(mService);
+            mAccessibilityTracing =
+                    AccessibilityController.getAccessibilityControllerInternal(mService);
             mRecurringAccessibilityEventsIntervalMillis = ViewConfiguration
                     .getSendRecurringAccessibilityEventsInterval();
             computeChangedWindows(true);
         }
 
         void performComputeChangedWindows(boolean forceSend) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".performComputeChangedWindows",
-                        "forceSend=" + forceSend);
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".performComputeChangedWindows",
+                        FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK, "forceSend=" + forceSend);
             }
             mHandler.removeMessages(MyHandler.MESSAGE_COMPUTE_CHANGED_WINDOWS);
             computeChangedWindows(forceSend);
         }
 
         void scheduleComputeChangedWindows() {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(LOG_TAG + ".scheduleComputeChangedWindows");
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".scheduleComputeChangedWindows",
+                        FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK);
             }
             if (!mHandler.hasMessages(MyHandler.MESSAGE_COMPUTE_CHANGED_WINDOWS)) {
                 mHandler.sendEmptyMessageDelayed(MyHandler.MESSAGE_COMPUTE_CHANGED_WINDOWS,
@@ -1542,6 +1617,13 @@
             mEmbeddedDisplayIdList.add(displayId);
         }
 
+        void notifyDisplayReparented(int embeddedDisplayId) {
+            // Notifies the A11y framework the display is reparented and
+            // becomes an embedded display for removing the un-used
+            // displayWindowObserver of this embedded one.
+            mCallback.onDisplayReparented(embeddedDisplayId);
+        }
+
         boolean shellRootIsAbove(WindowState windowState, ShellRoot shellRoot) {
             int wsLayer = mService.mPolicy.getWindowLayerLw(windowState);
             int shellLayer = mService.mPolicy.getWindowLayerFromTypeLw(shellRoot.getWindowType(),
@@ -1594,9 +1676,9 @@
          * @param forceSend Send the windows the accessibility even if they haven't changed.
          */
         void computeChangedWindows(boolean forceSend) {
-            if (mAccessibilityTracing.isEnabled()) {
-                mAccessibilityTracing.logState(
-                        LOG_TAG + ".computeChangedWindows", "forceSend=" + forceSend);
+            if (mAccessibilityTracing.isTracingEnabled(FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK)) {
+                mAccessibilityTracing.logTrace(LOG_TAG + ".computeChangedWindows",
+                        FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK, "forceSend=" + forceSend);
             }
             if (DEBUG) {
                 Slog.i(LOG_TAG, "computeChangedWindows()");
@@ -1645,7 +1727,7 @@
                 boolean focusedWindowAdded = false;
 
                 final int visibleWindowCount = visibleWindows.size();
-                HashSet<Integer> skipRemainingWindowsForTasks = new HashSet<>();
+                ArrayList<TaskFragment> skipRemainingWindowsForTaskFragments = new ArrayList<>();
 
                 ArrayList<ShellRoot> shellRoots = getSortedShellRoots(dc.mShellRoots);
 
@@ -1667,10 +1749,10 @@
                     computeWindowRegionInScreen(windowState, regionInScreen);
 
                     if (windowMattersToAccessibility(windowState, regionInScreen, unaccountedSpace,
-                            skipRemainingWindowsForTasks)) {
+                            skipRemainingWindowsForTaskFragments)) {
                         addPopulatedWindowInfo(windowState, regionInScreen, windows, addedWindows);
                         updateUnaccountedSpace(windowState, regionInScreen, unaccountedSpace,
-                                skipRemainingWindowsForTasks);
+                                skipRemainingWindowsForTaskFragments);
                         focusedWindowAdded |= windowState.isFocused();
                     } else if (isUntouchableNavigationBar(windowState, mTempRegion1)) {
                         // If this widow is navigation bar without touchable region, accounting the
@@ -1726,7 +1808,7 @@
 
         private boolean windowMattersToAccessibility(WindowState windowState,
                 Region regionInScreen, Region unaccountedSpace,
-                HashSet<Integer> skipRemainingWindowsForTasks) {
+                ArrayList<TaskFragment> skipRemainingWindowsForTaskFragments) {
             final RecentsAnimationController controller = mService.getRecentsAnimationController();
             if (controller != null && controller.shouldIgnoreForAccessibility(windowState)) {
                 return false;
@@ -1737,8 +1819,9 @@
             }
 
             // If the window is part of a task that we're finished with - ignore.
-            final Task task = windowState.getTask();
-            if (task != null && skipRemainingWindowsForTasks.contains(task.mTaskId)) {
+            final TaskFragment taskFragment = windowState.getTaskFragment();
+            if (taskFragment != null
+                    && skipRemainingWindowsForTaskFragments.contains(taskFragment)) {
                 return false;
             }
 
@@ -1764,7 +1847,8 @@
         }
 
         private void updateUnaccountedSpace(WindowState windowState, Region regionInScreen,
-                Region unaccountedSpace, HashSet<Integer> skipRemainingWindowsForTasks) {
+                Region unaccountedSpace,
+                ArrayList<TaskFragment> skipRemainingWindowsForTaskFragments) {
             if (windowState.mAttrs.type
                     != WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY) {
 
@@ -1794,11 +1878,11 @@
                                 Region.Op.REVERSE_DIFFERENCE);
                     }
 
-                    final Task task = windowState.getTask();
-                    if (task != null) {
+                    final TaskFragment taskFragment = windowState.getTaskFragment();
+                    if (taskFragment != null) {
                         // If the window is associated with a particular task, we can skip the
                         // rest of the windows for that task.
-                        skipRemainingWindowsForTasks.add(task.mTaskId);
+                        skipRemainingWindowsForTaskFragments.add(taskFragment);
                     } else if (!windowState.hasTapExcludeRegion()) {
                         // If the window is not associated with a particular task, then it is
                         // globally modal. In this case we can skip all remaining windows when
@@ -1945,8 +2029,8 @@
     private static final class AccessibilityControllerInternalImpl
             implements AccessibilityControllerInternal {
 
-        private static AccessibilityControllerInternal sInstance;
-        static AccessibilityControllerInternal getInstance(WindowManagerService service) {
+        private static AccessibilityControllerInternalImpl sInstance;
+        static AccessibilityControllerInternalImpl getInstance(WindowManagerService service) {
             synchronized (STATIC_LOCK) {
                 if (sInstance == null) {
                     sInstance = new AccessibilityControllerInternalImpl(service);
@@ -1956,18 +2040,23 @@
         }
 
         private final AccessibilityTracing mTracing;
+        private volatile long mEnabledTracingFlags;
+
         private AccessibilityControllerInternalImpl(WindowManagerService service) {
             mTracing = AccessibilityTracing.getInstance(service);
+            mEnabledTracingFlags = 0L;
         }
 
         @Override
-        public void startTrace() {
+        public void startTrace(long loggingTypes) {
+            mEnabledTracingFlags = loggingTypes;
             mTracing.startTrace();
         }
 
         @Override
         public void stopTrace() {
             mTracing.stopTrace();
+            mEnabledTracingFlags = 0L;
         }
 
         @Override
@@ -1975,19 +2064,37 @@
             return mTracing.isEnabled();
         }
 
-        @Override
-        public void logTrace(
-                String where, String callingParams, byte[] a11yDump, int callingUid,
-                StackTraceElement[] stackTrace) {
-            mTracing.logState(where, callingParams, a11yDump, callingUid, stackTrace);
+        boolean isTracingEnabled(long flags) {
+            return (flags & mEnabledTracingFlags) != 0L;
+        }
+
+        void logTrace(String where, long loggingTypes) {
+            logTrace(where, loggingTypes, "");
+        }
+
+        void logTrace(String where, long loggingTypes, String callingParams) {
+            logTrace(where, loggingTypes, callingParams, "".getBytes(), Binder.getCallingUid());
+        }
+
+        void logTrace(String where, long loggingTypes, String callingParams, byte[] a11yDump,
+                int callingUid) {
+            mTracing.logState(where, loggingTypes, callingParams, a11yDump, callingUid,
+                    new HashSet<String>(Arrays.asList("logTrace")));
         }
 
         @Override
-        public void logTrace(
-                String where, String callingParams, byte[] a11yDump, int callingUid,
-                StackTraceElement[] callStack, long timeStamp, int processId, long threadId) {
-            mTracing.logState(where, callingParams, a11yDump, callingUid, callStack, timeStamp,
-                    processId, threadId);
+        public void logTrace(String where, long loggingTypes, String callingParams, byte[] a11yDump,
+                int callingUid, StackTraceElement[] stackTrace, Set<String> ignoreStackEntries) {
+            mTracing.logState(where, loggingTypes, callingParams, a11yDump, callingUid, stackTrace,
+                    ignoreStackEntries);
+        }
+
+        @Override
+        public void logTrace(String where, long loggingTypes, String callingParams, byte[] a11yDump,
+                int callingUid, StackTraceElement[] callStack, long timeStamp, int processId,
+                long threadId, Set<String> ignoreStackEntries) {
+            mTracing.logState(where, loggingTypes, callingParams, a11yDump, callingUid, callStack,
+                    timeStamp, processId, threadId, ignoreStackEntries);
         }
     }
 
@@ -2003,8 +2110,7 @@
         }
 
         private static final int BUFFER_CAPACITY = 1024 * 1024 * 12;
-        private static final String TRACE_FILENAME = "/data/misc/a11ytrace/a11y_trace.pb";
-        private static final String TRACE_DIRECTORY = "/data/misc/a11ytrace/";
+        private static final String TRACE_FILENAME = "/data/misc/a11ytrace/a11y_trace" + WINSCOPE_EXT;
         private static final String TAG = "AccessibilityTracing";
         private static final long MAGIC_NUMBER_VALUE =
                 ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
@@ -2034,13 +2140,6 @@
                 return;
             }
             synchronized (mLock) {
-                try {
-                    Files.createDirectories(Paths.get(TRACE_DIRECTORY));
-                    mTraceFile.createNewFile();
-                } catch (Exception e) {
-                    Slog.e(TAG, "Error: Failed to create trace file.");
-                    return;
-                }
                 mEnabled = true;
                 mBuffer.resetBuffer();
             }
@@ -2071,86 +2170,127 @@
         /**
          * Write an accessibility trace log entry.
          */
-        void logState(String where) {
+        void logState(String where, long loggingTypes) {
             if (!mEnabled) {
                 return;
             }
-            logState(where, "");
+            logState(where, loggingTypes, "");
         }
 
         /**
          * Write an accessibility trace log entry.
          */
-        void logState(String where, String callingParams) {
+        void logState(String where, long loggingTypes, String callingParams) {
             if (!mEnabled) {
                 return;
             }
-            logState(where, callingParams, "".getBytes());
+            logState(where, loggingTypes, callingParams, "".getBytes());
         }
 
         /**
          * Write an accessibility trace log entry.
          */
-        void logState(String where, String callingParams, byte[] a11yDump) {
+        void logState(String where, long loggingTypes, String callingParams, byte[] a11yDump) {
             if (!mEnabled) {
                 return;
             }
-            logState(where, callingParams, a11yDump, Binder.getCallingUid());
+            logState(where, loggingTypes, callingParams, a11yDump, Binder.getCallingUid(),
+                    new HashSet<String>(Arrays.asList("logState")));
         }
 
         /**
          * Write an accessibility trace log entry.
          */
-        void logState(
-                String where, String callingParams, byte[] a11yDump, int callingUid) {
+        void logState(String where, long loggingTypes, String callingParams, byte[] a11yDump,
+                int callingUid, Set<String> ignoreStackEntries) {
             if (!mEnabled) {
                 return;
             }
             StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
-
-            logState(where, callingParams, a11yDump, callingUid, stackTraceElements);
+            ignoreStackEntries.add("logState");
+            logState(where, loggingTypes, callingParams, a11yDump, callingUid, stackTraceElements,
+                    ignoreStackEntries);
         }
 
         /**
          * Write an accessibility trace log entry.
          */
-        void logState(String where, String callingParams, byte[] a11yDump, int callingUid,
-                StackTraceElement[] stackTrace) {
+        void logState(String where, long loggingTypes, String callingParams, byte[] a11yDump,
+                int callingUid, StackTraceElement[] stackTrace, Set<String> ignoreStackEntries) {
             if (!mEnabled) {
                 return;
             }
-
-            log(where, callingParams, a11yDump, callingUid, stackTrace,
+            log(where, loggingTypes, callingParams, a11yDump, callingUid, stackTrace,
                     SystemClock.elapsedRealtimeNanos(),
                     Process.myPid() + ":" + Application.getProcessName(),
-                    Thread.currentThread().getId() + ":" + Thread.currentThread().getName());
+                    Thread.currentThread().getId() + ":" + Thread.currentThread().getName(),
+                    ignoreStackEntries);
         }
 
         /**
          * Write an accessibility trace log entry.
          */
-        void logState(String where, String callingParams, byte[] a11yDump, int callingUid,
-                StackTraceElement[] callingStack, long timeStamp, int processId, long threadId) {
+        void logState(String where, long loggingTypes, String callingParams, byte[] a11yDump,
+                int callingUid, StackTraceElement[] callingStack, long timeStamp, int processId,
+                long threadId, Set<String> ignoreStackEntries) {
             if (!mEnabled) {
                 return;
             }
-            log(where, callingParams, a11yDump, callingUid, callingStack, timeStamp,
-                    String.valueOf(processId), String.valueOf(threadId));
+            log(where, loggingTypes, callingParams, a11yDump, callingUid, callingStack, timeStamp,
+                    String.valueOf(processId), String.valueOf(threadId), ignoreStackEntries);
         }
 
-        private  String toStackTraceString(StackTraceElement[] stackTraceElements) {
+        private  String toStackTraceString(StackTraceElement[] stackTraceElements,
+                Set<String> ignoreStackEntries) {
+
             if (stackTraceElements == null) {
                 return "";
             }
+
             StringBuilder stringBuilder = new StringBuilder();
-            boolean skip = true;
-            for (int i = 0; i < stackTraceElements.length; i++) {
-                if (stackTraceElements[i].toString().contains(
-                            AccessibilityTracing.class.getSimpleName())) {
-                    skip = false;
-                } else if (!skip) {
-                    stringBuilder.append(stackTraceElements[i].toString()).append("\n");
+            int i = 0;
+
+            // Skip the first a few elements until after any ignoreStackEntries
+            int firstMatch = -1;
+            while (i < stackTraceElements.length) {
+                for (String ele : ignoreStackEntries) {
+                    if (stackTraceElements[i].toString().contains(ele)) {
+                        // found the first stack element containing the ignorable stack entries
+                        firstMatch = i;
+                        break;
+                    }
                 }
+                if (firstMatch < 0) {
+                    // Haven't found the first match yet, continue
+                    i++;
+                } else {
+                    break;
+                }
+            }
+            int lastMatch = firstMatch;
+            if (i < stackTraceElements.length) {
+                i++;
+                // Found the first match. Now look for the last match.
+                while (i < stackTraceElements.length) {
+                    for (String ele : ignoreStackEntries) {
+                        if (stackTraceElements[i].toString().contains(ele)) {
+                            // This is a match. Look at the next stack element.
+                            lastMatch = i;
+                            break;
+                        }
+                    }
+                    if (lastMatch != i) {
+                        // Found a no-match.
+                        break;
+                    }
+                    i++;
+                }
+            }
+
+            i = lastMatch + 1;
+            while (i < stackTraceElements.length) {
+                stringBuilder.append(stackTraceElements[i].toString()).append("\n");
+                i++;
             }
             return stringBuilder.toString();
         }
@@ -2158,19 +2298,22 @@
         /**
          * Write the current state to the buffer
          */
-        private void log(String where, String callingParams, byte[] a11yDump, int callingUid,
-                StackTraceElement[] callingStack, long timeStamp, String processName,
-                String threadName) {
+        private void log(String where, long loggingTypes, String callingParams, byte[] a11yDump,
+                int callingUid, StackTraceElement[] callingStack, long timeStamp,
+                String processName, String threadName, Set<String> ignoreStackEntries) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = timeStamp;
-            args.arg2 = where;
-            args.arg3 = processName;
-            args.arg4 = threadName;
-            args.arg5 = callingUid;
-            args.arg6 = callingParams;
-            args.arg7 = callingStack;
-            args.arg8 = a11yDump;
-            mHandler.obtainMessage(LogHandler.MESSAGE_LOG_TRACE_ENTRY, args).sendToTarget();
+            args.arg2 = loggingTypes;
+            args.arg3 = where;
+            args.arg4 = processName;
+            args.arg5 = threadName;
+            args.arg6 = ignoreStackEntries;
+            args.arg7 = callingParams;
+            args.arg8 = callingStack;
+            args.arg9 = a11yDump;
+
+            mHandler.obtainMessage(
+                    LogHandler.MESSAGE_LOG_TRACE_ENTRY, callingUid, 0, args).sendToTarget();
         }
 
         /**
@@ -2199,8 +2342,6 @@
                                     LocalServices.getService(PackageManagerInternal.class);
 
                             long tokenOuter = os.start(ENTRY);
-                            String callingStack =
-                                    toStackTraceString((StackTraceElement[]) args.arg7);
 
                             long reportedTimeStampNanos = (long) args.arg1;
                             long currentElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
@@ -2213,13 +2354,25 @@
 
                             os.write(ELAPSED_REALTIME_NANOS, reportedTimeStampNanos);
                             os.write(CALENDAR_TIME, fm.format(reportedTimeMillis).toString());
-                            os.write(WHERE, (String) args.arg2);
-                            os.write(PROCESS_NAME, (String) args.arg3);
-                            os.write(THREAD_ID_NAME, (String) args.arg4);
-                            os.write(CALLING_PKG, pmInternal.getNameForUid((int) args.arg5));
-                            os.write(CALLING_PARAMS, (String) args.arg6);
+
+                            long loggingTypes = (long) args.arg2;
+                            List<String> loggingTypeNames =
+                                    AccessibilityTrace.getNamesOfLoggingTypes(loggingTypes);
+
+                            for (String type : loggingTypeNames) {
+                                os.write(LOGGING_TYPE, type);
+                            }
+                            os.write(WHERE, (String) args.arg3);
+                            os.write(PROCESS_NAME, (String) args.arg4);
+                            os.write(THREAD_ID_NAME, (String) args.arg5);
+                            os.write(CALLING_PKG, pmInternal.getNameForUid(message.arg1));
+                            os.write(CALLING_PARAMS, (String) args.arg7);
+
+                            String callingStack = toStackTraceString(
+                                    (StackTraceElement[]) args.arg8, (Set<String>) args.arg6);
+
                             os.write(CALLING_STACKS, callingStack);
-                            os.write(ACCESSIBILITY_SERVICE, (byte[]) args.arg8);
+                            os.write(ACCESSIBILITY_SERVICE, (byte[]) args.arg9);
 
                             long tokenInner = os.start(WINDOW_MANAGER_SERVICE);
                             synchronized (mService.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index e02e867..ee72fc8 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -30,6 +30,11 @@
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IMMERSIVE;
+import static com.android.server.wm.ActivityRecord.State.DESTROYED;
+import static com.android.server.wm.ActivityRecord.State.DESTROYING;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
@@ -37,10 +42,9 @@
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
 import static com.android.server.wm.ActivityTaskManagerService.TAG_SWITCH;
 import static com.android.server.wm.ActivityTaskManagerService.enforceNotIsolatedCaller;
-import static com.android.server.wm.Task.ActivityState.DESTROYED;
-import static com.android.server.wm.Task.ActivityState.DESTROYING;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
@@ -72,6 +76,7 @@
 import android.util.Slog;
 import android.view.RemoteAnimationDefinition;
 import android.window.SizeConfigurationBuckets;
+import android.window.TransitionInfo;
 
 import com.android.internal.app.AssistUtils;
 import com.android.internal.policy.IKeyguardDismissCallback;
@@ -193,7 +198,7 @@
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "activityStopped");
             r = ActivityRecord.isInRootTaskLocked(token);
             if (r != null) {
-                if (r.attachedToProcess() && r.isState(Task.ActivityState.RESTARTING_PROCESS)) {
+                if (r.attachedToProcess() && r.isState(RESTARTING_PROCESS)) {
                     // The activity was requested to restart from
                     // {@link #restartActivityProcessIfVisible}.
                     restartingName = r.app.mName;
@@ -540,6 +545,29 @@
     }
 
     @Override
+    @Nullable
+    public IBinder getActivityTokenBelow(IBinder activityToken) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                final ActivityRecord ar = ActivityRecord.isInAnyTask(activityToken);
+                if (ar == null) {
+                    return null;
+                }
+                // Exclude finishing activity.
+                final ActivityRecord below = ar.getTask().getActivity((r) -> !r.finishing,
+                        ar, false /*includeBoundary*/, true /*traverseTopToBottom*/);
+                if (below != null && below.getUid() == ar.getUid()) {
+                    return below.appToken.asBinder();
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+        return null;
+    }
+
+    @Override
     public ComponentName getCallingActivity(IBinder token) {
         synchronized (mGlobalLock) {
             final ActivityRecord r = getCallingRecord(token);
@@ -1040,10 +1068,14 @@
         final long origId = Binder.clearCallingIdentity();
         synchronized (mGlobalLock) {
             final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
-            if (r != null && r.isState(Task.ActivityState.RESUMED, Task.ActivityState.PAUSING)) {
+            if (r != null && r.isState(RESUMED, PAUSING)) {
                 r.mDisplayContent.mAppTransition.overridePendingAppTransition(
                         packageName, enterAnim, exitAnim, null, null,
                         r.mOverrideTaskTransition);
+                r.mTransitionController.setOverrideAnimation(
+                        TransitionInfo.AnimationOptions.makeCustomAnimOptions(packageName,
+                                enterAnim, exitAnim, r.mOverrideTaskTransition),
+                        null /* startCallback */, null /* finishCallback */);
             }
         }
         Binder.restoreCallingIdentity(origId);
@@ -1183,7 +1215,7 @@
         try {
             final Intent baseActivityIntent;
             final boolean launchedFromHome;
-
+            final boolean isLastRunningActivity;
             synchronized (mGlobalLock) {
                 final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
                 if (r == null) return;
@@ -1195,22 +1227,25 @@
                     return;
                 }
 
-                final Intent baseIntent = r.getTask().getBaseIntent();
-                final boolean activityIsBaseActivity = baseIntent != null
-                        && r.mActivityComponent.equals(baseIntent.getComponent());
-                baseActivityIntent = activityIsBaseActivity ? r.intent : null;
+                final Task task = r.getTask();
+                isLastRunningActivity = task.topRunningActivity() == r;
+
+                final boolean isBaseActivity = r.mActivityComponent.equals(task.realActivity);
+                baseActivityIntent = isBaseActivity ? r.intent : null;
+
                 launchedFromHome = r.isLaunchSourceType(ActivityRecord.LAUNCH_SOURCE_TYPE_HOME);
             }
 
             // If the activity is one of the main entry points for the application, then we should
             // refrain from finishing the activity and instead move it to the back to keep it in
             // memory. The requirements for this are:
-            //   1. The current activity is the base activity for the task.
-            //   2. a. If the activity was launched by the home process, we trust that its intent
+            //   1. The activity is the last running activity in the task.
+            //   2. The current activity is the base activity for the task.
+            //   3. a. If the activity was launched by the home process, we trust that its intent
             //         was resolved, so we check if the it is a main intent for the application.
             //      b. Otherwise, we query Package Manager to verify whether the activity is a
             //         launcher activity for the application.
-            if (baseActivityIntent != null
+            if (baseActivityIntent != null && isLastRunningActivity
                     && ((launchedFromHome && ActivityRecord.isMainIntent(baseActivityIntent))
                         || isLauncherActivity(baseActivityIntent.getComponent()))) {
                 moveActivityTaskToBack(token, false /* nonRoot */);
diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java
new file mode 100644
index 0000000..1c2333a
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callback to intercept activity starts and possibly block/redirect them.
+ */
+public abstract class ActivityInterceptorCallback {
+    /**
+     * Intercept the launch intent based on various signals. If an interception happened, returns
+     * a new/existing non-null {@link Intent} which may redirect to another activity.
+     *
+     * @return null if no interception occurred, or a non-null intent which replaces the
+     * existing intent.
+     */
+    public abstract @Nullable Intent intercept(ActivityInterceptorInfo info);
+
+    /**
+     * The unique id of each interceptor which determines the order it will execute in.
+     */
+    @IntDef(suffix = { "_ORDERED_ID" }, value = {
+            FIRST_ORDERED_ID,
+            LAST_ORDERED_ID // Update this when adding new ids
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OrderedId {}
+
+    /**
+     * The first id, used by the framework to determine the valid range of ids.
+     */
+    static final int FIRST_ORDERED_ID = 0;
+
+    /**
+     * The final id, used by the framework to determine the valid range of ids. Update this when
+     * adding new ids.
+     */
+    static final int LAST_ORDERED_ID = FIRST_ORDERED_ID;
+
+    /**
+     * Data class for storing the various arguments needed for activity interception.
+     */
+    public static final class ActivityInterceptorInfo {
+        public final int realCallingUid;
+        public final int realCallingPid;
+        public final int userId;
+        public final String callingPackage;
+        public final String callingFeatureId;
+        public final Intent intent;
+        public final ResolveInfo rInfo;
+        public final ActivityInfo aInfo;
+        public final String resolvedType;
+        public final int callingPid;
+        public final int callingUid;
+        public final ActivityOptions checkedOptions;
+
+        public ActivityInterceptorInfo(int realCallingUid, int realCallingPid, int userId,
+                String callingPackage, String callingFeatureId, Intent intent,
+                ResolveInfo rInfo, ActivityInfo aInfo, String resolvedType, int callingPid,
+                int callingUid, ActivityOptions checkedOptions) {
+            this.realCallingUid = realCallingUid;
+            this.realCallingPid = realCallingPid;
+            this.userId = userId;
+            this.callingPackage = callingPackage;
+            this.callingFeatureId = callingFeatureId;
+            this.intent = intent;
+            this.rInfo = rInfo;
+            this.aInfo = aInfo;
+            this.resolvedType = resolvedType;
+            this.callingPid = callingPid;
+            this.callingUid = callingUid;
+            this.checkedOptions = checkedOptions;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 494f496..c715c39 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -59,6 +59,8 @@
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE;
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE;
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_WARM_LAUNCH;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
 import static com.android.server.am.MemoryStatUtil.MemoryStat;
 import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_METRICS;
@@ -89,6 +91,7 @@
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 
@@ -157,6 +160,9 @@
     private final ArrayList<TransitionInfo> mTransitionInfoList = new ArrayList<>();
     /** Map : Last launched activity => {@link TransitionInfo} */
     private final ArrayMap<ActivityRecord, TransitionInfo> mLastTransitionInfo = new ArrayMap<>();
+    /** SparseArray : Package UID => {@link PackageCompatStateInfo} */
+    private final SparseArray<PackageCompatStateInfo> mPackageUidToCompatStateInfo =
+            new SparseArray<>(0);
 
     private ArtManagerInternal mArtManagerInternal;
     private final StringBuilder mStringBuilder = new StringBuilder();
@@ -187,6 +193,10 @@
             return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.allDrawn();
         }
 
+        boolean hasActiveTransitionInfo() {
+            return mAssociatedTransitionInfo != null;
+        }
+
         boolean contains(ActivityRecord r) {
             return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.contains(r);
         }
@@ -300,9 +310,12 @@
                 return;
             }
             if (mLastLaunchedActivity != null) {
-                // Transfer the launch cookie because it is a consecutive launch event.
+                // Transfer the launch cookie and launch root task because it is a consecutive
+                // launch event.
                 r.mLaunchCookie = mLastLaunchedActivity.mLaunchCookie;
                 mLastLaunchedActivity.mLaunchCookie = null;
+                r.mLaunchRootTask = mLastLaunchedActivity.mLaunchRootTask;
+                mLastLaunchedActivity.mLaunchRootTask = null;
             }
             mLastLaunchedActivity = r;
             if (!r.noDisplay && !r.isReportedDrawn()) {
@@ -333,10 +346,11 @@
         }
 
         /** Only keep the records which can be drawn. */
-        void updatePendingDraw() {
+        void updatePendingDraw(boolean keepInitializing) {
             for (int i = mPendingDrawActivities.size() - 1; i >= 0; i--) {
                 final ActivityRecord r = mPendingDrawActivities.get(i);
-                if (!r.mVisibleRequested) {
+                if (!r.mVisibleRequested
+                        && !(keepInitializing && r.isState(ActivityRecord.State.INITIALIZING))) {
                     if (DEBUG_METRICS) Slog.i(TAG, "Discard pending draw " + r);
                     mPendingDrawActivities.remove(i);
                 }
@@ -444,6 +458,15 @@
         }
     }
 
+    /** Information about the App Compat state logging associated with a package UID . */
+    private static final class PackageCompatStateInfo {
+        /** All activities that have a visible state. */
+        final ArrayList<ActivityRecord> mVisibleActivities = new ArrayList<>();
+        /** The last logged state. */
+        int mLastLoggedState = APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+        @Nullable ActivityRecord mLastLoggedActivity;
+    }
+
     ActivityMetricsLogger(ActivityTaskSupervisor supervisor, Looper looper) {
         mLastLogTimeSecs = SystemClock.elapsedRealtime() / 1000;
         mSupervisor = supervisor;
@@ -630,6 +653,7 @@
             if (crossPackage) {
                 startLaunchTrace(info);
             }
+            scheduleCheckActivityToBeDrawnIfSleeping(launchedActivity);
             return;
         }
 
@@ -651,13 +675,7 @@
             // As abort for no process switch.
             launchObserverNotifyIntentFailed();
         }
-        if (launchedActivity.mDisplayContent.isSleeping()) {
-            // It is unknown whether the activity can be drawn or not, e.g. it depends on the
-            // keyguard states and the attributes or flags set by the activity. If the activity
-            // keeps invisible in the grace period, the tracker will be cancelled so it won't get
-            // a very long launch time that takes unlocking as the end of launch.
-            scheduleCheckActivityToBeDrawn(launchedActivity, UNKNOWN_VISIBILITY_CHECK_DELAY_MS);
-        }
+        scheduleCheckActivityToBeDrawnIfSleeping(launchedActivity);
 
         // If the previous transitions are no longer visible, abort them to avoid counting the
         // launch time when resuming from back stack. E.g. launch 2 independent tasks in a short
@@ -665,13 +683,23 @@
         // visible such as after the top task is finished.
         for (int i = mTransitionInfoList.size() - 2; i >= 0; i--) {
             final TransitionInfo prevInfo = mTransitionInfoList.get(i);
-            prevInfo.updatePendingDraw();
+            prevInfo.updatePendingDraw(false /* keepInitializing */);
             if (prevInfo.allDrawn()) {
                 abort(prevInfo, "nothing will be drawn");
             }
         }
     }
 
+    private void scheduleCheckActivityToBeDrawnIfSleeping(@NonNull ActivityRecord r) {
+        if (r.mDisplayContent.isSleeping()) {
+            // It is unknown whether the activity can be drawn or not, e.g. it depends on the
+            // keyguard states and the attributes or flags set by the activity. If the activity
+            // keeps invisible in the grace period, the tracker will be cancelled so it won't get
+            // a very long launch time that takes unlocking as the end of launch.
+            scheduleCheckActivityToBeDrawn(r, UNKNOWN_VISIBILITY_CHECK_DELAY_MS);
+        }
+    }
+
     /**
      * Notifies the tracker that all windows of the app have been drawn.
      *
@@ -690,6 +718,7 @@
         // Always calculate the delay because the caller may need to know the individual drawn time.
         info.mWindowsDrawnDelayMs = info.calculateDelay(timestampNs);
         info.removePendingDrawActivity(r);
+        info.updatePendingDraw(false /* keepInitializing */);
         final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info);
         if (info.mLoggedTransitionStarting && info.allDrawn()) {
             done(false /* abort */, info, "notifyWindowsDrawn - all windows drawn", timestampNs);
@@ -741,7 +770,9 @@
             info.mCurrentTransitionDelayMs = info.calculateDelay(timestampNs);
             info.mReason = activityToReason.valueAt(index);
             info.mLoggedTransitionStarting = true;
-            info.updatePendingDraw();
+            // Do not remove activity in initializing state because the transition may be started
+            // by starting window. The initializing activity may be requested to visible soon.
+            info.updatePendingDraw(true /* keepInitializing */);
             if (info.allDrawn()) {
                 done(false /* abort */, info, "notifyTransitionStarting - all windows drawn",
                         timestampNs);
@@ -759,6 +790,21 @@
     /** Makes sure that the reference to the removed activity is cleared. */
     void notifyActivityRemoved(@NonNull ActivityRecord r) {
         mLastTransitionInfo.remove(r);
+
+        final int packageUid = r.info.applicationInfo.uid;
+        final PackageCompatStateInfo compatStateInfo = mPackageUidToCompatStateInfo.get(packageUid);
+        if (compatStateInfo == null) {
+            return;
+        }
+
+        compatStateInfo.mVisibleActivities.remove(r);
+        if (compatStateInfo.mLastLoggedActivity == r) {
+            compatStateInfo.mLastLoggedActivity = null;
+        }
+        if (compatStateInfo.mVisibleActivities.isEmpty()) {
+            // No need to keep the entry if there are no visible activities.
+            mPackageUidToCompatStateInfo.remove(packageUid);
+        }
     }
 
     /**
@@ -775,7 +821,7 @@
             Slog.i(TAG, "notifyVisibilityChanged " + r + " visible=" + r.mVisibleRequested
                     + " state=" + r.getState() + " finishing=" + r.finishing);
         }
-        if (r.isState(Task.ActivityState.RESUMED) && r.mDisplayContent.isSleeping()) {
+        if (r.isState(ActivityRecord.State.RESUMED) && r.mDisplayContent.isSleeping()) {
             // The activity may be launching while keyguard is locked. The keyguard may be dismissed
             // after the activity finished relayout, so skip the visibility check to avoid aborting
             // the tracking of launch event.
@@ -1255,6 +1301,115 @@
                 memoryStat.swapInBytes);
     }
 
+    /**
+     * Logs the current App Compat state of the given {@link ActivityRecord} with its package
+     * UID, if all of the following hold:
+     * <ul>
+     *   <li>The current state is different than the last logged state for the package UID of the
+     *   activity.
+     *   <li>If the current state is NOT_VISIBLE, there is a previously logged state for the
+     *   package UID and there are no other visible activities with the same package UID.
+     *   <li>The last logged activity with the same package UID is either {@code activity} or the
+     *   last logged state is NOT_VISIBLE or NOT_LETTERBOXED.
+     * </ul>
+     *
+     * <p>If the current state is NOT_VISIBLE and the previous state which was logged by {@code
+     * activity} wasn't, looks for the first visible activity with the same package UID that has
+     * a letterboxed state, or a non-letterboxed state if there isn't one, and logs that state.
+     *
+     * <p>This method assumes that the caller is wrapping the call with a synchronized block so
+     * that there won't be a race condition between two activities with the same package.
+     */
+    void logAppCompatState(@NonNull ActivityRecord activity) {
+        final int packageUid = activity.info.applicationInfo.uid;
+        final int state = activity.getAppCompatState();
+
+        if (!mPackageUidToCompatStateInfo.contains(packageUid)) {
+            mPackageUidToCompatStateInfo.put(packageUid, new PackageCompatStateInfo());
+        }
+        final PackageCompatStateInfo compatStateInfo = mPackageUidToCompatStateInfo.get(packageUid);
+        final int lastLoggedState = compatStateInfo.mLastLoggedState;
+        final ActivityRecord lastLoggedActivity = compatStateInfo.mLastLoggedActivity;
+
+        final boolean isVisible = state != APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+        final ArrayList<ActivityRecord> visibleActivities = compatStateInfo.mVisibleActivities;
+        if (isVisible && !visibleActivities.contains(activity)) {
+            visibleActivities.add(activity);
+        } else if (!isVisible) {
+            visibleActivities.remove(activity);
+            if (visibleActivities.isEmpty()) {
+                // No need to keep the entry if there are no visible activities.
+                mPackageUidToCompatStateInfo.remove(packageUid);
+            }
+        }
+
+        if (state == lastLoggedState) {
+            // We don’t want to log the same state twice or log DEFAULT_NOT_VISIBLE before any
+            // visible state was logged.
+            return;
+        }
+
+        if (!isVisible && !visibleActivities.isEmpty()) {
+            // There is another visible activity for this package UID.
+            if (activity == lastLoggedActivity) {
+                // Make sure a new visible state is logged if needed.
+                findAppCompatStateToLog(compatStateInfo, packageUid);
+            }
+            return;
+        }
+
+        if (activity != lastLoggedActivity
+                && lastLoggedState != APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE
+                && lastLoggedState != APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED) {
+            // Another visible activity for this package UID has logged a letterboxed state.
+            return;
+        }
+
+        logAppCompatStateInternal(activity, state, packageUid, compatStateInfo);
+    }
+
+    /**
+     * Looks for the first visible activity in {@code compatStateInfo} that has a letterboxed
+     * state, or a non-letterboxed state if there isn't one, and logs that state for the given
+     * {@code packageUid}.
+     */
+    private void findAppCompatStateToLog(PackageCompatStateInfo compatStateInfo, int packageUid) {
+        final ArrayList<ActivityRecord> visibleActivities = compatStateInfo.mVisibleActivities;
+
+        ActivityRecord activityToLog = null;
+        int stateToLog = APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+        for (int i = 0; i < visibleActivities.size(); i++) {
+            ActivityRecord activity = visibleActivities.get(i);
+            int state = activity.getAppCompatState();
+            if (state == APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE) {
+                // This shouldn't happen.
+                Slog.w(TAG, "Visible activity with NOT_VISIBLE App Compat state for package UID: "
+                        + packageUid);
+                continue;
+            }
+            if (stateToLog == APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE || (
+                    stateToLog == APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED
+                            && state != APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED)) {
+                activityToLog = activity;
+                stateToLog = state;
+            }
+        }
+        if (activityToLog != null && stateToLog != APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE) {
+            logAppCompatStateInternal(activityToLog, stateToLog, packageUid, compatStateInfo);
+        }
+    }
+
+    private void logAppCompatStateInternal(@NonNull ActivityRecord activity, int state,
+            int packageUid, PackageCompatStateInfo compatStateInfo) {
+        compatStateInfo.mLastLoggedState = state;
+        compatStateInfo.mLastLoggedActivity = activity;
+        FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, packageUid, state);
+
+        if (DEBUG_METRICS) {
+            Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s)", packageUid, state));
+        }
+    }
+
     private ArtManagerInternal getArtManagerInternal() {
         if (mArtManagerInternal == null) {
             // Note that this may be null.
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d13c8ba..7365f88 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -46,8 +46,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.activityTypeToString;
 import static android.app.WindowConfiguration.isSplitScreenWindowingMode;
-import static android.app.servertransaction.TransferSplashScreenViewStateItem.ATTACH_TO;
-import static android.app.servertransaction.TransferSplashScreenViewStateItem.HANDOVER_TO;
 import static android.content.Intent.ACTION_MAIN;
 import static android.content.Intent.CATEGORY_HOME;
 import static android.content.Intent.CATEGORY_LAUNCHER;
@@ -127,8 +125,24 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SWITCH;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
+import static com.android.server.wm.ActivityRecord.State.DESTROYED;
+import static com.android.server.wm.ActivityRecord.State.DESTROYING;
+import static com.android.server.wm.ActivityRecord.State.FINISHING;
+import static com.android.server.wm.ActivityRecord.State.INITIALIZING;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STARTED;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 import static com.android.server.wm.ActivityRecordProto.ALL_DRAWN;
 import static com.android.server.wm.ActivityRecordProto.APP_STOPPED;
 import static com.android.server.wm.ActivityRecordProto.CLIENT_VISIBLE;
@@ -191,18 +205,7 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
-import static com.android.server.wm.Task.ActivityState.DESTROYED;
-import static com.android.server.wm.Task.ActivityState.DESTROYING;
-import static com.android.server.wm.Task.ActivityState.FINISHING;
-import static com.android.server.wm.Task.ActivityState.INITIALIZING;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
-import static com.android.server.wm.Task.ActivityState.RESTARTING_PROCESS;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
-import static com.android.server.wm.Task.ActivityState.STARTED;
-import static com.android.server.wm.Task.ActivityState.STOPPED;
-import static com.android.server.wm.Task.ActivityState.STOPPING;
-import static com.android.server.wm.Task.TASK_VISIBILITY_VISIBLE;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE;
 import static com.android.server.wm.TaskPersister.DEBUG;
 import static com.android.server.wm.TaskPersister.IMAGE_EXTENSION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
@@ -218,7 +221,6 @@
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
 import static com.android.server.wm.WindowState.LEGACY_POLICY_VISIBILITY;
 import static com.android.server.wm.WindowStateAnimator.HAS_DRAWN;
-import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_BEFORE_ANIM;
 
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
 import static org.xmlpull.v1.XmlPullParser.END_TAG;
@@ -273,13 +275,13 @@
 import android.os.Bundle;
 import android.os.Debug;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.os.storage.StorageManager;
 import android.service.contentcapture.ActivityEvent;
 import android.service.dreams.DreamActivity;
 import android.service.dreams.DreamManagerInternal;
@@ -303,6 +305,7 @@
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationTarget;
+import android.view.Surface.Rotation;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.WindowInsets.Type;
@@ -310,24 +313,24 @@
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.TransitionOldType;
 import android.view.animation.Animation;
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 import android.window.SizeConfigurationBuckets;
 import android.window.SplashScreen;
 import android.window.SplashScreenView;
 import android.window.SplashScreenView.SplashScreenViewParcelable;
 import android.window.TaskSnapshot;
+import android.window.TransitionInfo.AnimationOptions;
 import android.window.WindowContainerToken;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.ResolverActivity;
 import com.android.internal.content.ReferrerIntent;
+import com.android.internal.os.TransferPipe;
 import com.android.internal.policy.AttributeCache;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ToBooleanFunction;
 import com.android.internal.util.XmlUtils;
-import com.android.internal.util.function.pooled.PooledFunction;
-import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
 import com.android.server.am.AppTimeTracker;
 import com.android.server.am.PendingIntentRecord;
@@ -338,7 +341,6 @@
 import com.android.server.uri.UriPermissionOwner;
 import com.android.server.wm.ActivityMetricsLogger.TransitionInfoSnapshot;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
-import com.android.server.wm.Task.ActivityState;
 import com.android.server.wm.WindowManagerService.H;
 import com.android.server.wm.utils.InsetUtils;
 
@@ -347,6 +349,7 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
@@ -396,10 +399,6 @@
     private static final int STARTING_WINDOW_TYPE_SNAPSHOT = 1;
     private static final int STARTING_WINDOW_TYPE_SPLASH_SCREEN = 2;
 
-    /**
-     * Value to increment the z-layer when boosting a layer during animations. BOOST in l33tsp34k.
-     */
-    @VisibleForTesting static final int Z_BOOST_BASE = 800570000;
     static final int INVALID_PID = -1;
 
     // How long we wait until giving up on the last activity to pause.  This
@@ -484,13 +483,13 @@
     private ActivityOptions mPendingOptions;
     /** Non-null if {@link #mPendingOptions} specifies the remote animation. */
     private RemoteAnimationAdapter mPendingRemoteAnimation;
-    private IRemoteTransition mPendingRemoteTransition;
+    private RemoteTransition mPendingRemoteTransition;
     ActivityOptions returningOptions; // options that are coming back via convertToTranslucent
     AppTimeTracker appTimeTracker; // set if we are tracking the time in this app/task/activity
     ActivityServiceConnectionsHolder mServiceConnectionsHolder; // Service connections.
     UriPermissionOwner uriPermissions; // current special URI access perms.
     WindowProcessController app;      // if non-null, hosting application
-    private ActivityState mState;    // current state we are in
+    private State mState;    // current state we are in
     private Bundle mIcicle;         // last saved activity state
     private PersistableBundle mPersistentState; // last persistently saved activity state
     private boolean mHaveState = true; // Indicates whether the last saved state of activity is
@@ -540,11 +539,6 @@
     final ActivityTaskSupervisor mTaskSupervisor;
     final RootWindowContainer mRootWindowContainer;
 
-    static final int STARTING_WINDOW_NOT_SHOWN = 0;
-    static final int STARTING_WINDOW_SHOWN = 1;
-    static final int STARTING_WINDOW_REMOVED = 2;
-    int mStartingWindowState = STARTING_WINDOW_NOT_SHOWN;
-
     // Tracking splash screen status from previous activity
     boolean mSplashScreenStyleEmpty = false;
 
@@ -552,6 +546,21 @@
     static final int LAUNCH_SOURCE_TYPE_HOME = 2;
     static final int LAUNCH_SOURCE_TYPE_SYSTEMUI = 3;
     static final int LAUNCH_SOURCE_TYPE_APPLICATION = 4;
+
+    enum State {
+        INITIALIZING,
+        STARTED,
+        RESUMED,
+        PAUSING,
+        PAUSED,
+        STOPPING,
+        STOPPED,
+        FINISHING,
+        DESTROYING,
+        DESTROYED,
+        RESTARTING_PROCESS
+    }
+
     /**
      * The type of launch source.
      */
@@ -648,6 +657,14 @@
     boolean allDrawn;
     private boolean mLastAllDrawn;
 
+    /**
+     * Solely for reporting to ActivityMetricsLogger. Just tracks whether, the last time this
+     * Actiivty was part of a syncset, all windows were ready by the time the sync was ready (vs.
+     * only the top-occluding ones). The assumption here is if some were not ready, they were
+     * covered with starting-window/splash-screen.
+     */
+    boolean mLastAllReadyAtSync = false;
+
     private boolean mLastContainsShowWhenLockedWindow;
     private boolean mLastContainsDismissKeyguardWindow;
     private boolean mLastContainsTurnScreenOnWindow;
@@ -705,7 +722,7 @@
     // TODO: rename to mNoDisplay
     @VisibleForTesting
     boolean noDisplay;
-    boolean mShowForAllUsers;
+    final boolean mShowForAllUsers;
     // TODO: Make this final
     int mTargetSdk;
 
@@ -728,6 +745,13 @@
     boolean startingDisplayed;
     boolean startingMoved;
 
+    /**
+     * If it is non-null, it requires all activities who have the same starting data to be drawn
+     * to remove the starting window.
+     * TODO(b/189385912): Remove starting window related fields after migrating them to task.
+     */
+    private StartingData mSharedStartingData;
+
     boolean mHandleExitSplashScreen;
     @TransferSplashScreenState
     int mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE;
@@ -797,6 +821,7 @@
      */
     private final Configuration mTmpConfig = new Configuration();
     private final Rect mTmpBounds = new Rect();
+    private final Rect mTmpOutNonDecorBounds = new Rect();
 
     // Token for targeting this activity for assist purposes.
     final Binder assistToken = new Binder();
@@ -808,6 +833,9 @@
     // Tracking cookie for the launch of this activity and it's task.
     IBinder mLaunchCookie;
 
+    // Tracking indicated launch root in order to propagate it among trampoline activities.
+    WindowContainerToken mLaunchRootTask;
+
     // Entering PiP is usually done in two phases, we put the task into pinned mode first and
     // SystemUi sets the pinned mode on activity after transition is done.
     boolean mWaitForEnteringPinnedMode;
@@ -862,19 +890,6 @@
         }
     };
 
-    private static String startingWindowStateToString(int state) {
-        switch (state) {
-            case STARTING_WINDOW_NOT_SHOWN:
-                return "STARTING_WINDOW_NOT_SHOWN";
-            case STARTING_WINDOW_SHOWN:
-                return "STARTING_WINDOW_SHOWN";
-            case STARTING_WINDOW_REMOVED:
-                return "STARTING_WINDOW_REMOVED";
-            default:
-                return "unknown state=" + state;
-        }
-    }
-
     @Override
     void dump(PrintWriter pw, String prefix, boolean dumpAll) {
         final long now = SystemClock.uptimeMillis();
@@ -999,7 +1014,8 @@
             pw.println(mPendingRemoteAnimation.getCallingPid());
         }
         if (mPendingRemoteTransition != null) {
-            pw.print(prefix + " pendingRemoteTransition=" + mPendingRemoteTransition);
+            pw.print(prefix + " pendingRemoteTransition="
+                    + mPendingRemoteTransition.getRemoteTransition());
         }
         if (appTimeTracker != null) {
             appTimeTracker.dumpWithHeader(pw, prefix, false);
@@ -1018,6 +1034,11 @@
             pw.print("launchCookie=");
             pw.println(mLaunchCookie);
         }
+        if (mLaunchRootTask != null) {
+            pw.print(prefix);
+            pw.print("mLaunchRootTask=");
+            pw.println(mLaunchRootTask);
+        }
         pw.print(prefix); pw.print("mHaveState="); pw.print(mHaveState);
                 pw.print(" mIcicle="); pw.println(mIcicle);
         pw.print(prefix); pw.print("state="); pw.print(mState);
@@ -1026,9 +1047,7 @@
                 pw.print(" finishing="); pw.println(finishing);
         pw.print(prefix); pw.print("keysPaused="); pw.print(keysPaused);
                 pw.print(" inHistory="); pw.print(inHistory);
-                pw.print(" idle="); pw.print(idle);
-                pw.print(" mStartingWindowState=");
-                pw.println(startingWindowStateToString(mStartingWindowState));
+                pw.print(" idle="); pw.println(idle);
         pw.print(prefix); pw.print("occludesParent="); pw.print(occludesParent());
                 pw.print(" noDisplay="); pw.print(noDisplay);
                 pw.print(" immersive="); pw.print(immersive);
@@ -1073,6 +1092,9 @@
             pw.print(" firstWindowDrawn="); pw.print(firstWindowDrawn);
             pw.print(" mIsExiting="); pw.println(mIsExiting);
         }
+        if (mSharedStartingData != null) {
+            pw.println(prefix + "mSharedStartingData=" + mSharedStartingData);
+        }
         if (mStartingWindow != null || mStartingSurface != null
                 || startingDisplayed || startingMoved || mVisibleSetFromTransferredStartingWindow) {
             pw.print(prefix); pw.print("startingWindow="); pw.print(mStartingWindow);
@@ -1146,6 +1168,76 @@
         mLetterboxUiController.dump(pw, prefix);
     }
 
+    static boolean dumpActivity(FileDescriptor fd, PrintWriter pw, int index, ActivityRecord r,
+            String prefix, String label, boolean complete, boolean brief, boolean client,
+            String dumpPackage, boolean needNL, Runnable header, Task lastTask) {
+        if (dumpPackage != null && !dumpPackage.equals(r.packageName)) {
+            return false;
+        }
+
+        final boolean full = !brief && (complete || !r.isInHistory());
+        if (needNL) {
+            pw.println("");
+        }
+        if (header != null) {
+            header.run();
+        }
+
+        String innerPrefix = prefix + "  ";
+        String[] args = new String[0];
+        if (lastTask != r.getTask()) {
+            lastTask = r.getTask();
+            pw.print(prefix);
+            pw.print(full ? "* " : "  ");
+            pw.println(lastTask);
+            if (full) {
+                lastTask.dump(pw, prefix + "  ");
+            } else if (complete) {
+                // Complete + brief == give a summary.  Isn't that obvious?!?
+                if (lastTask.intent != null) {
+                    pw.print(prefix);
+                    pw.print("  ");
+                    pw.println(lastTask.intent.toInsecureString());
+                }
+            }
+        }
+        pw.print(prefix); pw.print(full ? "* " : "    "); pw.print(label);
+        pw.print(" #"); pw.print(index); pw.print(": ");
+        pw.println(r);
+        if (full) {
+            r.dump(pw, innerPrefix, true /* dumpAll */);
+        } else if (complete) {
+            // Complete + brief == give a summary.  Isn't that obvious?!?
+            pw.print(innerPrefix);
+            pw.println(r.intent.toInsecureString());
+            if (r.app != null) {
+                pw.print(innerPrefix);
+                pw.println(r.app);
+            }
+        }
+        if (client && r.attachedToProcess()) {
+            // flush anything that is already in the PrintWriter since the thread is going
+            // to write to the file descriptor directly
+            pw.flush();
+            try {
+                TransferPipe tp = new TransferPipe();
+                try {
+                    r.app.getThread().dumpActivity(
+                            tp.getWriteFd(), r.appToken, innerPrefix, args);
+                    // Short timeout, since blocking here can deadlock with the application.
+                    tp.go(fd, 2000);
+                } finally {
+                    tp.kill();
+                }
+            } catch (IOException e) {
+                pw.println(innerPrefix + "Failure while dumping the activity: " + e);
+            } catch (RemoteException e) {
+                pw.println(innerPrefix + "Got a RemoteException while dumping the activity");
+            }
+        }
+        return true;
+    }
+
     void setAppTimeTracker(AppTimeTracker att) {
         appTimeTracker = att;
     }
@@ -1255,15 +1347,8 @@
                 updatePictureInPictureMode(null, false);
             } else {
                 mLastReportedMultiWindowMode = inMultiWindowMode;
-                computeConfigurationAfterMultiWindowModeChange();
-                // If the activity is in stopping or stopped state, for instance, it's in the
-                // split screen task and not the top one, the last configuration it should keep
-                // is the one before multi-window mode change.
-                final ActivityState state = getState();
-                if (state != STOPPED && state != STOPPING) {
-                    ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS,
-                            true /* ignoreVisibility */);
-                }
+                ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS,
+                        false /* ignoreVisibility */);
             }
         }
     }
@@ -1282,33 +1367,48 @@
             // precede the configuration change from the resize.
             mLastReportedPictureInPictureMode = inPictureInPictureMode;
             mLastReportedMultiWindowMode = inPictureInPictureMode;
-            if (targetRootTaskBounds != null && !targetRootTaskBounds.isEmpty()) {
-                computeConfigurationAfterMultiWindowModeChange();
-            }
             ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS,
                     true /* ignoreVisibility */);
         }
     }
 
-    private void computeConfigurationAfterMultiWindowModeChange() {
-        final Configuration newConfig = new Configuration();
-        newConfig.setTo(task.getRequestedOverrideConfiguration());
-        Rect outBounds = newConfig.windowConfiguration.getBounds();
-        final Configuration parentConfig = task.getParent().getConfiguration();
-        task.adjustForMinimalTaskDimensions(outBounds, outBounds, parentConfig);
-        task.computeConfigResourceOverrides(newConfig, parentConfig);
-    }
-
     Task getTask() {
         return task;
     }
 
+    @Nullable
+    TaskFragment getTaskFragment() {
+        WindowContainer parent = getParent();
+        return parent != null ? parent.asTaskFragment() : null;
+    }
+
+    /** Whether we should prepare a transition for this {@link ActivityRecord} parent change. */
+    private boolean shouldStartChangeTransition(
+            @Nullable TaskFragment newParent, @Nullable TaskFragment oldParent) {
+        if (mWmService.mDisableTransitionAnimation
+                || mDisplayContent == null || newParent == null || oldParent == null
+                || getSurfaceControl() == null || !isVisible() || !isVisibleRequested()) {
+            return false;
+        }
+
+        // Transition change for the activity moving into a TaskFragment of different bounds.
+        return newParent.isOrganizedTaskFragment()
+                && !newParent.getBounds().equals(oldParent.getBounds());
+    }
+
     @Override
-    void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent) {
-        final Task oldTask = oldParent != null ? (Task) oldParent : null;
-        final Task newTask = newParent != null ? (Task) newParent : null;
+    void onParentChanged(ConfigurationContainer rawNewParent, ConfigurationContainer rawOldParent) {
+        final TaskFragment oldParent = (TaskFragment) rawOldParent;
+        final TaskFragment newParent = (TaskFragment) rawNewParent;
+        final Task oldTask = oldParent != null ? oldParent.getTask() : null;
+        final Task newTask = newParent != null ? newParent.getTask() : null;
         this.task = newTask;
 
+        if (shouldStartChangeTransition(newParent, oldParent)) {
+            // Animate change transition on TaskFragment level to get the correct window crop.
+            newParent.initializeChangeTransition(getBounds(), getSurfaceControl());
+        }
+
         super.onParentChanged(newParent, oldParent);
 
         if (isPersistable()) {
@@ -1364,11 +1464,19 @@
 
         updateColorTransform();
 
-        if (oldTask != null) {
-            oldTask.cleanUpActivityReferences(this);
+        if (oldParent != null) {
+            oldParent.cleanUpActivityReferences(this);
         }
-        if (newTask != null && isState(RESUMED)) {
-            newTask.setResumedActivity(this, "onParentChanged");
+
+        if (newParent != null && isState(RESUMED)) {
+            newParent.setResumedActivity(this, "onParentChanged");
+            if (mStartingWindow != null && mStartingData != null
+                    && mStartingData.mAssociatedTask == null && newParent.isEmbedded()) {
+                // The starting window should keep covering its task when the activity is
+                // reparented to a task fragment that may not fill the task bounds.
+                associateStartingDataWithTask();
+                attachStartingSurfaceToAssociatedTask();
+            }
             mImeInsetsFrozenUntilStartInput = false;
         }
 
@@ -1417,7 +1525,7 @@
         }
 
         // TODO(b/169035022): move to a more-appropriate place.
-        mAtmService.getTransitionController().collect(this);
+        mTransitionController.collect(this);
         if (prevDc.mOpeningApps.remove(this)) {
             // Transfer opening transition to new display.
             mDisplayContent.mOpeningApps.add(this);
@@ -1709,6 +1817,7 @@
                     ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null;
             mHandoverLaunchDisplayId = options.getLaunchDisplayId();
             mLaunchCookie = options.getLaunchCookie();
+            mLaunchRootTask = options.getLaunchRootTask();
         }
 
         mPersistentState = persistentState;
@@ -1790,6 +1899,13 @@
             task.setRootProcess(proc);
         }
         proc.addActivityIfNeeded(this);
+
+        // Update the associated task fragment after setting the process, since it's required for
+        // filtering to only report activities that belong to the same process.
+        final TaskFragment tf = getTaskFragment();
+        if (tf != null) {
+            tf.sendTaskFragmentInfoChanged();
+        }
     }
 
     boolean hasProcess() {
@@ -1919,9 +2035,10 @@
         }
     }
 
+    @VisibleForTesting
     boolean addStartingWindow(String pkg, int resolvedTheme, CompatibilityInfo compatInfo,
             CharSequence nonLocalizedLabel, int labelRes, int icon, int logo, int windowFlags,
-            IBinder transferFrom, boolean newTask, boolean taskSwitch, boolean processRunning,
+            ActivityRecord from, boolean newTask, boolean taskSwitch, boolean processRunning,
             boolean allowTaskSnapshot, boolean activityCreated, boolean useEmpty) {
         // If the display is frozen, we won't do anything until the actual window is
         // displayed so there is no reason to put in the starting window.
@@ -1980,7 +2097,7 @@
         }
         applyStartingWindowTheme(pkg, resolvedTheme);
 
-        if (transferStartingWindow(transferFrom)) {
+        if (from != null && transferStartingWindow(from)) {
             return true;
         }
 
@@ -2005,6 +2122,11 @@
 
         ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SnapshotStartingData");
         mStartingData = new SnapshotStartingData(mWmService, snapshot, typeParams);
+        if (task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+            // Associate with the task so if this activity is resized by task fragment later, the
+            // starting window can keep the same bounds as the task.
+            associateStartingDataWithTask();
+        }
         scheduleAddStartingWindow();
         return true;
     }
@@ -2198,7 +2320,8 @@
         // unable to copy from shell, maybe it's not a splash screen. or something went wrong.
         // either way, abort and reset the sequence.
         if (parcelable == null
-                || mTransferringSplashScreenState != TRANSFER_SPLASH_SCREEN_COPYING) {
+                || mTransferringSplashScreenState != TRANSFER_SPLASH_SCREEN_COPYING
+                || mStartingWindow == null) {
             if (parcelable != null) {
                 parcelable.clearIfNeeded();
             }
@@ -2207,13 +2330,17 @@
             return;
         }
         // schedule attach splashScreen to client
+        final SurfaceControl windowAnimationLeash = TaskOrganizerController
+                .applyStartingWindowAnimation(mStartingWindow);
         try {
             mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT;
             mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
-                    TransferSplashScreenViewStateItem.obtain(ATTACH_TO, parcelable));
+                    TransferSplashScreenViewStateItem.obtain(parcelable,
+                            windowAnimationLeash));
             scheduleTransferSplashScreenTimeout();
         } catch (Exception e) {
             Slog.w(TAG, "onCopySplashScreenComplete fail: " + this);
+            mStartingWindow.cancelAnimation();
             parcelable.clearIfNeeded();
             mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH;
         }
@@ -2223,14 +2350,9 @@
         removeTransferSplashScreenTimeout();
         // Client has draw the splash screen, so we can remove the starting window.
         if (mStartingWindow != null) {
+            mStartingWindow.cancelAnimation();
             mStartingWindow.hide(false, false);
         }
-        try {
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
-                    TransferSplashScreenViewStateItem.obtain(HANDOVER_TO, null));
-        } catch (Exception e) {
-            Slog.w(TAG, "onSplashScreenAttachComplete fail: " + this);
-        }
         // no matter what, remove the starting window.
         mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH;
         removeStartingWindowAnimation(false /* prepareAnimation */);
@@ -2253,15 +2375,44 @@
         }
     }
 
+    void attachStartingWindow(@NonNull WindowState startingWindow) {
+        startingWindow.mStartingData = mStartingData;
+        mStartingWindow = startingWindow;
+        if (mStartingData != null && mStartingData.mAssociatedTask != null) {
+            attachStartingSurfaceToAssociatedTask();
+        }
+    }
+
+    private void attachStartingSurfaceToAssociatedTask() {
+        // Associate the configuration of starting window with the task.
+        overrideConfigurationPropagation(mStartingWindow, mStartingData.mAssociatedTask);
+        getSyncTransaction().reparent(mStartingWindow.mSurfaceControl,
+                mStartingData.mAssociatedTask.mSurfaceControl);
+    }
+
+    private void associateStartingDataWithTask() {
+        mStartingData.mAssociatedTask = task;
+        task.forAllActivities(r -> {
+            if (r.mVisibleRequested && !r.firstWindowDrawn) {
+                r.mSharedStartingData = mStartingData;
+            }
+        });
+    }
+
     void removeStartingWindow() {
+        if (transferSplashScreenIfNeeded()) {
+            return;
+        }
         removeStartingWindowAnimation(true /* prepareAnimation */);
     }
 
     void removeStartingWindowAnimation(boolean prepareAnimation) {
-        if (transferSplashScreenIfNeeded()) {
-            return;
-        }
         mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE;
+        if (mSharedStartingData != null) {
+            mSharedStartingData.mAssociatedTask.forAllActivities(r -> {
+                r.mSharedStartingData = null;
+            });
+        }
         if (mStartingWindow == null) {
             if (mStartingData != null) {
                 // Starting window has not been added yet, but it is scheduled to be added.
@@ -2318,38 +2469,25 @@
         }
     }
 
-    private void removeAppTokenFromDisplay() {
-        if (mWmService.mRoot == null) return;
-
-        final DisplayContent dc = mWmService.mRoot.getDisplayContent(getDisplayId());
-        if (dc == null) {
-            Slog.w(TAG, "removeAppTokenFromDisplay: Attempted to remove token: "
-                    + appToken + " from non-existing displayId=" + getDisplayId());
-            return;
-        }
-        // Resume key dispatching if it is currently paused before we remove the container.
-        resumeKeyDispatchingLocked();
-        dc.removeAppToken(appToken.asBinder());
-    }
-
     /**
-     * Reparents this activity into {@param newTask} at the provided {@param position}.  The caller
-     * should ensure that the {@param newTask} is not already the parent of this activity.
+     * Reparents this activity into {@param newTaskFrag} at the provided {@param position}. The
+     * caller should ensure that the {@param newTaskFrag} is not already the parent of this
+     * activity.
      */
-    void reparent(Task newTask, int position, String reason) {
+    void reparent(TaskFragment newTaskFrag, int position, String reason) {
         if (getParent() == null) {
             Slog.w(TAG, "reparent: Attempted to reparent non-existing app token: " + appToken);
             return;
         }
-        final Task prevTask = task;
-        if (prevTask == newTask) {
-            throw new IllegalArgumentException(reason + ": task=" + newTask
+        final TaskFragment prevTaskFrag = getTaskFragment();
+        if (prevTaskFrag == newTaskFrag) {
+            throw new IllegalArgumentException(reason + ": task fragment =" + newTaskFrag
                     + " is already the parent of r=" + this);
         }
 
         ProtoLog.i(WM_DEBUG_ADD_REMOVE, "reparent: moving activity=%s"
-                + " to task=%d at %d", this, task.mTaskId, position);
-        reparent(newTask, position);
+                + " to new task fragment in task=%d at %d", this, task.mTaskId, position);
+        reparent(newTaskFrag, position);
     }
 
     private boolean isHomeIntent(Intent intent) {
@@ -2470,6 +2608,19 @@
         return task != null ? task.getOrganizedTask() : null;
     }
 
+    /** Returns the organized parent {@link TaskFragment}. */
+    @Nullable
+    TaskFragment getOrganizedTaskFragment() {
+        final TaskFragment parent = getTaskFragment();
+        return parent != null ? parent.getOrganizedTaskFragment() : null;
+    }
+
+    @Override
+    boolean isEmbedded() {
+        final TaskFragment parent = getTaskFragment();
+        return parent != null && parent.isEmbedded();
+    }
+
     @Override
     @Nullable
     TaskDisplayArea getDisplayArea() {
@@ -2561,7 +2712,9 @@
     boolean isResizeable() {
         return mAtmService.mForceResizableActivities
                 || ActivityInfo.isResizeableMode(info.resizeMode)
-                || info.supportsPictureInPicture();
+                || info.supportsPictureInPicture()
+                // If the activity can be embedded, it should inherit the bounds of task fragment.
+                || isEmbedded();
     }
 
     /** @return whether this activity is non-resizeable but is forced to be resizable. */
@@ -2741,7 +2894,7 @@
     /**
      * @return Whether AppOps allows this package to enter picture-in-picture.
      */
-    private boolean checkEnterPictureInPictureAppOpsState() {
+    boolean checkEnterPictureInPictureAppOpsState() {
         return mAtmService.getAppOpsManager().checkOpNoThrow(
                 OP_PICTURE_IN_PICTURE, info.applicationInfo.uid, packageName) == MODE_ALLOWED;
     }
@@ -2881,7 +3034,7 @@
     @interface FinishRequest {}
 
     /**
-     * See {@link #finishIfPossible(int, Intent, String, boolean)}
+     * See {@link #finishIfPossible(int, Intent, NeededUriGrants, String, boolean)}
      */
     @FinishRequest int finishIfPossible(String reason, boolean oomAdj) {
         return finishIfPossible(Activity.RESULT_CANCELED,
@@ -2915,7 +3068,7 @@
         }
 
         final Task rootTask = getRootTask();
-        final boolean mayAdjustTop = (isState(RESUMED) || rootTask.getResumedActivity() == null)
+        final boolean mayAdjustTop = (isState(RESUMED) || rootTask.getTopResumedActivity() == null)
                 && rootTask.isFocusedRootTaskOnDisplay()
                 // Do not adjust focus task because the task will be reused to launch new activity.
                 && !task.isClearingToReuseTask();
@@ -2926,9 +3079,9 @@
 
         mAtmService.deferWindowLayout();
         try {
-            final Transition newTransition = (!mAtmService.getTransitionController().isCollecting()
-                    && mAtmService.getTransitionController().getTransitionPlayer() != null)
-                    ? mAtmService.getTransitionController().createTransition(TRANSIT_CLOSE) : null;
+            final Transition newTransition = (!mTransitionController.isCollecting()
+                    && mTransitionController.getTransitionPlayer() != null)
+                    ? mTransitionController.createTransition(TRANSIT_CLOSE) : null;
             mTaskSupervisor.mNoHistoryActivities.remove(this);
             makeFinishingLocked();
             // Make a local reference to its task since this.task could be set to null once this
@@ -2961,7 +3114,7 @@
             final boolean endTask = task.getTopNonFinishingActivity() == null
                     && !task.isClearingToReuseTask();
             if (newTransition != null) {
-                mAtmService.getTransitionController().requestStartTransition(newTransition,
+                mTransitionController.requestStartTransition(newTransition,
                         endTask ? task : null, null /* remote */);
             }
             if (isState(RESUMED)) {
@@ -2994,12 +3147,12 @@
                 // Tell window manager to prepare for this one to be removed.
                 setVisibility(false);
 
-                if (task.getPausingActivity() == null) {
+                if (getTaskFragment().getPausingActivity() == null) {
                     ProtoLog.v(WM_DEBUG_STATES, "Finish needs to pause: %s", this);
                     if (DEBUG_USER_LEAVING) {
                         Slog.v(TAG_USER_LEAVING, "finish() => pause with userLeaving=false");
                     }
-                    task.startPausingLocked(false /* userLeaving */, false /* uiSleeping */,
+                    getTaskFragment().startPausing(false /* userLeaving */, false /* uiSleeping */,
                             null /* resuming */, "finish");
                 }
 
@@ -3121,6 +3274,20 @@
         // TODO(b/137329632): find the next activity directly underneath this one, not just anywhere
         final ActivityRecord next = getDisplayArea().topRunningActivity(
                 true /* considerKeyguardState */);
+
+        // If the finishing activity is the last activity of a organized TaskFragment and has an
+        // adjacent TaskFragment, check if the activity removal should be delayed.
+        boolean delayRemoval = false;
+        final TaskFragment taskFragment = getTaskFragment();
+        if (next != null && taskFragment != null && taskFragment.isEmbedded()) {
+            final TaskFragment organized = taskFragment.getOrganizedTaskFragment();
+            final TaskFragment adjacent =
+                    organized != null ? organized.getAdjacentTaskFragment() : null;
+            if (adjacent != null && organized.topRunningActivity() == null) {
+                delayRemoval = organized.isDelayLastActivityRemoval();
+            }
+        }
+
         // isNextNotYetVisible is to check if the next activity is invisible, or it has been
         // requested to be invisible but its windows haven't reported as invisible.  If so, it
         // implied that the current finishing activity should be added into stopping list rather
@@ -3130,12 +3297,12 @@
 
         // Clear last paused activity to ensure top activity can be resumed during sleeping.
         if (isNextNotYetVisible && mDisplayContent.isSleeping()
-                && next == next.getRootTask().mLastPausedActivity) {
-            next.getRootTask().mLastPausedActivity = null;
+                && next == next.getTaskFragment().mLastPausedActivity) {
+            next.getTaskFragment().clearLastPausedActivity();
         }
 
         if (isCurrentVisible) {
-            if (isNextNotYetVisible) {
+            if (isNextNotYetVisible || delayRemoval) {
                 // Add this activity to the list of stopping activities. It will be processed and
                 // destroyed when the next activity reports idle.
                 addToStopping(false /* scheduleIdle */, false /* idleDelayed */,
@@ -3318,8 +3485,8 @@
             if (DEBUG_SWITCH) {
                 final Task task = getTask();
                 Slog.v(TAG_SWITCH, "Safely destroying " + this + " in state " + getState()
-                        + " resumed=" + task.getResumedActivity()
-                        + " pausing=" + task.getPausingActivity()
+                        + " resumed=" + task.getTopResumedActivity()
+                        + " pausing=" + task.getTopPausingActivity()
                         + " for reason " + reason);
             }
             return destroyImmediately(reason);
@@ -3343,7 +3510,9 @@
         setState(DESTROYED, "removeFromHistory");
         if (DEBUG_APP) Slog.v(TAG_APP, "Clearing app during remove for activity " + this);
         detachFromProcess();
-        removeAppTokenFromDisplay();
+        // Resume key dispatching if it is currently paused before we remove the container.
+        resumeKeyDispatchingLocked();
+        mDisplayContent.removeAppToken(appToken);
 
         cleanUpActivityServices();
         removeUriPermissionsLocked();
@@ -3361,10 +3530,25 @@
             return;
         }
         finishing = true;
+        final TaskFragment taskFragment = getTaskFragment();
+        if (taskFragment != null) {
+            final Task task = taskFragment.getTask();
+            if (task != null && task.isClearingToReuseTask()
+                    && taskFragment.getTopNonFinishingActivity() == null) {
+                taskFragment.mClearedTaskForReuse = true;
+            }
+            taskFragment.sendTaskFragmentInfoChanged();
+        }
         if (stopped) {
             abortAndClearOptionsAnimation();
         }
-        mAtmService.getTransitionController().requestTransitionIfNeeded(TRANSIT_CLOSE, this);
+        if (mTransitionController.isCollecting()) {
+            // We don't want the finishing to change the transition ready state since there will not
+            // be corresponding setReady for finishing.
+            mTransitionController.collectExistenceChange(this);
+        } else {
+            mTransitionController.requestTransitionIfNeeded(TRANSIT_CLOSE, this);
+        }
     }
 
     /**
@@ -3398,7 +3582,7 @@
      * Note: Call before {@link #removeFromHistory(String)}.
      */
     void cleanUp(boolean cleanServices, boolean setState) {
-        task.cleanUpActivityReferences(this);
+        getTaskFragment().cleanUpActivityReferences(this);
         clearLastParentBeforePip();
 
         // Clean up the splash screen if it was still displayed.
@@ -3506,7 +3690,7 @@
             // failed more than twice. Skip activities that's already finishing cleanly by itself.
             remove = false;
         } else if ((!mHaveState && !stateNotNeeded
-                && !isState(ActivityState.RESTARTING_PROCESS)) || finishing) {
+                && !isState(State.RESTARTING_PROCESS)) || finishing) {
             // Don't currently have state for the activity, or it is finishing -- always remove it.
             remove = true;
         } else if (!mVisibleRequested && launchCount > 2
@@ -3544,18 +3728,33 @@
         }
         cleanUp(true /* cleanServices */, true /* setState */);
         if (remove) {
+            if (mStartingData != null && mVisible && task != null) {
+                // A corner case that the app terminates its trampoline activity on a separated
+                // process by killing itself. Transfer the starting window to the next activity
+                // which will be visible, so the dead activity can be removed immediately (no
+                // longer animating) and the reveal animation can play normally on next activity.
+                final ActivityRecord top = task.topRunningActivity();
+                if (top != null && !top.mVisible && top.shouldBeVisible()) {
+                    top.transferStartingWindow(this);
+                }
+            }
             removeFromHistory("appDied");
         }
     }
 
     @Override
     void removeImmediately() {
-        if (!finishing) {
+        if (mState != DESTROYED) {
+            Slog.w(TAG, "Force remove immediately " + this + " state=" + mState);
             // If Task#removeImmediately is called directly with alive activities, ensure that the
             // activities are destroyed and detached from process.
             destroyImmediately("removeImmediately");
+            // Complete the destruction immediately because this activity will not be found in
+            // hierarchy, it is unable to report completion.
+            destroyed("removeImmediately");
+        } else {
+            onRemovedFromDisplay();
         }
-        onRemovedFromDisplay();
         super.removeImmediately();
     }
 
@@ -3582,8 +3781,6 @@
 
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Removing app token: %s", this);
 
-        commitVisibility(false /* visible */, true /* performLayout */);
-
         getDisplayContent().mOpeningApps.remove(this);
         getDisplayContent().mUnknownAppVisibilityController.appRemovedOrHidden(this);
         mWmService.mTaskSnapshotController.onAppRemoved(this);
@@ -3591,8 +3788,6 @@
         mTaskSupervisor.mStoppingActivities.remove(this);
         waitingToShow = false;
 
-        // TODO(b/169035022): move to a more-appropriate place.
-        mAtmService.getTransitionController().collect(this);
         // Defer removal of this activity when either a child is animating, or app transition is on
         // going. App transition animation might be applied on the parent task not on the activity,
         // but the actual frame buffer is associated with the activity, so we have to keep the
@@ -3604,10 +3799,20 @@
         } else if (getDisplayContent().mAppTransition.isTransitionSet()) {
             getDisplayContent().mClosingApps.add(this);
             delayed = true;
-        } else if (mAtmService.getTransitionController().inTransition()) {
+        } else if (mTransitionController.inTransition()) {
             delayed = true;
         }
 
+        // Don't commit visibility if it is waiting to animate. It will be set post animation.
+        if (!delayed) {
+            commitVisibility(false /* visible */, true /* performLayout */);
+        } else {
+            setVisibleRequested(false /* visible */);
+        }
+
+        // TODO(b/169035022): move to a more-appropriate place.
+        mTransitionController.collect(this);
+
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                 "Removing app %s delayed=%b animation=%s animating=%b", this, delayed,
                 getAnimation(),
@@ -3769,17 +3974,14 @@
         }
     }
 
-    boolean transferStartingWindow(IBinder transferFrom) {
-        final ActivityRecord fromActivity = getDisplayContent().getActivityRecord(transferFrom);
-        if (fromActivity == null) {
-            return false;
-        }
-
+    private boolean transferStartingWindow(@NonNull ActivityRecord fromActivity) {
         final WindowState tStartingWindow = fromActivity.mStartingWindow;
         if (tStartingWindow != null && fromActivity.mStartingSurface != null) {
             // In this case, the starting icon has already been displayed, so start
             // letting windows get shown immediately without any more transitions.
-            getDisplayContent().mSkipAppTransitionAnimation = true;
+            if (fromActivity.mVisible) {
+                mDisplayContent.mSkipAppTransitionAnimation = true;
+            }
 
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Moving existing starting %s"
                     + " from %s to %s", tStartingWindow, fromActivity, this);
@@ -3795,6 +3997,7 @@
 
                 // Transfer the starting window over to the new token.
                 mStartingData = fromActivity.mStartingData;
+                mSharedStartingData = fromActivity.mSharedStartingData;
                 mStartingSurface = fromActivity.mStartingSurface;
                 startingDisplayed = fromActivity.startingDisplayed;
                 fromActivity.startingDisplayed = false;
@@ -3809,7 +4012,7 @@
 
                 ProtoLog.v(WM_DEBUG_ADD_REMOVE,
                         "Removing starting %s from %s", tStartingWindow, fromActivity);
-                mAtmService.getTransitionController().collect(tStartingWindow);
+                mTransitionController.collect(tStartingWindow);
                 tStartingWindow.reparent(this, POSITION_TOP);
 
                 // Propagate other interesting state between the tokens. If the old token is displayed,
@@ -3835,7 +4038,7 @@
                     // the token we transfer the animation over. Thus, set this flag to indicate
                     // we've transferred the animation.
                     mUseTransferredAnimation = true;
-                } else if (mAtmService.getTransitionController().getTransitionPlayer() != null) {
+                } else if (mTransitionController.getTransitionPlayer() != null) {
                     // In the new transit system, just set this every time we transfer the window
                     mUseTransferredAnimation = true;
                 }
@@ -3857,6 +4060,7 @@
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW,
                     "Moving pending starting from %s to %s", fromActivity, this);
             mStartingData = fromActivity.mStartingData;
+            mSharedStartingData = fromActivity.mSharedStartingData;
             fromActivity.mStartingData = null;
             fromActivity.startingMoved = true;
             scheduleAddStartingWindow();
@@ -3875,16 +4079,10 @@
      * immediately finishes after, so we have to transfer T to M.
      */
     void transferStartingWindowFromHiddenAboveTokenIfNeeded() {
-        final PooledFunction p = PooledLambda.obtainFunction(ActivityRecord::transferStartingWindow,
-                this, PooledLambda.__(ActivityRecord.class));
-        task.forAllActivities(p);
-        p.recycle();
-    }
-
-    private boolean transferStartingWindow(ActivityRecord fromActivity) {
-        if (fromActivity == this) return true;
-
-        return !fromActivity.mVisibleRequested && transferStartingWindow(fromActivity.token);
+        task.forAllActivities(fromActivity -> {
+            if (fromActivity == this) return true;
+            return !fromActivity.mVisibleRequested && transferStartingWindow(fromActivity);
+        });
     }
 
     void checkKeyguardFlagsChanged() {
@@ -4040,20 +4238,6 @@
         return callback.test(this) ? this : null;
     }
 
-    @Override
-    protected void setLayer(Transaction t, int layer) {
-        if (!mSurfaceAnimator.hasLeash()) {
-            t.setLayer(mSurfaceControl, layer);
-        }
-    }
-
-    @Override
-    protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
-        if (!mSurfaceAnimator.hasLeash()) {
-            t.setRelativeLayer(mSurfaceControl, relativeTo, layer);
-        }
-    }
-
     void logStartActivity(int tag, Task task) {
         final Uri data = intent.getData();
         final String strData = data != null ? data.toSafeString() : null;
@@ -4230,6 +4414,9 @@
     private void applyOptionsAnimation(ActivityOptions pendingOptions, Intent intent) {
         final int animationType = pendingOptions.getAnimationType();
         final DisplayContent displayContent = getDisplayContent();
+        AnimationOptions options = null;
+        IRemoteCallback startCallback = null;
+        IRemoteCallback finishCallback = null;
         switch (animationType) {
             case ANIM_CUSTOM:
                 displayContent.mAppTransition.overridePendingAppTransition(
@@ -4239,11 +4426,19 @@
                         pendingOptions.getAnimationStartedListener(),
                         pendingOptions.getAnimationFinishedListener(),
                         pendingOptions.getOverrideTaskTransition());
+                options = AnimationOptions.makeCustomAnimOptions(pendingOptions.getPackageName(),
+                        pendingOptions.getCustomEnterResId(), pendingOptions.getCustomExitResId(),
+                        pendingOptions.getOverrideTaskTransition());
+                startCallback = pendingOptions.getAnimationStartedListener();
+                finishCallback = pendingOptions.getAnimationFinishedListener();
                 break;
             case ANIM_CLIP_REVEAL:
                 displayContent.mAppTransition.overridePendingAppTransitionClipReveal(
                         pendingOptions.getStartX(), pendingOptions.getStartY(),
                         pendingOptions.getWidth(), pendingOptions.getHeight());
+                options = AnimationOptions.makeClipRevealAnimOptions(
+                        pendingOptions.getStartX(), pendingOptions.getStartY(),
+                        pendingOptions.getWidth(), pendingOptions.getHeight());
                 if (intent.getSourceBounds() == null) {
                     intent.setSourceBounds(new Rect(pendingOptions.getStartX(),
                             pendingOptions.getStartY(),
@@ -4255,6 +4450,9 @@
                 displayContent.mAppTransition.overridePendingAppTransitionScaleUp(
                         pendingOptions.getStartX(), pendingOptions.getStartY(),
                         pendingOptions.getWidth(), pendingOptions.getHeight());
+                options = AnimationOptions.makeScaleUpAnimOptions(
+                        pendingOptions.getStartX(), pendingOptions.getStartY(),
+                        pendingOptions.getWidth(), pendingOptions.getHeight());
                 if (intent.getSourceBounds() == null) {
                     intent.setSourceBounds(new Rect(pendingOptions.getStartX(),
                             pendingOptions.getStartY(),
@@ -4270,6 +4468,9 @@
                         pendingOptions.getStartX(), pendingOptions.getStartY(),
                         pendingOptions.getAnimationStartedListener(),
                         scaleUp);
+                options = AnimationOptions.makeThumnbnailAnimOptions(buffer,
+                        pendingOptions.getStartX(), pendingOptions.getStartY(), scaleUp);
+                startCallback = pendingOptions.getAnimationStartedListener();
                 if (intent.getSourceBounds() == null && buffer != null) {
                     intent.setSourceBounds(new Rect(pendingOptions.getStartX(),
                             pendingOptions.getStartY(),
@@ -4309,6 +4510,7 @@
             case ANIM_OPEN_CROSS_PROFILE_APPS:
                 displayContent.mAppTransition
                         .overridePendingAppTransitionStartCrossProfileApps();
+                options = AnimationOptions.makeCrossProfileAnimOptions();
                 break;
             case ANIM_NONE:
             case ANIM_UNDEFINED:
@@ -4317,6 +4519,10 @@
                 Slog.e(TAG_WM, "applyOptionsLocked: Unknown animationType=" + animationType);
                 break;
         }
+
+        if (options != null) {
+            mTransitionController.setOverrideAnimation(options, startCallback, finishCallback);
+        }
     }
 
     void clearAllDrawn() {
@@ -4396,8 +4602,8 @@
         return opts;
     }
 
-    IRemoteTransition takeRemoteTransition() {
-        IRemoteTransition out = mPendingRemoteTransition;
+    RemoteTransition takeRemoteTransition() {
+        RemoteTransition out = mPendingRemoteTransition;
         mPendingRemoteTransition = null;
         return out;
     }
@@ -4449,6 +4655,10 @@
         }
     }
 
+    boolean getDeferHidingClient() {
+        return mDeferHidingClient;
+    }
+
     @Override
     boolean isVisible() {
         // If the activity isn't hidden then it is considered visible and there is no need to check
@@ -4484,6 +4694,7 @@
         if (app != null) {
             mTaskSupervisor.onProcessActivityStateChanged(app, false /* forceBatch */);
         }
+        logAppCompatState();
     }
 
     /**
@@ -4541,7 +4752,7 @@
                 Debug.getCallers(6));
 
         // Before setting mVisibleRequested so we can track changes.
-        mAtmService.getTransitionController().collect(this);
+        mTransitionController.collect(this);
 
         onChildVisibilityRequested(visible);
 
@@ -4613,7 +4824,7 @@
         }
 
         // If in a transition, defer commits for activities that are going invisible
-        if (!visible && mAtmService.getTransitionController().inTransition()) {
+        if (!visible && inTransition()) {
             return;
         }
         // If we are preparing an app transition, then delay changing
@@ -4677,8 +4888,9 @@
      * @param visible {@code true} if this {@link ActivityRecord} should become visible, otherwise
      *                this should become invisible.
      * @param performLayout if {@code true}, perform surface placement after committing visibility.
+     * @param fromTransition {@code true} if this is part of finishing a transition.
      */
-    void commitVisibility(boolean visible, boolean performLayout) {
+    void commitVisibility(boolean visible, boolean performLayout, boolean fromTransition) {
         // Reset the state of mVisibleSetFromTransferredStartingWindow since visibility is actually
         // been set by the app now.
         mVisibleSetFromTransferredStartingWindow = false;
@@ -4698,7 +4910,8 @@
         } else {
             // If we are being set visible, and the starting window is not yet displayed,
             // then make sure it doesn't get displayed.
-            if (mStartingWindow != null && !mStartingWindow.isDrawn()) {
+            if (mStartingWindow != null && !mStartingWindow.isDrawn()
+                    && (firstWindowDrawn || allDrawn)) {
                 mStartingWindow.clearPolicyVisibilityFlag(LEGACY_POLICY_VISIBILITY);
                 mStartingWindow.mLegacyPolicyVisibilityAfterAnim = false;
             }
@@ -4706,6 +4919,14 @@
             // getting visible so we also wait for them.
             forAllWindows(mWmService::makeWindowFreezingScreenIfNeededLocked, true);
         }
+        // dispatchTaskInfoChangedIfNeeded() right after ActivityRecord#setVisibility() can report
+        // the stale visible state, because the state will be updated after the app transition.
+        // So tries to report the actual visible state again where the state is changed.
+        Task task = getOrganizedTask();
+        while (task != null) {
+            task.dispatchTaskInfoChangedIfNeeded(false /* force */);
+            task = task.getParent().asTask();
+        }
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                 "commitVisibility: %s: visible=%b mVisibleRequested=%b", this,
                 isVisible(), mVisibleRequested);
@@ -4719,7 +4940,11 @@
         displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/);
         mUseTransferredAnimation = false;
 
-        postApplyAnimation(visible);
+        postApplyAnimation(visible, fromTransition);
+    }
+
+    void commitVisibility(boolean visible, boolean performLayout) {
+        commitVisibility(visible, performLayout, false /* fromTransition */);
     }
 
     /**
@@ -4730,12 +4955,16 @@
      *
      * @param visible {@code true} if this {@link ActivityRecord} has become visible, otherwise
      *                this has become invisible.
+     * @param fromTransition {@code true} if this call is part of finishing a transition. This is
+     *                       needed because the shell transition is no-longer active by the time
+     *                       commitVisibility is called.
      */
-    private void postApplyAnimation(boolean visible) {
+    private void postApplyAnimation(boolean visible, boolean fromTransition) {
+        final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled();
         final boolean delayed = isAnimating(PARENTS | CHILDREN,
                 ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION
                         | ANIMATION_TYPE_RECENTS);
-        if (!delayed) {
+        if (!delayed && !usingShellTransitions) {
             // We aren't delayed anything, but exiting windows rely on the animation finished
             // callback being called in case the ActivityRecord was pretending to be delayed,
             // which we might have done because we were in closing/opening apps list.
@@ -4754,8 +4983,8 @@
         // updated.
         // If we're becoming invisible, update the client visibility if we are not running an
         // animation. Otherwise, we'll update client visibility in onAnimationFinished.
-        if (visible || !isAnimating(PARENTS,
-                ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)) {
+        if (visible || !isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)
+                || usingShellTransitions) {
             setClientVisible(visible);
         }
 
@@ -4771,7 +5000,8 @@
 
         final DisplayContent displayContent = getDisplayContent();
         if (!displayContent.mClosingApps.contains(this)
-                && !displayContent.mOpeningApps.contains(this)) {
+                && !displayContent.mOpeningApps.contains(this)
+                && !fromTransition) {
             // Take the screenshot before possibly hiding the WSA, otherwise the screenshot
             // will not be taken.
             mWmService.mTaskSnapshotController.notifyAppVisibilityChanged(this, visible);
@@ -4865,7 +5095,7 @@
         return mCurrentLaunchCanTurnScreenOn;
     }
 
-    void setState(ActivityState state, String reason) {
+    void setState(State state, String reason) {
         ProtoLog.v(WM_DEBUG_STATES, "State movement: %s from:%s to:%s reason:%s",
                 this, getState(), state, reason);
 
@@ -4877,8 +5107,8 @@
 
         mState = state;
 
-        if (task != null) {
-            task.onActivityStateChanged(this, state, reason);
+        if (getTaskFragment() != null) {
+            getTaskFragment().onActivityStateChanged(this, state, reason);
         }
 
         // The WindowManager interprets the app stopping signal as
@@ -4938,44 +5168,42 @@
         }
     }
 
-    ActivityState getState() {
+    State getState() {
         return mState;
     }
 
     /**
      * Returns {@code true} if the Activity is in the specified state.
      */
-    boolean isState(ActivityState state) {
+    boolean isState(State state) {
         return state == mState;
     }
 
     /**
      * Returns {@code true} if the Activity is in one of the specified states.
      */
-    boolean isState(ActivityState state1, ActivityState state2) {
+    boolean isState(State state1, State state2) {
         return state1 == mState || state2 == mState;
     }
 
     /**
      * Returns {@code true} if the Activity is in one of the specified states.
      */
-    boolean isState(ActivityState state1, ActivityState state2, ActivityState state3) {
+    boolean isState(State state1, State state2, State state3) {
         return state1 == mState || state2 == mState || state3 == mState;
     }
 
     /**
      * Returns {@code true} if the Activity is in one of the specified states.
      */
-    boolean isState(ActivityState state1, ActivityState state2, ActivityState state3,
-            ActivityState state4) {
+    boolean isState(State state1, State state2, State state3, State state4) {
         return state1 == mState || state2 == mState || state3 == mState || state4 == mState;
     }
 
     /**
      * Returns {@code true} if the Activity is in one of the specified states.
      */
-    boolean isState(ActivityState state1, ActivityState state2, ActivityState state3,
-            ActivityState state4, ActivityState state5) {
+    boolean isState(State state1, State state2, State state3, State state4, State state5) {
         return state1 == mState || state2 == mState || state3 == mState || state4 == mState
                 || state5 == mState;
     }
@@ -4983,8 +5211,8 @@
     /**
      * Returns {@code true} if the Activity is in one of the specified states.
      */
-    boolean isState(ActivityState state1, ActivityState state2, ActivityState state3,
-            ActivityState state4, ActivityState state5, ActivityState state6) {
+    boolean isState(State state1, State state2, State state3, State state4, State state5,
+            State state6) {
         return state1 == mState || state2 == mState || state3 == mState || state4 == mState
                 || state5 == mState || state6 == mState;
     }
@@ -5041,6 +5269,7 @@
     void notifyAppStopped() {
         ProtoLog.v(WM_DEBUG_ADD_REMOVE, "notifyAppStopped: %s", this);
         mAppStopped = true;
+        firstWindowDrawn = false;
         // This is to fix the edge case that auto-enter-pip is finished in Launcher but app calls
         // setAutoEnterEnabled(false) and transitions to STOPPED state, see b/191930787.
         // Clear any surface transactions and content overlay in this case.
@@ -5112,7 +5341,7 @@
 
     void updateVisibilityIgnoringKeyguard(boolean behindFullscreenActivity) {
         visibleIgnoringKeyguard = (!behindFullscreenActivity || mLaunchTaskBehind)
-                && okToShowLocked();
+                && showToCurrentUser();
     }
 
     boolean shouldBeVisible() {
@@ -5182,7 +5411,8 @@
             // returns. Just need to confirm this reasoning makes sense.
             final boolean deferHidingClient = canEnterPictureInPicture
                     && !isState(STARTED, STOPPING, STOPPED, PAUSED);
-            if (deferHidingClient && pictureInPictureArgs.isAutoEnterEnabled()) {
+            if (!mTransitionController.isShellTransitionsEnabled()
+                    && deferHidingClient && pictureInPictureArgs.isAutoEnterEnabled()) {
                 // Go ahead and just put the activity in pip if it supports auto-pip.
                 mAtmService.enterPictureInPictureMode(this, pictureInPictureArgs);
                 return;
@@ -5198,13 +5428,6 @@
                     supportsEnterPipOnTaskSwitch = false;
                     break;
                 case RESUMED:
-                    // If the app is capable of entering PIP, we should try pausing it now
-                    // so it can PIP correctly.
-                    if (deferHidingClient) {
-                        task.startPausingLocked(false /* uiSleeping */,
-                                null /* resuming */, "makeInvisible");
-                        break;
-                    }
                 case INITIALIZING:
                 case PAUSING:
                 case PAUSED:
@@ -5301,7 +5524,8 @@
      */
     private boolean shouldBeResumed(ActivityRecord activeActivity) {
         return shouldMakeActive(activeActivity) && isFocusable()
-                && getTask().getVisibility(activeActivity) == TASK_VISIBILITY_VISIBLE
+                && getTaskFragment().getVisibility(activeActivity)
+                        == TASK_FRAGMENT_VISIBILITY_VISIBLE
                 && canResumeByCompat();
     }
 
@@ -5355,7 +5579,7 @@
         if (!task.hasChild(this)) {
             throw new IllegalStateException("Activity not found in its task");
         }
-        return task.topRunningActivity() == this;
+        return getTaskFragment().topRunningActivity() == this;
     }
 
     void handleAlreadyVisible() {
@@ -5444,16 +5668,17 @@
         ProtoLog.v(WM_DEBUG_STATES, "Activity paused: token=%s, timeout=%b", appToken,
                 timeout);
 
-        if (task != null) {
+        final TaskFragment taskFragment = getTaskFragment();
+        if (taskFragment != null) {
             removePauseTimeout();
 
-            final ActivityRecord pausingActivity = task.getPausingActivity();
+            final ActivityRecord pausingActivity = taskFragment.getPausingActivity();
             if (pausingActivity == this) {
                 ProtoLog.v(WM_DEBUG_STATES, "Moving to PAUSED: %s %s", this,
                         (timeout ? "(due to timeout)" : " (pause complete)"));
                 mAtmService.deferWindowLayout();
                 try {
-                    task.completePauseLocked(true /* resumeNext */, null /* resumingActivity */);
+                    taskFragment.completePause(true /* resumeNext */, null /* resumingActivity */);
                 } finally {
                     mAtmService.continueWindowLayout();
                 }
@@ -5708,6 +5933,9 @@
     }
 
     void startFreezingScreen(int overrideOriginalDisplayRotation) {
+        if (mTransitionController.isShellTransitionsEnabled()) {
+            return;
+        }
         ProtoLog.i(WM_DEBUG_ORIENTATION,
                 "Set freezing of %s: visible=%b freezing=%b visibleRequested=%b. %s",
                 appToken, isVisible(), mFreezingScreen, mVisibleRequested,
@@ -5808,7 +6036,7 @@
         }
     }
 
-    void onFirstWindowDrawn(WindowState win, WindowStateAnimator winAnimator) {
+    void onFirstWindowDrawn(WindowState win) {
         firstWindowDrawn = true;
         // stop tracking
         mSplashScreenStyleEmpty = true;
@@ -5824,7 +6052,22 @@
             // own stuff.
             win.cancelAnimation();
         }
-        removeStartingWindow();
+
+        // Remove starting window directly if is in a pure task. Otherwise if it is associated with
+        // a task (e.g. nested task fragment), then remove only if all visible windows in the task
+        // are drawn.
+        final Task associatedTask =
+                mSharedStartingData != null ? mSharedStartingData.mAssociatedTask : null;
+        if (associatedTask == null) {
+            removeStartingWindow();
+        } else if (associatedTask.getActivity(
+                r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) {
+            // The last drawn activity may not be the one that owns the starting window.
+            final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
+            if (r != null) {
+                r.removeStartingWindow();
+            }
+        }
         updateReportedVisibilityLocked();
     }
 
@@ -5868,6 +6111,9 @@
         if (task != null) {
             task.setHasBeenVisible(true);
         }
+        // Clear indicated launch root task because there's no trampoline activity to expect after
+        // the windows are drawn.
+        mLaunchRootTask = null;
     }
 
     /** Called when the windows associated app window container are visible. */
@@ -6001,11 +6247,7 @@
         // TODO(shell-transitions): Remove mDeferHidingClient once everything is shell-transitions.
         //                          pip activities should just remain in clientVisible.
         if (!clientVisible && mDeferHidingClient) return;
-        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                "setClientVisible: %s clientVisible=%b Callers=%s", this, clientVisible,
-                Debug.getCallers(5));
         super.setClientVisible(clientVisible);
-        sendAppVisibilityToClients();
     }
 
     /**
@@ -6124,9 +6366,9 @@
                 return this;
             }
             // Try to use the one which is closest to top.
-            ActivityRecord r = rootTask.getResumedActivity();
+            ActivityRecord r = rootTask.getTopResumedActivity();
             if (r == null) {
-                r = rootTask.getPausingActivity();
+                r = rootTask.getTopPausingActivity();
             }
             if (r != null) {
                 return r;
@@ -6135,22 +6377,8 @@
         return this;
     }
 
-    /** Checks whether the activity should be shown for current user. */
-    public boolean okToShowLocked() {
-        // We cannot show activities when the device is locked and the application is not
-        // encryption aware.
-        if (!StorageManager.isUserKeyUnlocked(mUserId)
-                && !info.applicationInfo.isEncryptionAware()) {
-            return false;
-        }
-
-        return (info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0
-                || (mTaskSupervisor.isCurrentProfileLocked(mUserId)
-                && mAtmService.mAmInternal.isUserRunning(mUserId, 0 /* flags */));
-    }
-
     boolean canBeTopRunning() {
-        return !finishing && okToShowLocked();
+        return !finishing && showToCurrentUser();
     }
 
     /**
@@ -6187,6 +6415,12 @@
         return null;
     }
 
+    @Nullable
+    static ActivityRecord isInAnyTask(IBinder token) {
+        final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+        return (r != null && r.isAttached()) ? r : null;
+    }
+
     /**
      * @return display id to which this record is attached,
      *         {@link android.view.Display#INVALID_DISPLAY} if not attached.
@@ -6204,7 +6438,8 @@
             // This would be redundant.
             return false;
         }
-        if (isState(RESUMED) || getRootTask() == null || this == task.getPausingActivity()
+        if (isState(RESUMED) || getRootTask() == null
+                || this == getTaskFragment().getPausingActivity()
                 || !mHaveState || !stopped) {
             // We're not ready for this kind of thing.
             return false;
@@ -6391,13 +6626,12 @@
         final boolean newSingleActivity = !newTask && !activityCreated
                 && task.getActivity((r) -> !r.finishing && r != this) == null;
 
-        final boolean shown = addStartingWindow(packageName, resolvedTheme,
+        final boolean scheduled = addStartingWindow(packageName, resolvedTheme,
                 compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
-                prev != null ? prev.appToken : null,
-                newTask || newSingleActivity, taskSwitch, isProcessRunning(),
+                prev, newTask || newSingleActivity, taskSwitch, isProcessRunning(),
                 allowTaskSnapshot(), activityCreated, mSplashScreenStyleEmpty);
-        if (shown) {
-            mStartingWindowState = STARTING_WINDOW_SHOWN;
+        if (DEBUG_STARTING_WINDOW_VERBOSE && scheduled) {
+            Slog.d(TAG, "Scheduled starting window for " + this);
         }
     }
 
@@ -6409,14 +6643,12 @@
      * It should only be called if this activity is behind other fullscreen activity.
      */
     void cancelInitializing() {
-        if (mStartingWindowState == STARTING_WINDOW_SHOWN) {
+        if (mStartingData != null) {
             // Remove orphaned starting window.
             if (DEBUG_VISIBILITY) Slog.w(TAG_VISIBILITY, "Found orphaned starting window " + this);
-            mStartingWindowState = STARTING_WINDOW_REMOVED;
             removeStartingWindowAnimation(false /* prepareAnimation */);
         }
-        if (isState(INITIALIZING) && !shouldBeVisible(
-                true /* behindFullscreenActivity */, true /* ignoringKeyguard */)) {
+        if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
             // Remove the unknown visibility record because an invisible activity shouldn't block
             // the keyguard transition.
             mDisplayContent.mUnknownAppVisibilityController.appRemovedOrHidden(this);
@@ -6461,17 +6693,6 @@
         }
     }
 
-    boolean hasWindowsAlive() {
-        for (int i = mChildren.size() - 1; i >= 0; i--) {
-            // No need to loop through child windows as the answer should be the same as that of the
-            // parent window.
-            if (!(mChildren.get(i)).mAppDied) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     void setWillReplaceWindows(boolean animate) {
         ProtoLog.d(WM_DEBUG_ADD_REMOVE,
                 "Marking app token %s with replacing windows.", this);
@@ -6555,12 +6776,6 @@
         return candidate;
     }
 
-    SurfaceControl getAppAnimationLayer() {
-        return getAppAnimationLayer(isActivityTypeHome() ? ANIMATION_LAYER_HOME
-                : needsZBoost() ? ANIMATION_LAYER_BOOSTED
-                        : ANIMATION_LAYER_STANDARD);
-    }
-
     @Override
     boolean needsZBoost() {
         return mNeedsZBoost || super.needsZBoost();
@@ -6621,29 +6836,9 @@
                 || mDisplayContent.isNextTransitionForward();
     }
 
-    private int getAnimationLayer() {
-        // The leash is parented to the animation layer. We need to preserve the z-order by using
-        // the prefix order index, but we boost if necessary.
-        int layer;
-        if (!inPinnedWindowingMode()) {
-            layer = getPrefixOrderIndex();
-        } else {
-            // Root pinned tasks have animations take place within themselves rather than an
-            // animation layer so we need to preserve the order relative to the root task (e.g.
-            // the order of our task/parent).
-            layer = getParent().getPrefixOrderIndex();
-        }
-
-        if (mNeedsZBoost) {
-            layer += Z_BOOST_BASE;
-        }
-        return layer;
-    }
-
     @Override
-    public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
-        t.setLayer(leash, getAnimationLayer());
-        getDisplayContent().assignRootTaskOrdering();
+    void resetSurfacePositionForAnimationLeash(SurfaceControl.Transaction t) {
+        // Noop as Activity may be offset for letterbox
     }
 
     @Override
@@ -6672,7 +6867,7 @@
 
             // Crop to root task bounds.
             t.setLayer(leash, 0);
-            t.setLayer(mAnimationBoundsLayer, getAnimationLayer());
+            t.setLayer(mAnimationBoundsLayer, getLastLayer());
 
             // Reparent leash to animation bounds layer.
             t.reparent(leash, mAnimationBoundsLayer);
@@ -6681,7 +6876,7 @@
 
     @Override
     void prepareSurfaces() {
-        final boolean show = isVisible() || isAnimating(PARENTS,
+        final boolean show = isVisible() || isAnimating(TRANSITION | PARENTS,
                 ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS);
 
         if (mSurfaceControl != null) {
@@ -6765,7 +6960,7 @@
         Rect appRect;
         if (win != null) {
             insets = win.getInsetsStateWithVisibilityOverride().calculateInsets(
-                    win.getFrame(), Type.systemBars(), false /* ignoreVisibility */);
+                    win.getFrame(), Type.systemBars(), false /* ignoreVisibility */).toRect();
             appRect = new Rect(win.getFrame());
             appRect.inset(insets);
         } else {
@@ -6774,8 +6969,7 @@
         }
         final Configuration displayConfig = mDisplayContent.getConfiguration();
         return getDisplayContent().mAppTransition.createThumbnailAspectScaleAnimationLocked(
-                appRect, insets, thumbnailHeader, task, displayConfig.uiMode,
-                displayConfig.orientation);
+                appRect, insets, thumbnailHeader, task, displayConfig.orientation);
     }
 
     @Override
@@ -6922,7 +7116,7 @@
             return;
         }
 
-        mDisplayContent.mPinnedTaskController.onCancelFixedRotationTransform(task);
+        mDisplayContent.mPinnedTaskController.onCancelFixedRotationTransform();
         // Perform rotation animation according to the rotation of this activity.
         startFreezingScreen(originalDisplayRotation);
         // This activity may relaunch or perform configuration change so once it has reported drawn,
@@ -7200,7 +7394,8 @@
             // If the activity has requested override bounds, the configuration needs to be
             // computed accordingly.
             if (!matchParentBounds()) {
-                task.computeConfigResourceOverrides(resolvedConfig, newParentConfiguration);
+                getTaskFragment().computeConfigResourceOverrides(resolvedConfig,
+                        newParentConfiguration);
             }
         // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds
         // are already calculated in resolveFixedOrientationConfiguration.
@@ -7251,6 +7446,8 @@
             }
             resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
         }
+
+        logAppCompatState();
     }
 
     /**
@@ -7260,24 +7457,61 @@
      * LetterboxUiController#shouldShowLetterboxUi} for more context.
      */
     boolean areBoundsLetterboxed() {
+        return getAppCompatState(/* ignoreVisibility= */ true)
+                != APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
+    }
+
+    /**
+     * Logs the current App Compat state via {@link ActivityMetricsLogger#logAppCompatState}.
+     */
+    private void logAppCompatState() {
+        mTaskSupervisor.getActivityMetricsLogger().logAppCompatState(this);
+    }
+
+    /**
+     * Returns the current App Compat state of this activity.
+     *
+     * <p>The App Compat state indicates whether the activity is visible and letterboxed, and if so
+     * what is the reason for letterboxing. The state is used for logging the time spent in
+     * letterbox (sliced by the reason) vs non-letterbox per app.
+     */
+    int getAppCompatState() {
+        return getAppCompatState(/* ignoreVisibility= */ false);
+    }
+
+    /**
+     * Same as {@link #getAppCompatState()} except when {@code ignoreVisibility} the visibility
+     * of the activity is ignored.
+     *
+     * @param ignoreVisibility whether to ignore the visibility of the activity and not return
+     *                         NOT_VISIBLE if {@code mVisibleRequested} is false.
+     */
+    private int getAppCompatState(boolean ignoreVisibility) {
+        if (!ignoreVisibility && !mVisibleRequested) {
+            return APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+        }
         if (mInSizeCompatModeForBounds) {
-            return true;
+            return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
         }
         // Letterbox for fixed orientation. This check returns true only when an activity is
         // letterboxed for fixed orientation. Aspect ratio restrictions are also applied if
         // present. But this doesn't return true when the activity is letterboxed only because
         // of aspect ratio restrictions.
         if (isLetterboxedForFixedOrientationAndAspectRatio()) {
-            return true;
+            return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION;
         }
         // Letterbox for limited aspect ratio.
-        return mIsAspectRatioApplied;
+        if (mIsAspectRatioApplied) {
+            return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO;
+        }
+
+        return APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
     }
 
     /**
      * Adjusts horizontal position of resolved bounds if they doesn't fill the parent using gravity
      * requested in the config or via an ADB command. For more context see {@link
-     * WindowManagerService#getLetterboxHorizontalPositionMultiplier}.
+     * LetterboxUiController#getHorizontalPositionMultiplier(Configuration)}.
      */
     private void updateResolvedBoundsHorizontalPosition(Configuration newParentConfiguration) {
         final Configuration resolvedConfig = getResolvedOverrideConfiguration();
@@ -7297,11 +7531,7 @@
                     parentAppBounds.width(), screenResolvedBounds.width());
         } else {
             float positionMultiplier =
-                    mWmService.mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier();
-            positionMultiplier =
-                    (positionMultiplier < 0.0f || positionMultiplier > 1.0f)
-                            // Default to central position if invalid value is provided.
-                            ? 0.5f : positionMultiplier;
+                    mLetterboxUiController.getHorizontalPositionMultiplier(newParentConfiguration);
             offsetX = (int) Math.ceil((parentAppBounds.width() - screenResolvedBounds.width())
                     * positionMultiplier);
         }
@@ -7315,7 +7545,16 @@
         }
 
         // Since bounds has changed, the configuration needs to be computed accordingly.
-        task.computeConfigResourceOverrides(resolvedConfig, newParentConfiguration);
+        getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration);
+    }
+
+    void recomputeConfiguration() {
+        onRequestedOverrideConfigurationChanged(getRequestedOverrideConfiguration());
+    }
+
+    boolean isInTransition() {
+        return mTransitionController.inTransition() // Shell transitions.
+                || isAnimating(PARENTS | TRANSITION); // Legacy transitions.
     }
 
     /**
@@ -7331,8 +7570,60 @@
     }
 
     /**
-     * Computes bounds (letterbox or pillarbox) when the parent doesn't handle the orientation
-     * change and the requested orientation is different from the parent.
+     * In some cases, applying insets to bounds changes the orientation. For example, if a
+     * close-to-square display rotates to portrait to respect a portrait orientation activity, after
+     * insets such as the status and nav bars are applied, the activity may actually have a
+     * landscape orientation. This method checks whether the orientations of the activity window
+     * with and without insets match or if the orientation with insets already matches the
+     * requested orientation. If not, it may be necessary to letterbox the window.
+     * @param parentBounds are the new parent bounds passed down to the activity and should be used
+     *                     to compute the stable bounds.
+     * @param outStableBounds will store the stable bounds, which are the bounds with insets
+     *                        applied, if orientation is not respected when insets are applied.
+     *                        Otherwise outStableBounds will be empty. Stable bounds should be used
+     *                        to compute letterboxed bounds if orientation is not respected when
+     *                        insets are applied.
+     */
+    private boolean orientationRespectedWithInsets(Rect parentBounds, Rect outStableBounds) {
+        outStableBounds.setEmpty();
+        if (mDisplayContent == null) {
+            return true;
+        }
+        // Only need to make changes if activity sets an orientation
+        final int requestedOrientation = getRequestedConfigurationOrientation();
+        if (requestedOrientation == ORIENTATION_UNDEFINED) {
+            return true;
+        }
+        // Compute parent orientation from bounds
+        final int orientation = parentBounds.height() >= parentBounds.width()
+                ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+        // Compute orientation from stable parent bounds (= parent bounds with insets applied)
+        final Task task = getTask();
+        task.calculateInsetFrames(mTmpOutNonDecorBounds /* outNonDecorBounds */,
+                outStableBounds /* outStableBounds */, parentBounds /* bounds */,
+                mDisplayContent.getDisplayInfo());
+        final int orientationWithInsets = outStableBounds.height() >= outStableBounds.width()
+                ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+        // If orientation does not match the orientation with insets applied, then a
+        // display rotation will not be enough to respect orientation. However, even if they do
+        // not match but the orientation with insets applied matches the requested orientation, then
+        // there is no need to modify the bounds because when insets are applied, the activity will
+        // have the desired orientation.
+        final boolean orientationRespectedWithInsets = orientation == orientationWithInsets
+                || orientationWithInsets == requestedOrientation;
+        if (orientationRespectedWithInsets) {
+            outStableBounds.setEmpty();
+        }
+        return orientationRespectedWithInsets;
+    }
+
+    /**
+     * Computes bounds (letterbox or pillarbox) when either:
+     * 1. The parent doesn't handle the orientation change and the requested orientation is
+     *    different from the parent (see {@link DisplayContent#setIgnoreOrientationRequest()}.
+     * 2. The parent handling the orientation is not enough. This occurs when the display rotation
+     *    may not be enough to respect orientation requests (see {@link
+     *    ActivityRecord#orientationRespectedWithInsets}).
      *
      * <p>If letterboxed due to fixed orientation then aspect ratio restrictions are also applied
      * in this method.
@@ -7340,9 +7631,15 @@
     private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig,
             int windowingMode) {
         mLetterboxBoundsForFixedOrientationAndAspectRatio = null;
-        if (handlesOrientationChangeFromDescendant()) {
+        final Rect parentBounds = newParentConfig.windowConfiguration.getBounds();
+        final Rect stableBounds = new Rect();
+        // If orientation is respected when insets are applied, then stableBounds will be empty.
+        boolean orientationRespectedWithInsets =
+                orientationRespectedWithInsets(parentBounds, stableBounds);
+        if (handlesOrientationChangeFromDescendant() && orientationRespectedWithInsets) {
             // No need to letterbox because of fixed orientation. Display will handle
-            // fixed-orientation requests.
+            // fixed-orientation requests and a display rotation is enough to respect requested
+            // orientation with insets applied.
             return;
         }
         if (WindowConfiguration.inMultiWindowMode(windowingMode) && isResizeable()) {
@@ -7362,7 +7659,9 @@
         // If the activity requires a different orientation (either by override or activityInfo),
         // make it fit the available bounds by scaling down its bounds.
         final int forcedOrientation = getRequestedConfigurationOrientation();
-        if (forcedOrientation == ORIENTATION_UNDEFINED || forcedOrientation == parentOrientation) {
+
+        if (forcedOrientation == ORIENTATION_UNDEFINED
+                || (forcedOrientation == parentOrientation && orientationRespectedWithInsets)) {
             return;
         }
 
@@ -7374,48 +7673,58 @@
             return;
         }
 
-        final Rect parentBounds = newParentConfig.windowConfiguration.getBounds();
-        final Rect parentAppBounds = newParentConfig.windowConfiguration.getAppBounds();
+        // TODO(b/182268157): Explore using only one type of parentBoundsWithInsets, either app
+        // bounds or stable bounds to unify aspect ratio logic.
+        final Rect parentBoundsWithInsets = orientationRespectedWithInsets
+                ? newParentConfig.windowConfiguration.getAppBounds() : stableBounds;
         final Rect containingBounds = new Rect();
-        final Rect containingAppBounds = new Rect();
-        // Need to shrink the containing bounds into a square because the parent orientation does
-        // not match the activity requested orientation.
+        final Rect containingBoundsWithInsets = new Rect();
+        // Need to shrink the containing bounds into a square because the parent orientation
+        // does not match the activity requested orientation.
         if (forcedOrientation == ORIENTATION_LANDSCAPE) {
-            // Shrink height to match width. Position height within app bounds.
-            final int bottom = Math.min(parentAppBounds.top + parentBounds.width(),
-                    parentAppBounds.bottom);
-            containingBounds.set(parentBounds.left, parentAppBounds.top, parentBounds.right,
+            // Landscape is defined as width > height. Make the container respect landscape
+            // orientation by shrinking height to one less than width. Landscape activity will be
+            // vertically centered within parent bounds with insets, so position vertical bounds
+            // within parent bounds with insets to prevent insets from unnecessarily trimming
+            // vertical bounds.
+            final int bottom = Math.min(parentBoundsWithInsets.top + parentBounds.width() - 1,
+                    parentBoundsWithInsets.bottom);
+            containingBounds.set(parentBounds.left, parentBoundsWithInsets.top, parentBounds.right,
                     bottom);
-            containingAppBounds.set(parentAppBounds.left, parentAppBounds.top,
-                    parentAppBounds.right, bottom);
+            containingBoundsWithInsets.set(parentBoundsWithInsets.left, parentBoundsWithInsets.top,
+                    parentBoundsWithInsets.right, bottom);
         } else {
-            // Shrink width to match height. Position width within app bounds.
-            final int right = Math.min(parentAppBounds.left + parentBounds.height(),
-                    parentAppBounds.right);
-            containingBounds.set(parentAppBounds.left, parentBounds.top, right,
+            // Portrait is defined as width <= height. Make the container respect portrait
+            // orientation by shrinking width to match height. Portrait activity will be
+            // horizontally centered within parent bounds with insets, so position horizontal bounds
+            // within parent bounds with insets to prevent insets from unnecessarily trimming
+            // horizontal bounds.
+            final int right = Math.min(parentBoundsWithInsets.left + parentBounds.height(),
+                    parentBoundsWithInsets.right);
+            containingBounds.set(parentBoundsWithInsets.left, parentBounds.top, right,
                     parentBounds.bottom);
-            containingAppBounds.set(parentAppBounds.left, parentAppBounds.top, right,
-                    parentAppBounds.bottom);
+            containingBoundsWithInsets.set(parentBoundsWithInsets.left, parentBoundsWithInsets.top,
+                    right, parentBoundsWithInsets.bottom);
         }
 
-        Rect mTmpFullBounds = new Rect(resolvedBounds);
+        // Store the current bounds to be able to revert to size compat mode values below if needed.
+        final Rect prevResolvedBounds = new Rect(resolvedBounds);
         resolvedBounds.set(containingBounds);
 
-        // Override from config_fixedOrientationLetterboxAspectRatio or via ADB with
-        // set-fixed-orientation-letterbox-aspect-ratio.
         final float letterboxAspectRatioOverride =
-                mWmService.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+                mLetterboxUiController.getFixedOrientationLetterboxAspectRatio(newParentConfig);
         final float desiredAspectRatio =
                 letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
                         ? letterboxAspectRatioOverride : computeAspectRatio(parentBounds);
         // Apply aspect ratio to resolved bounds
-        mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingAppBounds,
+        mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingBoundsWithInsets,
                 containingBounds, desiredAspectRatio, true);
 
-        // Vertically center if orientation is landscape. Bounds will later be horizontally centered
-        // in {@link updateResolvedBoundsHorizontalPosition()} regardless of orientation.
+        // Vertically center if orientation is landscape. Center within parent bounds with insets
+        // to ensure that insets do not trim height. Bounds will later be horizontally centered in
+        // {@link updateResolvedBoundsHorizontalPosition()} regardless of orientation.
         if (forcedOrientation == ORIENTATION_LANDSCAPE) {
-            final int offsetY = parentBounds.centerY() - resolvedBounds.centerY();
+            final int offsetY = parentBoundsWithInsets.centerY() - resolvedBounds.centerY();
             resolvedBounds.offset(0, offsetY);
         }
 
@@ -7427,14 +7736,15 @@
                 // The app shouldn't be resized, we only do fixed orientation letterboxing if the
                 // compat bounds are also from the same fixed orientation letterbox. Otherwise,
                 // clear the fixed orientation bounds to show app in size compat mode.
-                resolvedBounds.set(mTmpFullBounds);
+                resolvedBounds.set(prevResolvedBounds);
                 return;
             }
         }
 
         // Calculate app bounds using fixed orientation bounds because they will be needed later
         // for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}.
-        task.computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
+        getTaskFragment().computeConfigResourceOverrides(getResolvedOverrideConfiguration(),
+                newParentConfig);
         mLetterboxBoundsForFixedOrientationAndAspectRatio = new Rect(resolvedBounds);
     }
 
@@ -7462,7 +7772,7 @@
         if (!resolvedBounds.isEmpty() && !resolvedBounds.equals(parentBounds)) {
             // Compute the configuration based on the resolved bounds. If aspect ratio doesn't
             // restrict, the bounds should be the requested override bounds.
-            task.computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
+            getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                     getFixedRotationTransformDisplayInfo());
         }
     }
@@ -7522,10 +7832,10 @@
         // Use resolvedBounds to compute other override configurations such as appBounds. The bounds
         // are calculated in compat container space. The actual position on screen will be applied
         // later, so the calculation is simpler that doesn't need to involve offset from parent.
-        task.computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
+        getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                 mCompatDisplayInsets);
         // Use current screen layout as source because the size of app is independent to parent.
-        resolvedConfig.screenLayout = Task.computeScreenLayoutOverride(
+        resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
                 getConfiguration().screenLayout, resolvedConfig.screenWidthDp,
                 resolvedConfig.screenHeightDp);
 
@@ -7538,7 +7848,7 @@
         // Below figure is an example that puts an activity which was launched in a larger container
         // into a smaller container.
         //   The outermost rectangle is the real display bounds.
-        //   "@" is the container app bounds (parent bounds or fixed orientation bouds)
+        //   "@" is the container app bounds (parent bounds or fixed orientation bounds)
         //   "#" is the {@code resolvedBounds} that applies to application.
         //   "*" is the {@code mSizeCompatBounds} that used to show on screen if scaled.
         // ------------------------------
@@ -7581,12 +7891,15 @@
             mSizeCompatBounds = null;
         }
 
-        // Align to top of parent (bounds) - this is a UX choice and exclude the horizontal decor
-        // if needed. Horizontal position is adjusted in updateResolvedBoundsHorizontalPosition.
+        // Vertically center within parent (bounds) - this is a UX choice and exclude the horizontal
+        // decor if needed. Horizontal position is adjusted in
+        // updateResolvedBoundsHorizontalPosition.
         // Above coordinates are in "@" space, now place "*" and "#" to screen space.
         final boolean fillContainer = resolvedBounds.equals(containingBounds);
         final int screenPosX = fillContainer ? containerBounds.left : containerAppBounds.left;
-        final int screenPosY = containerBounds.top;
+        final int screenPosY = mSizeCompatBounds == null
+                ? (containerBounds.height() - resolvedBounds.height()) / 2
+                : (containerBounds.height() - mSizeCompatBounds.height()) / 2;
         if (screenPosX != 0 || screenPosY != 0) {
             if (mSizeCompatBounds != null) {
                 mSizeCompatBounds.offset(screenPosX, screenPosY);
@@ -7669,6 +7982,10 @@
         if (getUid() == SYSTEM_UID) {
             return false;
         }
+        // Do not sandbox to activity window bounds if the feature is disabled.
+        if (mDisplayContent != null && !mDisplayContent.sandboxDisplayApis()) {
+            return false;
+        }
         // Never apply sandboxing to an app that should be explicitly excluded from the config.
         if (info != null && info.neverSandboxDisplayApis()) {
             return false;
@@ -7693,13 +8010,10 @@
     @VisibleForTesting
     @Override
     Rect getAnimationBounds(int appRootTaskClipMode) {
-        if (appRootTaskClipMode == ROOT_TASK_CLIP_BEFORE_ANIM && getRootTask() != null) {
-            // Using the root task bounds here effectively applies the clipping before animation.
-            return getRootTask().getBounds();
-        }
-        // Use task-bounds if available so that activity-level letterbox (maxAspectRatio) is
+        // Use TaskFragment-bounds if available so that activity-level letterbox (maxAspectRatio) is
         // included in the animation.
-        return task != null ? task.getBounds() : getBounds();
+        final TaskFragment taskFragment = getTaskFragment();
+        return taskFragment != null ? taskFragment.getBounds() : getBounds();
     }
 
     @Override
@@ -7850,7 +8164,7 @@
 
         if (task == null || rootTask == null
                 || (inMultiWindowMode() && !shouldCreateCompatDisplayInsets()
-                    && !fixedOrientationLetterboxed)
+                && !fixedOrientationLetterboxed)
                 || (maxAspectRatio < 1 && minAspectRatio < 1 && desiredAspectRatio < 1)
                 || isInVrUiMode(getConfiguration())) {
             // We don't enforce aspect ratio if the activity task is in multiwindow unless it is in
@@ -7943,7 +8257,7 @@
         // If the bounds are restricted by fixed aspect ratio, then out bounds should be put in the
         // container app bounds. Otherwise the entire container bounds are available.
         if (!outBounds.equals(containingBounds)) {
-            // The horizontal position should not cover insets.
+            // The horizontal position should not cover insets (e.g. display cutout).
             outBounds.left = containingAppBounds.left;
         }
 
@@ -8101,7 +8415,9 @@
             configChangeFlags |= changes;
             startFreezingScreenLocked(globalChanges);
             forceNewConfig = false;
-            preserveWindow &= isResizeOnlyChange(changes);
+            // Do not preserve window if it is freezing screen because the original window won't be
+            // able to update drawn state that causes freeze timeout.
+            preserveWindow &= isResizeOnlyChange(changes) && !mFreezingScreen;
             final boolean hasResizeChange = hasResizeChange(changes & ~info.getRealConfigChanged());
             if (hasResizeChange) {
                 final boolean isDragResizing = task.isDragResizing();
@@ -8715,6 +9031,8 @@
      * compatibility mode activity compute the configuration without relying on its current display.
      */
     static class CompatDisplayInsets {
+        /** The original rotation the compat insets were computed in */
+        final @Rotation int mOriginalRotation;
         /** The container width on rotation 0. */
         private final int mWidth;
         /** The container height on rotation 0. */
@@ -8741,6 +9059,7 @@
         /** Constructs the environment to simulate the bounds behavior of the given container. */
         CompatDisplayInsets(DisplayContent display, ActivityRecord container,
                 @Nullable Rect fixedOrientationBounds) {
+            mOriginalRotation = display.getRotation();
             mIsFloating = container.getWindowConfiguration().tasksAreFloating();
             if (mIsFloating) {
                 final Rect containerBounds = container.getWindowConfiguration().getBounds();
@@ -8887,7 +9206,8 @@
                 outAppBounds.offset(insets.left, insets.top);
             } else if (rotation != ROTATION_UNDEFINED) {
                 // Ensure the app bounds won't overlap with insets.
-                Task.intersectWithInsetsIfFits(outAppBounds, outBounds, mNonDecorInsets[rotation]);
+                TaskFragment.intersectWithInsetsIfFits(outAppBounds, outBounds,
+                        mNonDecorInsets[rotation]);
             }
         }
     }
@@ -8910,17 +9230,19 @@
             return null;
         }
         final Rect insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets(
-                task.getBounds(), Type.systemBars(), false /* ignoreVisibility */);
+                task.getBounds(), Type.systemBars(), false /* ignoreVisibility */).toRect();
         InsetUtils.addInsets(insets, getLetterboxInsets());
 
-        return new RemoteAnimationTarget(task.mTaskId, record.getMode(),
-                record.mAdapter.mCapturedLeash, !fillsParent(),
+        final RemoteAnimationTarget target = new RemoteAnimationTarget(task.mTaskId,
+                record.getMode(), record.mAdapter.mCapturedLeash, !fillsParent(),
                 new Rect(), insets,
                 getPrefixOrderIndex(), record.mAdapter.mPosition, record.mAdapter.mLocalBounds,
-                record.mAdapter.mRootTaskBounds, task.getWindowConfiguration(),
+                record.mAdapter.mEndBounds, task.getWindowConfiguration(),
                 false /*isNotInRecents*/,
                 record.mThumbnailAdapter != null ? record.mThumbnailAdapter.mCapturedLeash : null,
-                record.mStartBounds, task.getTaskInfo());
+                record.mStartBounds, task.getTaskInfo(), checkEnterPictureInPictureAppOpsState());
+        target.hasAnimatingParent = record.hasAnimatingParent();
+        return target;
     }
 
     @Override
@@ -8956,6 +9278,19 @@
         return false;
     }
 
+    @Override
+    void finishSync(Transaction outMergedTransaction, boolean cancel) {
+        // This override is just for getting metrics. allFinished needs to be checked before
+        // finish because finish resets all the states.
+        mLastAllReadyAtSync = allSyncFinished();
+        super.finishSync(outMergedTransaction, cancel);
+    }
+
+    @Override
+    boolean canBeAnimationTarget() {
+        return true;
+    }
+
     static class Builder {
         private final ActivityTaskManagerService mAtmService;
         private WindowProcessController mCallerApp;
diff --git a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
index 8540fa7..30c7b23 100644
--- a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
+++ b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
@@ -16,10 +16,10 @@
 
 package com.android.server.wm;
 
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_CLEANUP;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
 
 import android.util.ArraySet;
 import android.util.Slog;
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index b6f2f24..6ad2f7c 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -27,6 +27,7 @@
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
 import android.app.IApplicationThread;
@@ -38,6 +39,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -488,6 +490,30 @@
         return START_SUCCESS;
     }
 
+    /**
+     * Starts an activity in the TaskFragment.
+     * @param taskFragment TaskFragment {@link TaskFragment} to start the activity in.
+     * @param activityIntent intent to start the activity.
+     * @param activityOptions ActivityOptions to start the activity with.
+     * @param resultTo the caller activity
+     * @return the start result.
+     */
+    int startActivityInTaskFragment(@NonNull TaskFragment taskFragment,
+            @NonNull Intent activityIntent, @Nullable Bundle activityOptions,
+            @Nullable IBinder resultTo) {
+        final ActivityRecord caller =
+                resultTo != null ? ActivityRecord.forTokenLocked(resultTo) : null;
+        return obtainStarter(activityIntent, "startActivityInTaskFragment")
+                .setActivityOptions(activityOptions)
+                .setInTaskFragment(taskFragment)
+                .setResultTo(resultTo)
+                .setRequestCode(-1)
+                .setCallingUid(Binder.getCallingUid())
+                .setCallingPid(Binder.getCallingPid())
+                .setUserId(caller != null ? caller.mUserId : mService.getCurrentUserId())
+                .execute();
+    }
+
     void registerRemoteAnimationForNextActivityStart(String packageName,
             RemoteAnimationAdapter adapter) {
         mPendingRemoteAnimationRegistry.addPendingAnimation(packageName, adapter);
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index 979cea9..223f0be 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -51,6 +51,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.BlockedAppActivity;
@@ -178,7 +179,33 @@
             // before issuing the work challenge.
             return true;
         }
-        return interceptLockedManagedProfileIfNeeded();
+        if (interceptLockedManagedProfileIfNeeded()) {
+            return true;
+        }
+
+        final SparseArray<ActivityInterceptorCallback> callbacks =
+                mService.getActivityInterceptorCallbacks();
+        final ActivityInterceptorCallback.ActivityInterceptorInfo interceptorInfo =
+                new ActivityInterceptorCallback.ActivityInterceptorInfo(mRealCallingUid,
+                        mRealCallingPid, mUserId, mCallingPackage, mCallingFeatureId, mIntent,
+                        mRInfo, mAInfo, mResolvedType, mCallingPid, mCallingUid,
+                        mActivityOptions);
+
+        for (int i = 0; i < callbacks.size(); i++) {
+            final ActivityInterceptorCallback callback = callbacks.valueAt(i);
+            final Intent newIntent = callback.intercept(interceptorInfo);
+            if (newIntent == null) {
+                continue;
+            }
+            mIntent = newIntent;
+            mCallingPid = mRealCallingPid;
+            mCallingUid = mRealCallingUid;
+            mRInfo = mSupervisor.resolveIntent(mIntent, null, mUserId, 0, mRealCallingUid);
+            mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags,
+                    null /*profilerInfo*/);
+            return true;
+        }
+        return false;
     }
 
     private boolean hasCrossProfileAnimation() {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index a9a25fc..01e3e19 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -23,11 +23,13 @@
 import static android.app.ActivityManager.START_CLASS_NOT_FOUND;
 import static android.app.ActivityManager.START_DELIVERED_TO_TOP;
 import static android.app.ActivityManager.START_FLAG_ONLY_IF_NEEDED;
+import static android.app.ActivityManager.START_PERMISSION_DENIED;
 import static android.app.ActivityManager.START_RETURN_INTENT_TO_CALLER;
 import static android.app.ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION;
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -58,6 +60,7 @@
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ACTIVITY_STARTS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS;
@@ -74,7 +77,6 @@
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
 import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
@@ -112,7 +114,7 @@
 import android.util.DebugUtils;
 import android.util.Pools.SynchronizedPool;
 import android.util.Slog;
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.HeavyWeightSwitcherActivity;
@@ -177,6 +179,7 @@
     private int mPreferredWindowingMode;
 
     private Task mInTask;
+    private TaskFragment mInTaskFragment;
     @VisibleForTesting
     boolean mAddingToTask;
     private Task mReuseTask;
@@ -190,7 +193,6 @@
     private Task mTargetTask;
     private boolean mMovedToFront;
     private boolean mNoAnimation;
-    private boolean mKeepCurTransition;
     private boolean mAvoidMoveToFront;
     private boolean mFrozeTaskList;
     private boolean mTransientLaunch;
@@ -342,6 +344,7 @@
         boolean avoidMoveToFront;
         ActivityRecord[] outActivity;
         Task inTask;
+        TaskFragment inTaskFragment;
         String reason;
         ProfilerInfo profilerInfo;
         Configuration globalConfig;
@@ -392,6 +395,7 @@
             componentSpecified = false;
             outActivity = null;
             inTask = null;
+            inTaskFragment = null;
             reason = null;
             profilerInfo = null;
             globalConfig = null;
@@ -407,7 +411,7 @@
         /**
          * Adopts all values from passed in request.
          */
-        void set(Request request) {
+        void set(@NonNull Request request) {
             caller = request.caller;
             intent = request.intent;
             intentGrants = request.intentGrants;
@@ -432,6 +436,7 @@
             componentSpecified = request.componentSpecified;
             outActivity = request.outActivity;
             inTask = request.inTask;
+            inTaskFragment = request.inTaskFragment;
             reason = request.reason;
             profilerInfo = request.profilerInfo;
             globalConfig = request.globalConfig;
@@ -574,6 +579,7 @@
         mPreferredWindowingMode = starter.mPreferredWindowingMode;
 
         mInTask = starter.mInTask;
+        mInTaskFragment = starter.mInTaskFragment;
         mAddingToTask = starter.mAddingToTask;
         mReuseTask = starter.mReuseTask;
 
@@ -585,7 +591,6 @@
         mTargetRootTask = starter.mTargetRootTask;
         mMovedToFront = starter.mMovedToFront;
         mNoAnimation = starter.mNoAnimation;
-        mKeepCurTransition = starter.mKeepCurTransition;
         mAvoidMoveToFront = starter.mAvoidMoveToFront;
         mFrozeTaskList = starter.mFrozeTaskList;
 
@@ -737,7 +742,7 @@
                 Slog.w(TAG, "Unable to find app for caller " + mRequest.caller + " (pid="
                         + mRequest.callingPid + ") when starting: " + mRequest.intent.toString());
                 SafeActivityOptions.abort(mRequest.activityOptions);
-                return ActivityManager.START_PERMISSION_DENIED;
+                return START_PERMISSION_DENIED;
             }
         }
 
@@ -835,6 +840,7 @@
         final int startFlags = request.startFlags;
         final SafeActivityOptions options = request.activityOptions;
         Task inTask = request.inTask;
+        TaskFragment inTaskFragment = request.inTaskFragment;
 
         int err = ActivityManager.START_SUCCESS;
         // Pull the optional Ephemeral Installer-only bundle out of the options early.
@@ -850,7 +856,7 @@
             } else {
                 Slog.w(TAG, "Unable to find app for caller " + caller + " (pid=" + callingPid
                         + ") when starting: " + intent.toString());
-                err = ActivityManager.START_PERMISSION_DENIED;
+                err = START_PERMISSION_DENIED;
             }
         }
 
@@ -864,7 +870,7 @@
         ActivityRecord sourceRecord = null;
         ActivityRecord resultRecord = null;
         if (resultTo != null) {
-            sourceRecord = mRootWindowContainer.isInAnyTask(resultTo);
+            sourceRecord = ActivityRecord.isInAnyTask(resultTo);
             if (DEBUG_RESULTS) {
                 Slog.v(TAG_RESULTS, "Will send result to " + resultTo + " " + sourceRecord);
             }
@@ -1175,8 +1181,8 @@
         }
 
         mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
-                request.voiceInteractor, startFlags, true /* doResume */, checkedOptions, inTask,
-                restrictedBgActivity, intentGrants);
+                request.voiceInteractor, startFlags, true /* doResume */, checkedOptions,
+                inTask, inTaskFragment, restrictedBgActivity, intentGrants);
 
         if (request.outActivity != null) {
             request.outActivity[0] = mLastStartActivityRecord;
@@ -1502,7 +1508,7 @@
         final Task targetTask = r.getTask() != null
                 ? r.getTask()
                 : mTargetTask;
-        if (startedActivityRootTask == null || targetTask == null) {
+        if (startedActivityRootTask == null || targetTask == null || !targetTask.isAttached()) {
             return;
         }
 
@@ -1546,29 +1552,43 @@
      * Here also ensures that the starting activity is removed if the start wasn't successful.
      */
     private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
-                IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
-                int startFlags, boolean doResume, ActivityOptions options, Task inTask,
-                boolean restrictedBgActivity, NeededUriGrants intentGrants) {
+            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
+            int startFlags, boolean doResume, ActivityOptions options, Task inTask,
+            TaskFragment inTaskFragment, boolean restrictedBgActivity,
+            NeededUriGrants intentGrants) {
         int result = START_CANCELED;
+        boolean startResultSuccessful = false;
         final Task startedActivityRootTask;
 
         // Create a transition now to record the original intent of actions taken within
         // startActivityInner. Otherwise, logic in startActivityInner could start a different
         // transition based on a sub-action.
         // Only do the create here (and defer requestStart) since startActivityInner might abort.
-        final Transition newTransition = (!mService.getTransitionController().isCollecting()
-                && mService.getTransitionController().getTransitionPlayer() != null)
-                ? mService.getTransitionController().createTransition(TRANSIT_OPEN) : null;
-        IRemoteTransition remoteTransition = r.takeRemoteTransition();
+        final TransitionController transitionController = r.mTransitionController;
+        final Transition newTransition = (!transitionController.isCollecting()
+                && transitionController.getTransitionPlayer() != null)
+                ? transitionController.createTransition(TRANSIT_OPEN) : null;
+        RemoteTransition remoteTransition = r.takeRemoteTransition();
         if (newTransition != null && remoteTransition != null) {
             newTransition.setRemoteTransition(remoteTransition);
         }
-        mService.getTransitionController().collect(r);
+        transitionController.collect(r);
+        final boolean isTransient = r.getOptions() != null && r.getOptions().getTransientLaunch();
         try {
             mService.deferWindowLayout();
             Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "startActivityInner");
             result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
-                    startFlags, doResume, options, inTask, restrictedBgActivity, intentGrants);
+                    startFlags, doResume, options, inTask, inTaskFragment, restrictedBgActivity,
+                    intentGrants);
+            startResultSuccessful = ActivityManager.isStartResultSuccessful(result);
+            final boolean taskAlwaysOnTop = options != null && options.getTaskAlwaysOnTop();
+            // Apply setAlwaysOnTop when starting an Activity is successful regardless of creating
+            // a new Activity or recycling the existing Activity.
+            if (taskAlwaysOnTop && startResultSuccessful) {
+                final Task targetRootTask =
+                        mTargetRootTask != null ? mTargetRootTask : mTargetTask.getRootTask();
+                targetRootTask.setAlwaysOnTop(true);
+            }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
             startedActivityRootTask = handleStartResult(r, result);
@@ -1576,7 +1596,7 @@
             mSupervisor.mUserLeaving = false;
 
             // Transition housekeeping
-            if (!ActivityManager.isStartResultSuccessful(result)) {
+            if (!startResultSuccessful) {
                 if (newTransition != null) {
                     newTransition.abort();
                 }
@@ -1595,17 +1615,23 @@
                         statusBar.collapsePanels();
                     }
                 }
-                if (result == START_SUCCESS || result == START_TASK_TO_FRONT) {
+                final boolean started = result == START_SUCCESS || result == START_TASK_TO_FRONT;
+                if (started) {
                     // The activity is started new rather than just brought forward, so record
                     // it as an existence change.
-                    mService.getTransitionController().collectExistenceChange(r);
+                    transitionController.collectExistenceChange(r);
+                }
+                if (isTransient) {
+                    // `r` isn't guaranteed to be the actual relevant activity, so we must wait
+                    // until after we launched to identify the relevant activity.
+                    transitionController.setTransientLaunch(mLastStartActivityRecord);
                 }
                 if (newTransition != null) {
-                    mService.getTransitionController().requestStartTransition(newTransition,
+                    transitionController.requestStartTransition(newTransition,
                             mTargetTask, remoteTransition);
-                } else {
+                } else if (started) {
                     // Make the collecting transition wait until this request is ready.
-                    mService.getTransitionController().setReady(false);
+                    transitionController.setReady(r, false);
                 }
             }
         }
@@ -1665,15 +1691,15 @@
      *
      * Note: This method should only be called from {@link #startActivityUnchecked}.
      */
-
     // TODO(b/152429287): Make it easier to exercise code paths through startActivityInner
     @VisibleForTesting
     int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
             IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
             int startFlags, boolean doResume, ActivityOptions options, Task inTask,
-            boolean restrictedBgActivity, NeededUriGrants intentGrants) {
-        setInitialState(r, options, inTask, doResume, startFlags, sourceRecord, voiceSession,
-                voiceInteractor, restrictedBgActivity);
+            TaskFragment inTaskFragment, boolean restrictedBgActivity,
+            NeededUriGrants intentGrants) {
+        setInitialState(r, options, inTask, inTaskFragment, doResume, startFlags, sourceRecord,
+                voiceSession, voiceInteractor, restrictedBgActivity);
 
         computeLaunchingTaskFlags();
 
@@ -1681,6 +1707,8 @@
 
         mIntent.setFlags(mLaunchFlags);
 
+        // Get top task at beginning because the order may be changed when reusing existing task.
+        final Task prevTopTask = mPreferredTaskDisplayArea.getFocusedRootTask();
         final Task reusedTask = getReusableTask();
 
         // If requested, freeze the task list
@@ -1739,11 +1767,6 @@
 
         if (!mAvoidMoveToFront && mDoResume) {
             mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask);
-            if (mOptions != null) {
-                if (mOptions.getTaskAlwaysOnTop()) {
-                    mTargetRootTask.setAlwaysOnTop(true);
-                }
-            }
             if (!mTargetRootTask.isTopRootTaskInDisplayArea() && mService.mInternal.isDreaming()) {
                 // Launching underneath dream activity (fullscreen, always-on-top). Run the launch-
                 // -behind transition so the Activity gets created and starts in visible state.
@@ -1765,24 +1788,23 @@
                     UserHandle.getAppId(mStartActivity.info.applicationInfo.uid) /*recipient*/,
                     resultToUid /*visible*/, true /*direct*/);
         }
+        final Task startedTask = mStartActivity.getTask();
         if (newTask) {
-            EventLogTags.writeWmCreateTask(mStartActivity.mUserId,
-                    mStartActivity.getTask().mTaskId);
+            EventLogTags.writeWmCreateTask(mStartActivity.mUserId, startedTask.mTaskId);
         }
-        mStartActivity.logStartActivity(
-                EventLogTags.WM_CREATE_ACTIVITY, mStartActivity.getTask());
+        mStartActivity.logStartActivity(EventLogTags.WM_CREATE_ACTIVITY, startedTask);
 
-        mTargetRootTask.mLastPausedActivity = null;
+        mStartActivity.getTaskFragment().clearLastPausedActivity();
 
         mRootWindowContainer.startPowerModeLaunchIfNeeded(
                 false /* forceSend */, mStartActivity);
 
+        final boolean isTaskSwitch = startedTask != prevTopTask && !startedTask.isEmbedded();
         mTargetRootTask.startActivityLocked(mStartActivity,
                 topRootTask != null ? topRootTask.getTopNonFinishingActivity() : null, newTask,
-                mKeepCurTransition, mOptions, sourceRecord);
+                isTaskSwitch, mOptions, sourceRecord);
         if (mDoResume) {
-            final ActivityRecord topTaskActivity =
-                    mStartActivity.getTask().topRunningActivityLocked();
+            final ActivityRecord topTaskActivity = startedTask.topRunningActivityLocked();
             if (!mTargetRootTask.isTopActivityFocusable()
                     || (topTaskActivity != null && topTaskActivity.isTaskOverlay()
                     && mStartActivity != topTaskActivity)) {
@@ -1816,8 +1838,8 @@
         mRootWindowContainer.updateUserRootTask(mStartActivity.mUserId, mTargetRootTask);
 
         // Update the recent tasks list immediately when the activity starts
-        mSupervisor.mRecentTasks.add(mStartActivity.getTask());
-        mSupervisor.handleNonResizableTaskIfNeeded(mStartActivity.getTask(),
+        mSupervisor.mRecentTasks.add(startedTask);
+        mSupervisor.handleNonResizableTaskIfNeeded(startedTask,
                 mPreferredWindowingMode, mPreferredTaskDisplayArea, mTargetRootTask);
 
         return START_SUCCESS;
@@ -1931,10 +1953,46 @@
             }
         }
 
+        if (mInTaskFragment != null && !canEmbedActivity(mInTaskFragment, r, newTask, targetTask)) {
+            Slog.e(TAG, "Permission denied: Cannot embed " + r + " to " + mInTaskFragment.getTask()
+                    + " targetTask= " + targetTask);
+            return START_PERMISSION_DENIED;
+        }
+
         return START_SUCCESS;
     }
 
     /**
+     * Return {@code true} if an activity can be embedded to the TaskFragment.
+     * @param taskFragment the TaskFragment for embedding.
+     * @param starting the starting activity.
+     * @param newTask whether the starting activity is going to be launched on a new task.
+     * @param targetTask the target task for launching activity, which could be different from
+     *                   the one who hosting the embedding.
+     */
+    private boolean canEmbedActivity(@NonNull TaskFragment taskFragment, ActivityRecord starting,
+            boolean newTask, Task targetTask) {
+        final Task hostTask = taskFragment.getTask();
+        if (hostTask == null) {
+            return false;
+        }
+
+        // Allowing the embedding if the task is owned by system.
+        final int hostUid = hostTask.effectiveUid;
+        if (UserHandle.getAppId(hostUid) == Process.SYSTEM_UID) {
+            return true;
+        }
+
+        // Not allowed embedding an activity of another app.
+        if (hostUid != starting.getUid()) {
+            return false;
+        }
+
+        // Not allowed embedding task.
+        return !newTask && (targetTask == null || targetTask == hostTask);
+    }
+
+    /**
      * Prepare the target task to be reused for this launch, which including:
      * - Position the target task on valid root task on preferred display.
      * - Comply to the specified activity launch flags
@@ -2023,7 +2081,7 @@
         // We didn't do anything...  but it was needed (a.k.a., client don't use that intent!)
         // And for paranoia, make sure we have correctly resumed the top activity.
         resumeTargetRootTaskIfNeeded();
-      
+
         mLastStartActivityRecord = targetTaskTop;
         return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;
     }
@@ -2049,7 +2107,7 @@
         }
 
         // For paranoia, make sure we have correctly resumed the top activity.
-        topRootTask.mLastPausedActivity = null;
+        top.getTaskFragment().clearLastPausedActivity();
         if (mDoResume) {
             mRootWindowContainer.resumeFocusedTasksTopActivities();
         }
@@ -2145,7 +2203,7 @@
                 task.moveActivityToFrontLocked(act);
                 act.updateOptionsLocked(mOptions);
                 deliverNewIntent(act, intentGrants);
-                mTargetRootTask.mLastPausedActivity = null;
+                act.getTaskFragment().clearLastPausedActivity();
             } else {
                 mAddingToTask = true;
             }
@@ -2213,6 +2271,7 @@
         mPreferredWindowingMode = WINDOWING_MODE_UNDEFINED;
 
         mInTask = null;
+        mInTaskFragment = null;
         mAddingToTask = false;
         mReuseTask = null;
 
@@ -2224,7 +2283,6 @@
         mTargetTask = null;
         mMovedToFront = false;
         mNoAnimation = false;
-        mKeepCurTransition = false;
         mAvoidMoveToFront = false;
         mFrozeTaskList = false;
         mTransientLaunch = false;
@@ -2240,9 +2298,9 @@
     }
 
     private void setInitialState(ActivityRecord r, ActivityOptions options, Task inTask,
-            boolean doResume, int startFlags, ActivityRecord sourceRecord,
-            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
-            boolean restrictedBgActivity) {
+            TaskFragment inTaskFragment, boolean doResume, int startFlags,
+            ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession,
+            IVoiceInteractor voiceInteractor, boolean restrictedBgActivity) {
         reset(false /* clearRequest */);
 
         mStartActivity = r;
@@ -2305,7 +2363,7 @@
         // of this in the record so that we can skip it when trying to find
         // the top running activity.
         mDoResume = doResume;
-        if (!doResume || !r.okToShowLocked() || mLaunchTaskBehind) {
+        if (!doResume || !r.showToCurrentUser() || mLaunchTaskBehind) {
             r.delayedResume = true;
             mDoResume = false;
         }
@@ -2332,6 +2390,11 @@
             }
             mTransientLaunch = mOptions.getTransientLaunch();
             mTargetRootTask = Task.fromWindowContainerToken(mOptions.getLaunchRootTask());
+
+            if (inTaskFragment == null) {
+                inTaskFragment = TaskFragment.fromTaskFragmentToken(
+                        mOptions.getLaunchTaskFragmentToken(), mService);
+            }
         }
 
         mNotTop = (mLaunchFlags & FLAG_ACTIVITY_PREVIOUS_IS_TOP) != 0 ? sourceRecord : null;
@@ -2345,6 +2408,7 @@
             Slog.w(TAG, "Starting activity in task not in recents: " + inTask);
             mInTask = null;
         }
+        mInTaskFragment = inTaskFragment;
 
         mStartFlags = startFlags;
         // If the onlyIfNeeded flag is set, then we can do this if the activity being launched
@@ -2568,13 +2632,28 @@
     /**
      * Figure out which task and activity to bring to front when we have found an existing matching
      * activity record in history. May also clear the task if needed.
+     *
      * @param intentActivity Existing matching activity.
      * @return {@link ActivityRecord} brought to front.
      */
     private void setTargetRootTaskIfNeeded(ActivityRecord intentActivity) {
-        mTargetRootTask = intentActivity.getRootTask();
-        mTargetRootTask.mLastPausedActivity = null;
+        intentActivity.getTaskFragment().clearLastPausedActivity();
         Task intentTask = intentActivity.getTask();
+
+        // Only update the target-root-task when it is not indicated.
+        if (mTargetRootTask == null) {
+            if (mSourceRecord != null && mSourceRecord.mLaunchRootTask != null) {
+                // Inherit the target-root-task from source to ensure trampoline activities will be
+                // launched into the same root task.
+                mTargetRootTask = Task.fromWindowContainerToken(mSourceRecord.mLaunchRootTask);
+            } else {
+                final Task launchRootTask =
+                        getLaunchRootTask(mStartActivity, mLaunchFlags, intentTask, mOptions);
+                mTargetRootTask =
+                        launchRootTask != null ? launchRootTask : intentActivity.getRootTask();
+            }
+        }
+
         // If the target task is not in the front, then we need to bring it to the front...
         // except...  well, with SINGLE_TASK_LAUNCH it's not entirely clear. We'd like to have
         // the same behavior as if a new instance was being started, which means not bringing it
@@ -2602,16 +2681,14 @@
                     intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask());
                 }
 
-                final Task launchRootTask = getLaunchRootTask(mStartActivity, mLaunchFlags,
-                        intentTask, mOptions);
-                if (launchRootTask == null || launchRootTask == mTargetRootTask) {
+                if (mTargetRootTask == intentActivity.getRootTask()) {
                     // TODO(b/151572268): Figure out a better way to move tasks in above 2-levels
                     //  tasks hierarchies.
                     if (mTargetRootTask != intentTask
                             && mTargetRootTask != intentTask.getParent().asTask()) {
                         intentTask.getParent().positionChildAt(POSITION_TOP, intentTask,
                                 false /* includingParents */);
-                        intentTask = intentTask.getParent().asTask();
+                        intentTask = intentTask.getParent().asTaskFragment().getTask();
                     }
                     // If the task is in multi-windowing mode, the activity may already be on
                     // the top (visible to user but not the global top), then the result code
@@ -2627,8 +2704,12 @@
                             mStartActivity.appTimeTracker, DEFER_RESUME,
                             "bringingFoundTaskToFront");
                     mMovedToFront = !wasTopOfVisibleRootTask;
-                } else {
-                    intentTask.reparent(launchRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT,
+                } else if (intentActivity.getWindowingMode() != WINDOWING_MODE_PINNED) {
+                    // Leaves reparenting pinned task operations to task organizer to make sure it
+                    // dismisses pinned task properly.
+                    // TODO(b/199997762): Consider leaving all reparent operation of organized tasks
+                    //  to task organizer.
+                    intentTask.reparent(mTargetRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT,
                             ANIMATE, DEFER_RESUME, "reparentToTargetRootTask");
                     mMovedToFront = true;
                 }
@@ -2651,6 +2732,19 @@
         mTargetRootTask = intentActivity.getRootTask();
         mSupervisor.handleNonResizableTaskIfNeeded(intentTask, WINDOWING_MODE_UNDEFINED,
                 mRootWindowContainer.getDefaultTaskDisplayArea(), mTargetRootTask);
+
+        // We need to check if there is a launch root task in TDA for this target root task.
+        // If it exist, we need to reparent target root task from TDA to launch root task.
+        final TaskDisplayArea tda = mTargetRootTask.getDisplayArea();
+        final Task launchRootTask = tda.getLaunchRootTask(mTargetRootTask.getWindowingMode(),
+                mTargetRootTask.getActivityType(), null /** options */,
+                mSourceRootTask, 0 /** launchFlags */);
+        // If target root task is created by organizer, let organizer handle reparent itself.
+        if (!mTargetRootTask.mCreatedByOrganizer && launchRootTask != null
+                && launchRootTask != mTargetRootTask) {
+            mTargetRootTask.reparent(launchRootTask, POSITION_TOP);
+            mTargetRootTask = launchRootTask;
+        }
     }
 
     private void resumeTargetRootTaskIfNeeded() {
@@ -2678,7 +2772,7 @@
                 mNewTaskInfo != null ? mNewTaskInfo : mStartActivity.info,
                 mNewTaskIntent != null ? mNewTaskIntent : mIntent, mVoiceSession,
                 mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions);
-        mService.getTransitionController().collectExistenceChange(task);
+        task.mTransitionController.collectExistenceChange(task);
         addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask - mReuseTask");
 
         ProtoLog.v(WM_DEBUG_TASKS, "Starting new activity %s in new task %s",
@@ -2700,12 +2794,49 @@
         mIntentDelivered = true;
     }
 
-    private void addOrReparentStartingActivity(Task parent, String reason) {
-        if (mStartActivity.getTask() == null || mStartActivity.getTask() == parent) {
-            parent.addChild(mStartActivity);
+    private void addOrReparentStartingActivity(@NonNull Task task, String reason) {
+        TaskFragment newParent = task;
+        if (mInTaskFragment != null) {
+            // mInTaskFragment is created and added to the leaf task by task fragment organizer's
+            // request. If the task was resolved and different than mInTaskFragment, reparent the
+            // task to mInTaskFragment for embedding.
+            if (mInTaskFragment.getTask() != task) {
+                if (shouldReparentInTaskFragment(task)) {
+                    task.reparent(mInTaskFragment, POSITION_TOP);
+                }
+            } else {
+                newParent = mInTaskFragment;
+            }
         } else {
-            mStartActivity.reparent(parent, parent.getChildCount() /* top */, reason);
+            final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */,
+                    false /* includingEmbeddedTask */);
+            final TaskFragment taskFragment = top != null ? top.getTaskFragment() : null;
+            if (taskFragment != null && taskFragment.isEmbedded()
+                    && canEmbedActivity(taskFragment, mStartActivity, false /* newTask */, task)) {
+                // Use the embedded TaskFragment of the top activity as the new parent if the
+                // activity can be embedded.
+                newParent = top.getTaskFragment();
+            }
         }
+
+        if (mStartActivity.getTaskFragment() == null
+                || mStartActivity.getTaskFragment() == newParent) {
+            newParent.addChild(mStartActivity, POSITION_TOP);
+        } else {
+            mStartActivity.reparent(newParent, newParent.getChildCount() /* top */, reason);
+        }
+    }
+
+    private boolean shouldReparentInTaskFragment(Task task) {
+        // The task has not been embedded. We should reparent the task to TaskFragment.
+        if (!task.isEmbedded()) {
+            return true;
+        }
+        WindowContainer<?> parent = task.getParent();
+        // If the Activity is going to launch on top of embedded Task in the same TaskFragment,
+        // we don't need to reparent the Task. Otherwise, the embedded Task should reparent to
+        // another TaskFragment.
+        return parent.asTaskFragment() != mInTaskFragment;
     }
 
     private int adjustLaunchFlagsToDocumentMode(ActivityRecord r, boolean launchSingleInstance,
@@ -2936,6 +3067,11 @@
         return this;
     }
 
+    ActivityStarter setInTaskFragment(TaskFragment taskFragment) {
+        mRequest.inTaskFragment = taskFragment;
+        return this;
+    }
+
     ActivityStarter setWaitResult(WaitResult result) {
         mRequest.waitResult = result;
         return this;
@@ -3019,5 +3155,7 @@
         pw.print(mDoResume);
         pw.print(" mAddingToTask=");
         pw.println(mAddingToTask);
+        pw.print(" mInTaskFragment=");
+        pw.println(mInTaskFragment);
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 1759cde..3150ccd 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -30,6 +30,7 @@
 import android.content.res.CompatibilityInfo;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.LocaleList;
 import android.os.RemoteException;
 import android.service.voice.IVoiceInteractionSession;
 import android.util.IntArray;
@@ -595,11 +596,56 @@
     public abstract boolean isBaseOfLockedTask(String packageName);
 
     /**
-     * Create an interface to update configuration for an application.
+     * Creates an interface to update configuration for the calling application.
      */
     public abstract PackageConfigurationUpdater createPackageConfigurationUpdater();
 
     /**
+     * Creates an interface to update configuration for an arbitrary application specified by it's
+     * packageName and userId.
+     */
+    public abstract PackageConfigurationUpdater createPackageConfigurationUpdater(
+            String packageName, int userId);
+
+    /**
+     * Retrieves and returns the app-specific configuration for an arbitrary application specified
+     * by its packageName and userId. Returns null if no app-specific configuration has been set.
+     */
+    @Nullable
+    public abstract PackageConfig getApplicationConfig(String packageName,
+            int userId);
+
+    /**
+     * Holds app-specific configurations.
+     */
+    public static class PackageConfig {
+        /**
+         * nightMode for the application, null if app-specific nightMode is not set.
+         */
+        @Nullable
+        public final Integer mNightMode;
+
+        /**
+         * {@link LocaleList} for the application, null if app-specific locales are not set.
+         */
+        @Nullable
+        public final LocaleList mLocales;
+
+        PackageConfig(Integer nightMode, LocaleList locales) {
+            mNightMode = nightMode;
+            mLocales = locales;
+        }
+
+        /**
+         * Returns the string representation of the app-specific configuration.
+         */
+        @Override
+        public String toString() {
+            return "PackageConfig: nightMode " + mNightMode + " locales " + mLocales;
+        }
+    }
+
+    /**
      * An interface to update configuration for an application, and will persist override
      * configuration for this package.
      */
@@ -611,6 +657,14 @@
         PackageConfigurationUpdater setNightMode(int nightMode);
 
         /**
+         * Sets the app-specific locales for the application referenced by this updater.
+         * This setting is persisted and will overlay on top of the system locales for
+         * the said application.
+         * @return the current {@link PackageConfigurationUpdater} updated with the provided locale.
+         */
+        PackageConfigurationUpdater setLocales(LocaleList locales);
+
+        /**
          * Commit changes.
          */
         void commit();
@@ -621,4 +675,15 @@
      */
     public abstract boolean hasSystemAlertWindowPermission(int callingUid, int callingPid,
             String callingPackage);
+
+    /** Called when the device is waking up */
+    public abstract void notifyWakingUp();
+
+    /**
+     * Registers a callback which can intercept activity starts.
+     * @throws IllegalArgumentException if duplicate ids are provided
+     */
+    public abstract void registerActivityStartInterceptor(
+            @ActivityInterceptorCallback.OrderedId int id,
+            ActivityInterceptorCallback callback);
 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 08f3b1a..60a514e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -64,6 +64,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_WAKE;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS;
@@ -91,6 +92,8 @@
 import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.ScreenCompatPackage.PACKAGE;
 import static com.android.server.am.EventLogTags.writeBootProgressEnableScreen;
 import static com.android.server.am.EventLogTags.writeConfigurationChanged;
+import static com.android.server.wm.ActivityInterceptorCallback.FIRST_ORDERED_ID;
+import static com.android.server.wm.ActivityInterceptorCallback.LAST_ORDERED_ID;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ROOT_TASK;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH;
@@ -457,6 +460,8 @@
     /** The controller for all operations related to locktask. */
     private LockTaskController mLockTaskController;
     private ActivityStartController mActivityStartController;
+    private SparseArray<ActivityInterceptorCallback> mActivityInterceptorCallbacks =
+            new SparseArray<>();
     PackageConfigPersister mPackageConfigPersister;
 
     boolean mSuppressResizeConfigChanges;
@@ -652,16 +657,25 @@
      */
     volatile int mTopProcessState = ActivityManager.PROCESS_STATE_TOP;
 
+    /** Whether to keep higher priority to launch app while device is sleeping. */
+    private volatile boolean mRetainPowerModeAndTopProcessState;
+
+    /** The timeout to restore power mode if {@link #mRetainPowerModeAndTopProcessState} is set. */
+    private static final long POWER_MODE_UNKNOWN_VISIBILITY_TIMEOUT_MS = 1000;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
             POWER_MODE_REASON_START_ACTIVITY,
             POWER_MODE_REASON_FREEZE_DISPLAY,
+            POWER_MODE_REASON_UNKNOWN_VISIBILITY,
             POWER_MODE_REASON_ALL,
     })
     @interface PowerModeReason {}
 
     static final int POWER_MODE_REASON_START_ACTIVITY = 1 << 0;
     static final int POWER_MODE_REASON_FREEZE_DISPLAY = 1 << 1;
+    /** @see UnknownAppVisibilityController */
+    static final int POWER_MODE_REASON_UNKNOWN_VISIBILITY = 1 << 2;
     /** This can only be used by {@link #endLaunchPowerMode(int)}.*/
     static final int POWER_MODE_REASON_ALL = (1 << 2) - 1;
 
@@ -732,6 +746,7 @@
 
     WindowOrganizerController mWindowOrganizerController;
     TaskOrganizerController mTaskOrganizerController;
+    TaskFragmentOrganizerController mTaskFragmentOrganizerController;
 
     @Nullable
     private BackgroundActivityStartCallback mBackgroundActivityStartCallback;
@@ -805,6 +820,8 @@
         GL_ES_VERSION = SystemProperties.getInt("ro.opengles.version", GL_ES_VERSION_UNDEFINED);
         mWindowOrganizerController = new WindowOrganizerController(this);
         mTaskOrganizerController = mWindowOrganizerController.mTaskOrganizerController;
+        mTaskFragmentOrganizerController =
+                mWindowOrganizerController.mTaskFragmentOrganizerController;
     }
 
     public void onSystemReady() {
@@ -943,7 +960,7 @@
         setRecentTasks(new RecentTasks(this, mTaskSupervisor));
         mVrController = new VrController(mGlobalLock);
         mKeyguardController = mTaskSupervisor.getKeyguardController();
-        mPackageConfigPersister = new PackageConfigPersister(mTaskSupervisor.mPersisterQueue);
+        mPackageConfigPersister = new PackageConfigPersister(mTaskSupervisor.mPersisterQueue, this);
     }
 
     public void onActivityManagerInternalAdded() {
@@ -974,6 +991,7 @@
         synchronized (mGlobalLock) {
             mWindowManager = wm;
             mRootWindowContainer = wm.mRoot;
+            mWindowOrganizerController.setWindowManager(wm);
             mTempConfig.setToDefaults();
             mTempConfig.setLocales(LocaleList.getDefault());
             mConfigurationSeq = mTempConfig.seq = 1;
@@ -1102,6 +1120,10 @@
         return mBackgroundActivityStartCallback;
     }
 
+    SparseArray<ActivityInterceptorCallback> getActivityInterceptorCallbacks() {
+        return mActivityInterceptorCallbacks;
+    }
+
     private void start() {
         LocalServices.addService(ActivityTaskManagerInternal.class, mInternal);
     }
@@ -1222,8 +1244,8 @@
             // If this is coming from the currently resumed activity, it is
             // effectively saying that app switches are allowed at this point.
             final Task topFocusedRootTask = getTopDisplayFocusedRootTask();
-            if (topFocusedRootTask != null && topFocusedRootTask.getResumedActivity() != null
-                    && topFocusedRootTask.getResumedActivity().info.applicationInfo.uid
+            if (topFocusedRootTask != null && topFocusedRootTask.getTopResumedActivity() != null
+                    && topFocusedRootTask.getTopResumedActivity().info.applicationInfo.uid
                     == Binder.getCallingUid()) {
                 mAppSwitchesAllowed = true;
             }
@@ -1537,7 +1559,7 @@
                 sourceToken = resultTo;
             }
 
-            sourceRecord = mRootWindowContainer.isInAnyTask(sourceToken);
+            sourceRecord = ActivityRecord.isInAnyTask(sourceToken);
             if (sourceRecord == null) {
                 throw new SecurityException("Called with bad activity token: " + sourceToken);
             }
@@ -1719,10 +1741,8 @@
         final SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(bOptions);
         final long origId = Binder.clearCallingIdentity();
         try {
-            synchronized (mGlobalLock) {
-                return mTaskSupervisor.startActivityFromRecents(callingPid, callingUid, taskId,
-                        safeOptions);
-            }
+            return mTaskSupervisor.startActivityFromRecents(callingPid, callingUid, taskId,
+                    safeOptions);
         } finally {
             Binder.restoreCallingIdentity(origId);
         }
@@ -1881,25 +1901,42 @@
     @Override
     public void setFocusedTask(int taskId) {
         enforceTaskPermission("setFocusedTask()");
-        ProtoLog.d(WM_DEBUG_FOCUS, "setFocusedTask: taskId=%d", taskId);
         final long callingId = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
-                final Task task = mRootWindowContainer.anyTaskForId(taskId,
-                        MATCH_ATTACHED_TASK_ONLY);
-                if (task == null) {
-                    return;
-                }
-                final ActivityRecord r = task.topRunningActivityLocked();
-                if (r != null && r.moveFocusableActivityToTop("setFocusedTask")) {
-                    mRootWindowContainer.resumeFocusedTasksTopActivities();
-                }
+                setFocusedTask(taskId, null /* touchedActivity */);
             }
         } finally {
             Binder.restoreCallingIdentity(callingId);
         }
     }
 
+    void setFocusedTask(int taskId, ActivityRecord touchedActivity) {
+        ProtoLog.d(WM_DEBUG_FOCUS, "setFocusedTask: taskId=%d touchedActivity=%s", taskId,
+                touchedActivity);
+        final Task task = mRootWindowContainer.anyTaskForId(taskId, MATCH_ATTACHED_TASK_ONLY);
+        if (task == null) {
+            return;
+        }
+        final ActivityRecord r = task.topRunningActivityLocked();
+        if (r == null) {
+            return;
+        }
+
+        if (r.moveFocusableActivityToTop("setFocusedTask")) {
+            mRootWindowContainer.resumeFocusedTasksTopActivities();
+        } else if (touchedActivity != null && touchedActivity.isFocusable()) {
+            final TaskFragment parent = touchedActivity.getTaskFragment();
+            if (parent != null && parent.isEmbedded()) {
+                // Set the focused app directly if the focused window is currently embedded
+                final DisplayContent displayContent = touchedActivity.getDisplayContent();
+                displayContent.setFocusedApp(touchedActivity);
+                mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
+                        true /* updateInputWindows */);
+            }
+        }
+    }
+
     @Override
     public boolean removeTask(int taskId) {
         mAmInternal.enforceCallingPermission(REMOVE_TASKS, "removeTask()");
@@ -3431,7 +3468,6 @@
 
     @Override
     public IWindowOrganizerController getWindowOrganizerController() {
-        enforceTaskPermission("getWindowOrganizerController()");
         return mWindowOrganizerController;
     }
 
@@ -3775,6 +3811,20 @@
         }
     }
 
+    @Override
+    public void detachNavigationBarFromApp(@NonNull IBinder transition) {
+        mAmInternal.enforceCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
+                "detachNavigationBarFromApp");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                getTransitionController().legacyDetachNavigationBarFromApp(transition);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     void dumpLastANRLocked(PrintWriter pw) {
         pw.println("ACTIVITY MANAGER LAST ANR (dumpsys activity lastanr)");
         if (mLastANRState == null) {
@@ -4038,6 +4088,9 @@
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
+                // Window configuration is unrelated to persistent configuration (e.g. font scale,
+                // locale). Unset it to avoid affecting the current display configuration.
+                values.windowConfiguration.setToDefaults();
                 updateConfigurationLocked(values, null, false, true, userId,
                         false /* deferResume */);
             }
@@ -4217,15 +4270,39 @@
     }
 
     void startLaunchPowerMode(@PowerModeReason int reason) {
-        if (mPowerManagerInternal == null) return;
-        mPowerManagerInternal.setPowerMode(Mode.LAUNCH, true);
+        if (mPowerManagerInternal != null) {
+            mPowerManagerInternal.setPowerMode(Mode.LAUNCH, true);
+        }
         mLaunchPowerModeReasons |= reason;
+        if ((reason & POWER_MODE_REASON_UNKNOWN_VISIBILITY) != 0) {
+            if (mRetainPowerModeAndTopProcessState) {
+                mH.removeMessages(H.END_POWER_MODE_UNKNOWN_VISIBILITY_MSG);
+            }
+            mRetainPowerModeAndTopProcessState = true;
+            mH.sendEmptyMessageDelayed(H.END_POWER_MODE_UNKNOWN_VISIBILITY_MSG,
+                    POWER_MODE_UNKNOWN_VISIBILITY_TIMEOUT_MS);
+            Slog.d(TAG, "Temporarily retain top process state for launching app");
+        }
     }
 
     void endLaunchPowerMode(@PowerModeReason int reason) {
-        if (mPowerManagerInternal == null || mLaunchPowerModeReasons == 0) return;
+        if (mLaunchPowerModeReasons == 0) return;
         mLaunchPowerModeReasons &= ~reason;
-        if (mLaunchPowerModeReasons == 0) {
+
+        if ((mLaunchPowerModeReasons & POWER_MODE_REASON_UNKNOWN_VISIBILITY) != 0) {
+            boolean allResolved = true;
+            for (int i = mRootWindowContainer.getChildCount() - 1; i >= 0; i--) {
+                allResolved &= mRootWindowContainer.getChildAt(i).mUnknownAppVisibilityController
+                        .allResolved();
+            }
+            if (allResolved) {
+                mLaunchPowerModeReasons &= ~POWER_MODE_REASON_UNKNOWN_VISIBILITY;
+                mRetainPowerModeAndTopProcessState = false;
+                mH.removeMessages(H.END_POWER_MODE_UNKNOWN_VISIBILITY_MSG);
+            }
+        }
+
+        if (mLaunchPowerModeReasons == 0 && mPowerManagerInternal != null) {
             mPowerManagerInternal.setPowerMode(Mode.LAUNCH, false);
         }
     }
@@ -4676,7 +4753,7 @@
                                     mContext.getText(R.string.heavy_weight_notification_detail))
                             .setContentIntent(PendingIntent.getActivityAsUser(mContext, 0,
                                     intent, PendingIntent.FLAG_CANCEL_CURRENT
-                                    | PendingIntent.FLAG_IMMUTABLE, null,
+                                            | PendingIntent.FLAG_IMMUTABLE, null,
                                     new UserHandle(userId)))
                             .build();
             try {
@@ -5061,9 +5138,38 @@
         process.registerDisplayAreaConfigurationListener(imeContainer);
     }
 
+    @Override
+    public void setRunningRemoteTransitionDelegate(IApplicationThread caller) {
+        mAmInternal.enforceCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
+                "setRunningRemoteTransition");
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
+        synchronized (mGlobalLock) {
+            // Also only allow a process which is already runningRemoteAnimation to mark another
+            // process.
+            final WindowProcessController callingProc = getProcessController(callingPid,
+                    callingUid);
+            if (callingProc == null || !callingProc.isRunningRemoteTransition()) {
+                final String msg = "Can't call setRunningRemoteTransition from a process (pid="
+                        + callingPid + " uid=" + callingUid + ") which isn't itself running a "
+                        + "remote transition.";
+                Slog.e(TAG, msg);
+                throw new SecurityException(msg);
+            }
+            final WindowProcessController wpc = getProcessController(caller);
+            if (wpc == null) {
+                Slog.w(TAG, "Unable to find process for application " + caller);
+                return;
+            }
+            wpc.setRunningRemoteAnimation(true /* running */);
+            callingProc.addRemoteAnimationDelegate(wpc);
+        }
+    }
+
     final class H extends Handler {
         static final int REPORT_TIME_TRACKER_MSG = 1;
         static final int UPDATE_PROCESS_ANIMATING_STATE = 2;
+        static final int END_POWER_MODE_UNKNOWN_VISIBILITY_MSG = 3;
 
         static final int FIRST_ACTIVITY_TASK_MSG = 100;
         static final int FIRST_SUPERVISOR_TASK_MSG = 200;
@@ -5087,6 +5193,20 @@
                     }
                 }
                 break;
+                case END_POWER_MODE_UNKNOWN_VISIBILITY_MSG: {
+                    synchronized (mGlobalLock) {
+                        mRetainPowerModeAndTopProcessState = false;
+                        endLaunchPowerMode(POWER_MODE_REASON_UNKNOWN_VISIBILITY);
+                        if (mTopApp != null
+                                && mTopProcessState == ActivityManager.PROCESS_STATE_TOP_SLEEPING) {
+                            // Restore the scheduling group for sleeping.
+                            mTopApp.updateProcessInfo(false /* updateServiceConnection */,
+                                    false /* activityChange */, true /* updateOomAdj */,
+                                    false /* addPendingTopUid */);
+                        }
+                    }
+                }
+                break;
             }
         }
     }
@@ -5405,6 +5525,11 @@
         @HotPath(caller = HotPath.OOM_ADJUSTMENT)
         @Override
         public int getTopProcessState() {
+            if (mRetainPowerModeAndTopProcessState) {
+                // There is a launching app while device may be sleeping, force the top state so
+                // the launching process can have top-app scheduling group.
+                return ActivityManager.PROCESS_STATE_TOP;
+            }
             return mTopProcessState;
         }
 
@@ -6442,7 +6567,22 @@
 
         @Override
         public PackageConfigurationUpdater createPackageConfigurationUpdater() {
-            return new PackageConfigurationUpdaterImpl(Binder.getCallingPid());
+            return new PackageConfigurationUpdaterImpl(Binder.getCallingPid(),
+                    ActivityTaskManagerService.this);
+        }
+
+        @Override
+        public PackageConfigurationUpdater createPackageConfigurationUpdater(
+                String packageName , int userId) {
+            return new PackageConfigurationUpdaterImpl(packageName, userId,
+                    ActivityTaskManagerService.this);
+        }
+
+        @Override
+        @Nullable
+        public ActivityTaskManagerInternal.PackageConfig getApplicationConfig(String packageName,
+                int userId) {
+            return mPackageConfigPersister.findPackageConfiguration(packageName, userId);
         }
 
         @Override
@@ -6451,44 +6591,29 @@
             return ActivityTaskManagerService.this.hasSystemAlertWindowPermission(callingUid,
                     callingPid, callingPackage);
         }
-    }
 
-    final class PackageConfigurationUpdaterImpl implements
-            ActivityTaskManagerInternal.PackageConfigurationUpdater {
-        private final int mPid;
-        private int mNightMode;
-
-        PackageConfigurationUpdaterImpl(int pid) {
-            mPid = pid;
+        @Override
+        public void notifyWakingUp() {
+            // Start a transition for waking. This is needed for showWhenLocked activities.
+            getTransitionController().requestTransitionIfNeeded(TRANSIT_WAKE, 0 /* flags */,
+                    null /* trigger */, mRootWindowContainer.getDefaultDisplay());
         }
 
         @Override
-        public ActivityTaskManagerInternal.PackageConfigurationUpdater setNightMode(int nightMode) {
-            mNightMode = nightMode;
-            return this;
-        }
-
-        @Override
-        public void commit() {
+        public void registerActivityStartInterceptor(
+                @ActivityInterceptorCallback.OrderedId int id,
+                ActivityInterceptorCallback callback) {
             synchronized (mGlobalLock) {
-                final long ident = Binder.clearCallingIdentity();
-                try {
-                    final WindowProcessController wpc = mProcessMap.getProcess(mPid);
-                    if (wpc == null) {
-                        Slog.w(TAG, "Override application configuration: cannot find pid " + mPid);
-                        return;
-                    }
-                    wpc.setOverrideNightMode(mNightMode);
-                    wpc.updateNightModeForAllActivities(mNightMode);
-                    mPackageConfigPersister.updateFromImpl(wpc.mName, wpc.mUserId, this);
-                } finally {
-                    Binder.restoreCallingIdentity(ident);
+                if (mActivityInterceptorCallbacks.contains(id)) {
+                    throw new IllegalArgumentException("Duplicate id provided: " + id);
                 }
+                if (id > LAST_ORDERED_ID || id < FIRST_ORDERED_ID) {
+                    throw new IllegalArgumentException(
+                            "Provided id " + id + " is not in range of valid ids ["
+                                    + FIRST_ORDERED_ID + "," + LAST_ORDERED_ID + "]");
+                }
+                mActivityInterceptorCallbacks.put(id, callback);
             }
         }
-
-        int getNightMode() {
-            return mNightMode;
-        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index efa67e9..ba30592 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -49,6 +49,9 @@
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_CLEANUP;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_IDLE;
@@ -73,8 +76,6 @@
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK;
 import static com.android.server.wm.Task.REPARENT_KEEP_ROOT_TASK_AT_FRONT;
 import static com.android.server.wm.Task.TAG_CLEANUP;
@@ -140,7 +141,6 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.ReferrerIntent;
-import com.android.internal.os.TransferPipe;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.function.pooled.PooledConsumer;
@@ -151,7 +151,6 @@
 import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
 
 import java.io.FileDescriptor;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -353,6 +352,12 @@
      */
     private int mVisibilityTransactionDepth;
 
+    /**
+     * Whether to the visibility updates that started from {@code RootWindowContainer} should be
+     * deferred.
+     */
+    private boolean mDeferRootVisibilityUpdate;
+
     private ActivityMetricsLogger mActivityMetricsLogger;
 
     /** Check if placing task or activity on specified display is allowed. */
@@ -836,6 +841,12 @@
 
                 logIfTransactionTooLarge(r.intent, r.getSavedState());
 
+                if (r.isEmbedded()) {
+                    // Sending TaskFragmentInfo to client to ensure the info is updated before
+                    // the activity creation.
+                    mService.mTaskFragmentOrganizerController.dispatchPendingInfoChangedEvent(
+                            r.getOrganizedTaskFragment());
+                }
 
                 // Create activity launch transaction.
                 final ClientTransaction clientTransaction = ClientTransaction.obtain(
@@ -1376,8 +1387,9 @@
                 mUserLeaving = true;
             }
 
-            mService.getTransitionController().requestTransitionIfNeeded(TRANSIT_TO_FRONT,
-                    0 /* flags */, task, options != null ? options.getRemoteTransition() : null);
+            task.mTransitionController.requestTransitionIfNeeded(TRANSIT_TO_FRONT,
+                    0 /* flags */, task, task /* readyGroupRef */,
+                    options != null ? options.getRemoteTransition() : null);
             reason = reason + " findTaskToMoveToFront";
             boolean reparented = false;
             if (task.isResizeable() && canUseActivityOptionsLaunchBounds(options)) {
@@ -1551,11 +1563,17 @@
             return;
         }
         if (task.isVisible()) {
-            mService.getTransitionController().requestTransitionIfNeeded(TRANSIT_CLOSE, task);
+            if (task.mTransitionController.isCollecting()) {
+                // We don't want the finishing to change the transition ready state since there will
+                // not be corresponding setReady for finishing.
+                task.mTransitionController.collectExistenceChange(task);
+            } else {
+                task.mTransitionController.requestTransitionIfNeeded(TRANSIT_CLOSE, task);
+            }
         } else {
             // Removing a non-visible task doesn't require a transition, but if there is one
             // collecting, this should be a member just in case.
-            mService.getTransitionController().collect(task);
+            task.mTransitionController.collect(task);
         }
         task.mInRemoveTask = true;
         try {
@@ -1856,12 +1874,6 @@
         mHandler.obtainMessage(LAUNCH_TASK_BEHIND_COMPLETE, token).sendToTarget();
     }
 
-    /** Checks whether the userid is a profile of the current user. */
-    boolean isCurrentProfileLocked(int userId) {
-        if (userId == mRootWindowContainer.mCurrentUser) return true;
-        return mService.mAmInternal.isCurrentProfile(userId);
-    }
-
     /**
      * Processes the activities to be stopped or destroyed. This should be called when the resumed
      * activities are idle or drawn.
@@ -1875,7 +1887,7 @@
             final ActivityRecord s = mStoppingActivities.get(i);
             final boolean animating = s.isAnimating(TRANSITION | PARENTS,
                     ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)
-                    || mService.getTransitionController().inTransition(s);
+                    || s.inTransition();
             ProtoLog.v(WM_DEBUG_STATES, "Stopping %s: nowVisible=%b animating=%b "
                     + "finishing=%s", s, s.nowVisible, animating, s.finishing);
             if (!animating || mService.mShuttingDown) {
@@ -1930,6 +1942,7 @@
     void removeHistoryRecords(WindowProcessController app) {
         removeHistoryRecords(mStoppingActivities, app, "mStoppingActivities");
         removeHistoryRecords(mFinishingActivities, app, "mFinishingActivities");
+        removeHistoryRecords(mNoHistoryActivities, app, "mNoHistoryActivities");
     }
 
     private void removeHistoryRecords(ArrayList<ActivityRecord> list, WindowProcessController app,
@@ -1967,6 +1980,7 @@
                 mWaitingActivityLaunched.get(i).dump(pw, prefix + "  ");
             }
         }
+        pw.println(prefix + "mNoHistoryActivities=" + mNoHistoryActivities);
         pw.println();
     }
 
@@ -1991,76 +2005,14 @@
     static boolean dumpHistoryList(FileDescriptor fd, PrintWriter pw, List<ActivityRecord> list,
             String prefix, String label, boolean complete, boolean brief, boolean client,
             String dumpPackage, boolean needNL, Runnable header, Task lastTask) {
-        String innerPrefix = null;
-        String[] args = null;
         boolean printed = false;
-        for (int i=list.size()-1; i>=0; i--) {
+        for (int i = list.size() - 1; i >= 0; i--) {
             final ActivityRecord r = list.get(i);
-            if (dumpPackage != null && !dumpPackage.equals(r.packageName)) {
-                continue;
-            }
-            if (innerPrefix == null) {
-                innerPrefix = prefix + "      ";
-                args = new String[0];
-            }
-            printed = true;
-            final boolean full = !brief && (complete || !r.isInHistory());
-            if (needNL) {
-                pw.println("");
-                needNL = false;
-            }
-            if (header != null) {
-                header.run();
-                header = null;
-            }
-            if (lastTask != r.getTask()) {
-                lastTask = r.getTask();
-                pw.print(prefix);
-                pw.print(full ? "* " : "  ");
-                pw.println(lastTask);
-                if (full) {
-                    lastTask.dump(pw, prefix + "  ");
-                } else if (complete) {
-                    // Complete + brief == give a summary.  Isn't that obvious?!?
-                    if (lastTask.intent != null) {
-                        pw.print(prefix); pw.print("  ");
-                                pw.println(lastTask.intent.toInsecureString());
-                    }
-                }
-            }
-            pw.print(prefix); pw.print(full ? "  * " : "    "); pw.print(label);
-            pw.print(" #"); pw.print(i); pw.print(": ");
-            pw.println(r);
-            if (full) {
-                r.dump(pw, innerPrefix, true /* dumpAll */);
-            } else if (complete) {
-                // Complete + brief == give a summary.  Isn't that obvious?!?
-                pw.print(innerPrefix); pw.println(r.intent.toInsecureString());
-                if (r.app != null) {
-                    pw.print(innerPrefix); pw.println(r.app);
-                }
-            }
-            if (client && r.attachedToProcess()) {
-                // flush anything that is already in the PrintWriter since the thread is going
-                // to write to the file descriptor directly
-                pw.flush();
-                try {
-                    TransferPipe tp = new TransferPipe();
-                    try {
-                        r.app.getThread().dumpActivity(
-                                tp.getWriteFd(), r.appToken, innerPrefix, args);
-                        // Short timeout, since blocking here can deadlock with the application.
-                        tp.go(fd, 2000);
-                    } finally {
-                        tp.kill();
-                    }
-                } catch (IOException e) {
-                    pw.println(innerPrefix + "Failure while dumping the activity: " + e);
-                } catch (RemoteException e) {
-                    pw.println(innerPrefix + "Got a RemoteException while dumping the activity");
-                }
-                needNL = true;
-            }
+            ActivityRecord.dumpActivity(fd, pw, i, r, prefix, label, complete, brief,
+                    client, dumpPackage, needNL, header, lastTask);
+            lastTask = r.getTask();
+            header = null;
+            needNL = client && r.attachedToProcess();
         }
         return printed;
     }
@@ -2087,7 +2039,7 @@
     void updateTopResumedActivityIfNeeded() {
         final ActivityRecord prevTopActivity = mTopResumedActivity;
         final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
-        if (topRootTask == null || topRootTask.getResumedActivity() == prevTopActivity) {
+        if (topRootTask == null || topRootTask.getTopResumedActivity() == prevTopActivity) {
             if (mService.isSleepingLocked()) {
                 // There won't be a next resumed activity. The top process should still be updated
                 // according to the current top focused activity.
@@ -2109,7 +2061,7 @@
         }
 
         // Update the current top activity.
-        mTopResumedActivity = topRootTask.getResumedActivity();
+        mTopResumedActivity = topRootTask.getTopResumedActivity();
         scheduleTopResumedActivityStateIfNeeded();
 
         mService.updateTopApp(mTopResumedActivity);
@@ -2236,7 +2188,7 @@
         }
 
         if (!task.supportsSplitScreenWindowingMode() || forceNonResizable) {
-            if (mService.getTransitionController().getTransitionPlayer() != null) return;
+            if (task.mTransitionController.isShellTransitionsEnabled()) return;
             // Dismiss docked root task. If task appeared to be in docked root task but is not
             // resizable - we need to move it to top of fullscreen root task, otherwise it will
             // be covered.
@@ -2349,6 +2301,14 @@
         return mVisibilityTransactionDepth > 0;
     }
 
+    void setDeferRootVisibilityUpdate(boolean deferUpdate) {
+        mDeferRootVisibilityUpdate = deferUpdate;
+    }
+
+    boolean isRootVisibilityUpdateDeferred() {
+        return mDeferRootVisibilityUpdate;
+    }
+
     /**
      * Called when the state or visibility of an attached activity is changed.
      *
@@ -2416,8 +2376,7 @@
                     String processName = null;
                     int uid = 0;
                     synchronized (mService.mGlobalLock) {
-                        if (r.attachedToProcess()
-                                && r.isState(Task.ActivityState.RESTARTING_PROCESS)) {
+                        if (r.attachedToProcess() && r.isState(RESTARTING_PROCESS)) {
                             processName = r.app.mName;
                             uid = r.app.mUid;
                         }
@@ -2517,102 +2476,126 @@
         }
     }
 
+    /**
+     * Start the given task from the recent tasks. Do not hold WM global lock when calling this
+     * method to avoid potential deadlock or permission deny by UriGrantsManager when resolving
+     * activity (see {@link ActivityStarter.Request#resolveActivity} and
+     * {@link com.android.server.am.ContentProviderHelper#checkContentProviderUriPermission}).
+     *
+     * @return The result code of starter.
+     */
     int startActivityFromRecents(int callingPid, int callingUid, int taskId,
             SafeActivityOptions options) {
-        Task task = null;
+        final Task task;
+        final int taskCallingUid;
         final String callingPackage;
         final String callingFeatureId;
         final Intent intent;
         final int userId;
-        int activityType = ACTIVITY_TYPE_UNDEFINED;
-        int windowingMode = WINDOWING_MODE_UNDEFINED;
         final ActivityOptions activityOptions = options != null
                 ? options.getOptions(this)
                 : null;
         boolean moveHomeTaskForward = true;
-        if (activityOptions != null) {
-            activityType = activityOptions.getLaunchActivityType();
-            windowingMode = activityOptions.getLaunchWindowingMode();
-            if (activityOptions.freezeRecentTasksReordering()
-                    && mRecentTasks.isCallerRecents(callingUid)) {
-                mRecentTasks.setFreezeTaskListReordering();
+        synchronized (mService.mGlobalLock) {
+            int activityType = ACTIVITY_TYPE_UNDEFINED;
+            if (activityOptions != null) {
+                activityType = activityOptions.getLaunchActivityType();
+                final int windowingMode = activityOptions.getLaunchWindowingMode();
+                if (activityOptions.freezeRecentTasksReordering()
+                        && mRecentTasks.isCallerRecents(callingUid)) {
+                    mRecentTasks.setFreezeTaskListReordering();
+                }
+                if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+                        || activityOptions.getLaunchRootTask() != null) {
+                    // Don't move home activity forward if we are launching into primary split or
+                    // there is a launch root set.
+                    moveHomeTaskForward = false;
+                }
             }
-            if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
-                    || activityOptions.getLaunchRootTask() != null) {
-                // Don't move home activity forward if we are launching into primary split or there
-                // is a launch root set.
-                moveHomeTaskForward = false;
-            }
-        }
-        if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) {
-            throw new IllegalArgumentException("startActivityFromRecents: Task "
-                    + taskId + " can't be launch in the home/recents root task.");
-        }
-
-        mService.deferWindowLayout();
-        try {
-            task = mRootWindowContainer.anyTaskForId(taskId,
-                    MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE, activityOptions, ON_TOP);
-            if (task == null) {
-                mWindowManager.executeAppTransition();
-                throw new IllegalArgumentException(
-                        "startActivityFromRecents: Task " + taskId + " not found.");
+            if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) {
+                throw new IllegalArgumentException("startActivityFromRecents: Task "
+                        + taskId + " can't be launch in the home/recents root task.");
             }
 
-            if (moveHomeTaskForward) {
-                // We always want to return to the home activity instead of the recents activity
-                // from whatever is started from the recents activity, so move the home root task
-                // forward.
-                // TODO (b/115289124): Multi-display supports for recents.
-                mRootWindowContainer.getDefaultTaskDisplayArea().moveHomeRootTaskToFront(
-                        "startActivityFromRecents");
-            }
-
-            // If the user must confirm credentials (e.g. when first launching a work app and the
-            // Work Challenge is present) let startActivityInPackage handle the intercepting.
-            if (!mService.mAmInternal.shouldConfirmCredentials(task.mUserId)
-                    && task.getRootActivity() != null) {
-                final ActivityRecord targetActivity = task.getTopNonFinishingActivity();
-
-                mRootWindowContainer.startPowerModeLaunchIfNeeded(
-                        true /* forceSend */, targetActivity);
-                final LaunchingState launchingState =
-                        mActivityMetricsLogger.notifyActivityLaunching(task.intent);
-                try {
-                    mService.moveTaskToFrontLocked(null /* appThread */, null /* callingPackage */,
-                            task.mTaskId, 0, options);
-                    // Apply options to prevent pendingOptions be taken when scheduling activity
-                    // lifecycle transaction to make sure the override pending app transition will
-                    // be applied immediately.
-                    targetActivity.applyOptionsAnimation();
-                } finally {
-                    mActivityMetricsLogger.notifyActivityLaunched(launchingState,
-                            START_TASK_TO_FRONT, false /* newActivityCreated */, targetActivity,
-                            activityOptions);
+            boolean shouldStartActivity = false;
+            mService.deferWindowLayout();
+            try {
+                task = mRootWindowContainer.anyTaskForId(taskId,
+                        MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE, activityOptions, ON_TOP);
+                if (task == null) {
+                    mWindowManager.executeAppTransition();
+                    throw new IllegalArgumentException(
+                            "startActivityFromRecents: Task " + taskId + " not found.");
                 }
 
-                mService.getActivityStartController().postStartActivityProcessingForLastStarter(
-                        task.getTopNonFinishingActivity(), ActivityManager.START_TASK_TO_FRONT,
-                        task.getRootTask());
+                if (moveHomeTaskForward) {
+                    // We always want to return to the home activity instead of the recents
+                    // activity from whatever is started from the recents activity, so move
+                    // the home root task forward.
+                    // TODO (b/115289124): Multi-display supports for recents.
+                    mRootWindowContainer.getDefaultTaskDisplayArea().moveHomeRootTaskToFront(
+                            "startActivityFromRecents");
+                }
 
-                // As it doesn't go to ActivityStarter.executeRequest() path, we need to resume
-                // app switching here also.
-                mService.resumeAppSwitches();
+                // If the user must confirm credentials (e.g. when first launching a work
+                // app and the Work Challenge is present) let startActivityInPackage handle
+                // the intercepting.
+                if (!mService.mAmInternal.shouldConfirmCredentials(task.mUserId)
+                        && task.getRootActivity() != null) {
+                    final ActivityRecord targetActivity = task.getTopNonFinishingActivity();
 
-                return ActivityManager.START_TASK_TO_FRONT;
+                    mRootWindowContainer.startPowerModeLaunchIfNeeded(
+                            true /* forceSend */, targetActivity);
+                    final LaunchingState launchingState =
+                            mActivityMetricsLogger.notifyActivityLaunching(task.intent);
+                    try {
+                        mService.moveTaskToFrontLocked(null /* appThread */,
+                                null /* callingPackage */, task.mTaskId, 0, options);
+                        // Apply options to prevent pendingOptions be taken when scheduling
+                        // activity lifecycle transaction to make sure the override pending app
+                        // transition will be applied immediately.
+                        targetActivity.applyOptionsAnimation();
+                    } finally {
+                        mActivityMetricsLogger.notifyActivityLaunched(launchingState,
+                                START_TASK_TO_FRONT, false /* newActivityCreated */,
+                                targetActivity, activityOptions);
+                    }
+
+                    mService.getActivityStartController().postStartActivityProcessingForLastStarter(
+                            task.getTopNonFinishingActivity(), ActivityManager.START_TASK_TO_FRONT,
+                            task.getRootTask());
+
+                    // As it doesn't go to ActivityStarter.executeRequest() path, we need to resume
+                    // app switching here also.
+                    mService.resumeAppSwitches();
+                    return ActivityManager.START_TASK_TO_FRONT;
+                }
+                // The task is empty or needs to show the confirmation for credential.
+                shouldStartActivity = true;
+            } finally {
+                if (!shouldStartActivity) {
+                    mService.continueWindowLayout();
+                }
             }
+            taskCallingUid = task.mCallingUid;
             callingPackage = task.mCallingPackage;
             callingFeatureId = task.mCallingFeatureId;
             intent = task.intent;
             intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
             userId = task.mUserId;
-            return mService.getActivityStartController().startActivityInPackage(task.mCallingUid,
+        }
+        // ActivityStarter will acquire the lock where the places need, so execute the request
+        // outside of the lock.
+        try {
+            return mService.getActivityStartController().startActivityInPackage(taskCallingUid,
                     callingPid, callingUid, callingPackage, callingFeatureId, intent, null, null,
                     null, 0, 0, options, userId, task, "startActivityFromRecents",
                     false /* validateIncomingUser */, null /* originatingPendingIntent */,
                     false /* allowBackgroundActivityStart */);
         } finally {
-            mService.continueWindowLayout();
+            synchronized (mService.mGlobalLock) {
+                mService.continueWindowLayout();
+            }
         }
     }
 
@@ -2635,7 +2618,10 @@
         }
 
         boolean matches(ActivityRecord r) {
-            return mTargetComponent.equals(r.mActivityComponent) || mLaunchingState.contains(r);
+            if (!mLaunchingState.hasActiveTransitionInfo()) {
+                return mTargetComponent.equals(r.mActivityComponent);
+            }
+            return mLaunchingState.contains(r);
         }
 
         void dump(PrintWriter pw, String prefix) {
diff --git a/services/core/java/com/android/server/wm/AnimationAdapter.java b/services/core/java/com/android/server/wm/AnimationAdapter.java
index 529c4f6..5899a4e 100644
--- a/services/core/java/com/android/server/wm/AnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/AnimationAdapter.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import android.annotation.NonNull;
 import android.util.proto.ProtoOutputStream;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
@@ -52,7 +53,7 @@
      * @param finishCallback The callback to be invoked when the animation has finished.
      */
     void startAnimation(SurfaceControl animationLeash, Transaction t, @AnimationType int type,
-            OnAnimationFinishedCallback finishCallback);
+            @NonNull OnAnimationFinishedCallback finishCallback);
 
     /**
      * Called when the animation that was started with {@link #startAnimation} was cancelled by the
diff --git a/services/core/java/com/android/server/wm/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java
index 892db9c..c881864 100644
--- a/services/core/java/com/android/server/wm/AnrController.java
+++ b/services/core/java/com/android/server/wm/AnrController.java
@@ -31,7 +31,6 @@
 import android.view.InputApplicationHandle;
 
 import com.android.server.am.ActivityManagerService;
-import com.android.server.wm.EmbeddedWindowController.EmbeddedWindow;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -81,21 +80,19 @@
         final boolean aboveSystem;
         final ActivityRecord activity;
         synchronized (mService.mGlobalLock) {
-            WindowState windowState = mService.mInputToWindowMap.get(inputToken);
-            if (windowState != null) {
-                pid = windowState.mSession.mPid;
-                activity = windowState.mActivityRecord;
-                Slog.i(TAG_WM, "ANR in " + windowState.mAttrs.getTitle() + ". Reason:" + reason);
-            } else {
-                EmbeddedWindow embeddedWindow = mService.mEmbeddedWindowController.get(inputToken);
-                if (embeddedWindow == null) {
-                    Slog.e(TAG_WM, "Unknown token, dropping notifyConnectionUnresponsive request");
-                    return;
-                }
-                pid = embeddedWindow.mOwnerPid;
-                windowState = embeddedWindow.mHostWindowState;
-                activity = null; // Don't blame the host process, instead blame the embedded pid.
+            InputTarget target = mService.getInputTargetFromToken(inputToken);
+            if (target == null) {
+                Slog.e(TAG_WM, "Unknown token, dropping notifyConnectionUnresponsive request");
+                return;
             }
+
+            WindowState windowState = target.getWindowState();
+            pid = target.getPid();
+            // Blame the activity if the input token belongs to the window. If the target is
+            // embedded, then we will blame the pid instead.
+            activity = (windowState.mInputChannelToken == inputToken)
+                    ? windowState.mActivityRecord : null;
+            Slog.i(TAG_WM, "ANR in " + target + ". Reason:" + reason);
             aboveSystem = isWindowAboveSystem(windowState);
             dumpAnrStateLocked(activity, windowState, reason);
         }
@@ -109,19 +106,12 @@
     void notifyWindowResponsive(IBinder inputToken) {
         final int pid;
         synchronized (mService.mGlobalLock) {
-            WindowState windowState = mService.mInputToWindowMap.get(inputToken);
-            if (windowState != null) {
-                pid = windowState.mSession.mPid;
-            } else {
-                // Check if the token belongs to an embedded window.
-                EmbeddedWindow embeddedWindow = mService.mEmbeddedWindowController.get(inputToken);
-                if (embeddedWindow == null) {
-                    Slog.e(TAG_WM,
-                            "Unknown token, dropping notifyWindowConnectionResponsive request");
-                    return;
-                }
-                pid = embeddedWindow.mOwnerPid;
+            InputTarget target = mService.getInputTargetFromToken(inputToken);
+            if (target == null) {
+                Slog.e(TAG_WM, "Unknown token, dropping notifyWindowConnectionResponsive request");
+                return;
             }
+            pid = target.getPid();
         }
         mService.mAmInternal.inputDispatchingResumed(pid);
     }
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index 7f0adca..5589396 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -35,10 +35,10 @@
  */
 class AppTaskImpl extends IAppTask.Stub {
     private static final String TAG = "AppTaskImpl";
-    private ActivityTaskManagerService mService;
+    private final ActivityTaskManagerService mService;
 
-    private int mTaskId;
-    private int mCallingUid;
+    private final int mTaskId;
+    private final int mCallingUid;
 
     public AppTaskImpl(ActivityTaskManagerService service, int taskId, int callingUid) {
         mService = service;
@@ -113,9 +113,9 @@
                         return;
                     }
                 }
-                mService.mTaskSupervisor.startActivityFromRecents(callingPid,
-                        callingUid, mTaskId, null);
             }
+            mService.mTaskSupervisor.startActivityFromRecents(callingPid, callingUid, mTaskId,
+                    null /* options */);
         } finally {
             Binder.restoreCallingIdentity(origId);
         }
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index c1b287f..bd08d01 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -40,6 +40,9 @@
 import static android.view.WindowManager.TRANSIT_OLD_NONE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_BACK;
@@ -78,10 +81,7 @@
 import static com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation;
 import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenEnterAnimation;
 import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenExitAnimation;
-import static com.android.internal.policy.TransitionAnimation.THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN;
-import static com.android.internal.policy.TransitionAnimation.THUMBNAIL_TRANSITION_ENTER_SCALE_UP;
-import static com.android.internal.policy.TransitionAnimation.THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN;
-import static com.android.internal.policy.TransitionAnimation.THUMBNAIL_TRANSITION_EXIT_SCALE_UP;
+import static com.android.internal.policy.TransitionAnimation.prepareThumbnailAnimationWithDuration;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS_ANIM;
 import static com.android.server.wm.AppTransitionProto.APP_TRANSITION_STATE;
@@ -102,12 +102,7 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Picture;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.hardware.HardwareBuffer;
 import android.os.Binder;
 import android.os.Debug;
@@ -132,7 +127,6 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationSet;
 import android.view.animation.AnimationUtils;
-import android.view.animation.ClipRectAnimation;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 import android.view.animation.ScaleAnimation;
@@ -222,6 +216,7 @@
     private int mNextAppTransitionEnter;
     private int mNextAppTransitionExit;
     private int mNextAppTransitionInPlace;
+    private boolean mNextAppTransitionIsSync;
 
     // Keyed by WindowContainer hashCode.
     private final SparseArray<AppTransitionAnimationSpec> mNextAppTransitionAnimationsSpecs
@@ -357,6 +352,13 @@
         fetchAppTransitionSpecsFromFuture();
     }
 
+    void abort() {
+        if (mRemoteAnimationController != null) {
+            mRemoteAnimationController.cancelAnimation("aborted");
+        }
+        clear();
+    }
+
     boolean isRunning() {
         return mAppTransitionState == APP_STATE_RUNNING;
     }
@@ -441,6 +443,7 @@
 
         int redoLayout = notifyAppTransitionStartingLocked(
                 AppTransition.isKeyguardGoingAwayTransitOld(transit),
+                AppTransition.isKeyguardOccludeTransitOld(transit),
                 topOpeningAnim != null ? topOpeningAnim.getDurationHint() : 0,
                 topOpeningAnim != null
                         ? topOpeningAnim.getStatusBarTransitionsStartTime()
@@ -475,6 +478,8 @@
         mNextAppTransitionAnimationsSpecsFuture = null;
         mDefaultNextAppTransitionAnimationSpec = null;
         mAnimationFinishedCallback = null;
+        mOverrideTaskTransition = false;
+        mNextAppTransitionIsSync = false;
     }
 
     void freeze() {
@@ -553,12 +558,14 @@
         }
     }
 
-    private int notifyAppTransitionStartingLocked(boolean keyguardGoingAway, long duration,
-            long statusBarAnimationStartTime, long statusBarAnimationDuration) {
+    private int notifyAppTransitionStartingLocked(boolean keyguardGoingAway,
+            boolean keyguardOcclude, long duration, long statusBarAnimationStartTime,
+            long statusBarAnimationDuration) {
         int redoLayout = 0;
         for (int i = 0; i < mListeners.size(); i++) {
             redoLayout |= mListeners.get(i).onAppTransitionStartingLocked(keyguardGoingAway,
-                    duration, statusBarAnimationStartTime, statusBarAnimationDuration);
+                    keyguardOcclude, duration, statusBarAnimationStartTime,
+                    statusBarAnimationDuration);
         }
         return redoLayout;
     }
@@ -641,24 +648,6 @@
     /**
      * Prepares the specified animation with a standard duration, interpolator, etc.
      */
-    Animation prepareThumbnailAnimationWithDuration(@Nullable Animation a, int appWidth,
-            int appHeight, long duration, Interpolator interpolator) {
-        if (a != null) {
-            if (duration > 0) {
-                a.setDuration(duration);
-            }
-            a.setFillAfter(true);
-            if (interpolator != null) {
-                a.setInterpolator(interpolator);
-            }
-            a.initialize(appWidth, appHeight, appWidth, appHeight);
-        }
-        return a;
-    }
-
-    /**
-     * Prepares the specified animation with a standard duration, interpolator, etc.
-     */
     Animation prepareThumbnailAnimation(Animation a, int appWidth, int appHeight, int transit) {
         // Pick the desired duration.  If this is an inter-activity transition,
         // it  is the standard duration for that.  Otherwise we use the longer
@@ -678,56 +667,16 @@
     }
 
     /**
-     * Return the current thumbnail transition state.
-     */
-    int getThumbnailTransitionState(boolean enter) {
-        if (enter) {
-            if (mNextAppTransitionScaleUp) {
-                return THUMBNAIL_TRANSITION_ENTER_SCALE_UP;
-            } else {
-                return THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN;
-            }
-        } else {
-            if (mNextAppTransitionScaleUp) {
-                return THUMBNAIL_TRANSITION_EXIT_SCALE_UP;
-            } else {
-                return THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN;
-            }
-        }
-    }
-
-    /**
      * Creates an overlay with a background color and a thumbnail for the cross profile apps
      * animation.
      */
     HardwareBuffer createCrossProfileAppsThumbnail(
             @DrawableRes int thumbnailDrawableRes, Rect frame) {
-        final int width = frame.width();
-        final int height = frame.height();
-
-        final Picture picture = new Picture();
-        final Canvas canvas = picture.beginRecording(width, height);
-        canvas.drawColor(Color.argb(0.6f, 0, 0, 0));
-        final int thumbnailSize = mService.mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.cross_profile_apps_thumbnail_size);
-        final Drawable drawable = mService.mContext.getDrawable(thumbnailDrawableRes);
-        drawable.setBounds(
-                (width - thumbnailSize) / 2,
-                (height - thumbnailSize) / 2,
-                (width + thumbnailSize) / 2,
-                (height + thumbnailSize) / 2);
-        drawable.setTint(mContext.getColor(android.R.color.white));
-        drawable.draw(canvas);
-        picture.endRecording();
-
-        return Bitmap.createBitmap(picture).getHardwareBuffer();
+        return mTransitionAnimation.createCrossProfileAppsThumbnail(thumbnailDrawableRes, frame);
     }
 
     Animation createCrossProfileAppsThumbnailAnimationLocked(Rect appRect) {
-        final Animation animation =
-                mTransitionAnimation.loadCrossProfileAppThumbnailEnterAnimation();
-        return prepareThumbnailAnimationWithDuration(animation, appRect.width(),
-                appRect.height(), 0, null);
+        return mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(appRect);
     }
 
     /**
@@ -735,115 +684,14 @@
      * when a thumbnail is specified with the pending animation override.
      */
     Animation createThumbnailAspectScaleAnimationLocked(Rect appRect, @Nullable Rect contentInsets,
-            HardwareBuffer thumbnailHeader, WindowContainer container, int uiMode,
-            int orientation) {
-        Animation a;
-        final int thumbWidthI = thumbnailHeader.getWidth();
-        final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1;
-        final int thumbHeightI = thumbnailHeader.getHeight();
-        final int appWidth = appRect.width();
-
-        float scaleW = appWidth / thumbWidth;
-        getNextAppTransitionStartRect(container, mTmpRect);
-        final float fromX;
-        float fromY;
-        final float toX;
-        float toY;
-        final float pivotX;
-        final float pivotY;
-        if (shouldScaleDownThumbnailTransition(uiMode, orientation)) {
-            fromX = mTmpRect.left;
-            fromY = mTmpRect.top;
-
-            // For the curved translate animation to work, the pivot points needs to be at the
-            // same absolute position as the one from the real surface.
-            toX = mTmpRect.width() / 2 * (scaleW - 1f) + appRect.left;
-            toY = appRect.height() / 2 * (1 - 1 / scaleW) + appRect.top;
-            pivotX = mTmpRect.width() / 2;
-            pivotY = appRect.height() / 2 / scaleW;
-            if (mGridLayoutRecentsEnabled) {
-                // In the grid layout, the header is displayed above the thumbnail instead of
-                // overlapping it.
-                fromY -= thumbHeightI;
-                toY -= thumbHeightI * scaleW;
-            }
-        } else {
-            pivotX = 0;
-            pivotY = 0;
-            fromX = mTmpRect.left;
-            fromY = mTmpRect.top;
-            toX = appRect.left;
-            toY = appRect.top;
-        }
-        final long duration = getAspectScaleDuration();
-        final Interpolator interpolator = getAspectScaleInterpolator();
-        if (mNextAppTransitionScaleUp) {
-            // Animation up from the thumbnail to the full screen
-            Animation scale = new ScaleAnimation(1f, scaleW, 1f, scaleW, pivotX, pivotY);
-            scale.setInterpolator(interpolator);
-            scale.setDuration(duration);
-            Animation alpha = new AlphaAnimation(1f, 0f);
-            alpha.setInterpolator(mThumbnailFadeOutInterpolator);
-            alpha.setDuration(duration);
-            Animation translate = createCurvedMotion(fromX, toX, fromY, toY);
-            translate.setInterpolator(interpolator);
-            translate.setDuration(duration);
-
-            mTmpFromClipRect.set(0, 0, thumbWidthI, thumbHeightI);
-            mTmpToClipRect.set(appRect);
-
-            // Containing frame is in screen space, but we need the clip rect in the
-            // app space.
-            mTmpToClipRect.offsetTo(0, 0);
-            mTmpToClipRect.right = (int) (mTmpToClipRect.right / scaleW);
-            mTmpToClipRect.bottom = (int) (mTmpToClipRect.bottom / scaleW);
-
-            if (contentInsets != null) {
-                mTmpToClipRect.inset((int) (-contentInsets.left * scaleW),
-                        (int) (-contentInsets.top * scaleW),
-                        (int) (-contentInsets.right * scaleW),
-                        (int) (-contentInsets.bottom * scaleW));
-            }
-
-            Animation clipAnim = new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect);
-            clipAnim.setInterpolator(interpolator);
-            clipAnim.setDuration(duration);
-
-            // This AnimationSet uses the Interpolators assigned above.
-            AnimationSet set = new AnimationSet(false);
-            set.addAnimation(scale);
-            if (!mGridLayoutRecentsEnabled) {
-                // In the grid layout, the header should be shown for the whole animation.
-                set.addAnimation(alpha);
-            }
-            set.addAnimation(translate);
-            set.addAnimation(clipAnim);
-            a = set;
-        } else {
-            // Animation down from the full screen to the thumbnail
-            Animation scale = new ScaleAnimation(scaleW, 1f, scaleW, 1f, pivotX, pivotY);
-            scale.setInterpolator(interpolator);
-            scale.setDuration(duration);
-            Animation alpha = new AlphaAnimation(0f, 1f);
-            alpha.setInterpolator(mThumbnailFadeInInterpolator);
-            alpha.setDuration(duration);
-            Animation translate = createCurvedMotion(toX, fromX, toY, fromY);
-            translate.setInterpolator(interpolator);
-            translate.setDuration(duration);
-
-            // This AnimationSet uses the Interpolators assigned above.
-            AnimationSet set = new AnimationSet(false);
-            set.addAnimation(scale);
-            if (!mGridLayoutRecentsEnabled) {
-                // In the grid layout, the header should be shown for the whole animation.
-                set.addAnimation(alpha);
-            }
-            set.addAnimation(translate);
-            a = set;
-
-        }
-        return prepareThumbnailAnimationWithDuration(a, appWidth, appRect.height(), 0,
-                null);
+            HardwareBuffer thumbnailHeader, WindowContainer container, int orientation) {
+        AppTransitionAnimationSpec spec = mNextAppTransitionAnimationsSpecs.get(
+                container.hashCode());
+        return mTransitionAnimation.createThumbnailAspectScaleAnimationLocked(appRect,
+                contentInsets, thumbnailHeader, orientation, spec != null ? spec.rect : null,
+                mDefaultNextAppTransitionAnimationSpec != null
+                        ? mDefaultNextAppTransitionAnimationSpec.rect : null,
+                mNextAppTransitionScaleUp);
     }
 
     private Animation createCurvedMotion(float fromX, float toX, float fromY, float toY) {
@@ -1043,7 +891,7 @@
                             + "transit=%s Callers=%s",
                     a, appTransitionOldToString(transit), Debug.getCallers(3));
         } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CLIP_REVEAL) {
-            a = mTransitionAnimation.createClipRevealAnimationLocked(
+            a = mTransitionAnimation.createClipRevealAnimationLockedCompat(
                     transit, enter, frame, displayFrame,
                     mDefaultNextAppTransitionAnimationSpec != null
                             ? mDefaultNextAppTransitionAnimationSpec.rect : null);
@@ -1052,7 +900,7 @@
                             + "transit=%s Callers=%s",
                     a, appTransitionOldToString(transit), Debug.getCallers(3));
         } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_SCALE_UP) {
-            a = mTransitionAnimation.createScaleUpAnimationLocked(transit, enter, frame,
+            a = mTransitionAnimation.createScaleUpAnimationLockedCompat(transit, enter, frame,
                     mDefaultNextAppTransitionAnimationSpec != null
                             ? mDefaultNextAppTransitionAnimationSpec.rect : null);
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
@@ -1064,8 +912,8 @@
             mNextAppTransitionScaleUp =
                     (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP);
             final HardwareBuffer thumbnailHeader = getAppTransitionThumbnailHeader(container);
-            a = mTransitionAnimation.createThumbnailEnterExitAnimationLocked(
-                    getThumbnailTransitionState(enter), frame, transit, thumbnailHeader,
+            a = mTransitionAnimation.createThumbnailEnterExitAnimationLockedCompat(enter,
+                    mNextAppTransitionScaleUp, frame, transit, thumbnailHeader,
                     mDefaultNextAppTransitionAnimationSpec != null
                             ? mDefaultNextAppTransitionAnimationSpec.rect : null);
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
@@ -1080,9 +928,9 @@
                     (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP);
             AppTransitionAnimationSpec spec = mNextAppTransitionAnimationsSpecs.get(
                     container.hashCode());
-            a = mTransitionAnimation.createAspectScaledThumbnailEnterExitAnimationLocked(
-                    getThumbnailTransitionState(enter), orientation, transit, frame,
-                    insets, surfaceInsets, stableInsets, freeform, spec != null ? spec.rect : null,
+            a = mTransitionAnimation.createAspectScaledThumbnailEnterExitAnimationLocked(enter,
+                    mNextAppTransitionScaleUp, orientation, transit, frame, insets, surfaceInsets,
+                    stableInsets, freeform, spec != null ? spec.rect : null,
                     mDefaultNextAppTransitionAnimationSpec != null
                             ? mDefaultNextAppTransitionAnimationSpec.rect : null);
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
@@ -1098,7 +946,7 @@
                     "applyAnimation NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS: "
                             + "anim=%s transit=%s isEntrance=true Callers=%s",
                     a, appTransitionOldToString(transit), Debug.getCallers(3));
-        } else if (transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE) {
+        } else if (isChangeTransitOld(transit)) {
             // In the absence of a specific adapter, we just want to keep everything stationary.
             a = new AlphaAnimation(1.f, 1.f);
             a.setDuration(WindowChangeAnimationSpec.ANIMATION_DURATION);
@@ -1164,6 +1012,21 @@
                     animAttr = enter
                             ? WindowAnimation_launchTaskBehindSourceAnimation
                             : WindowAnimation_launchTaskBehindTargetAnimation;
+                    break;
+                // TODO(b/189386466): Use activity transition as the fallback. Investigate if we
+                //  need new TaskFragment transition.
+                case TRANSIT_OLD_TASK_FRAGMENT_OPEN:
+                    animAttr = enter
+                            ? WindowAnimation_activityOpenEnterAnimation
+                            : WindowAnimation_activityOpenExitAnimation;
+                    break;
+                // TODO(b/189386466): Use activity transition as the fallback. Investigate if we
+                //  need new TaskFragment transition.
+                case TRANSIT_OLD_TASK_FRAGMENT_CLOSE:
+                    animAttr = enter
+                            ? WindowAnimation_activityCloseEnterAnimation
+                            : WindowAnimation_activityCloseExitAnimation;
+                    break;
             }
             a = animAttr != 0 ? loadAnimationAttr(lp, animAttr, transit) : null;
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
@@ -1314,13 +1177,19 @@
     }
 
     void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter) {
+        overridePendingAppTransitionRemote(remoteAnimationAdapter, false /* sync */);
+    }
+
+    void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter,
+            boolean sync) {
         ProtoLog.i(WM_DEBUG_APP_TRANSITIONS, "Override pending remote transitionSet=%b adapter=%s",
                         isTransitionSet(), remoteAnimationAdapter);
-        if (isTransitionSet()) {
+        if (isTransitionSet() && !mNextAppTransitionIsSync) {
             clear();
             mNextAppTransitionType = NEXT_TRANSIT_TYPE_REMOTE;
             mRemoteAnimationController = new RemoteAnimationController(mService, mDisplayContent,
                     remoteAnimationAdapter, mHandler);
+            mNextAppTransitionIsSync = sync;
         }
     }
 
@@ -1468,6 +1337,15 @@
             case TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE: {
                 return "TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE";
             }
+            case TRANSIT_OLD_TASK_FRAGMENT_OPEN: {
+                return "TRANSIT_OLD_TASK_FRAGMENT_OPEN";
+            }
+            case TRANSIT_OLD_TASK_FRAGMENT_CLOSE: {
+                return "TRANSIT_OLD_TASK_FRAGMENT_CLOSE";
+            }
+            case TRANSIT_OLD_TASK_FRAGMENT_CHANGE: {
+                return "TRANSIT_OLD_TASK_FRAGMENT_CHANGE";
+            }
             default: {
                 return "<UNKNOWN: " + transition + ">";
             }
@@ -1672,7 +1550,7 @@
     }
 
     boolean prepareAppTransition(@TransitionType int transit, @TransitionFlags int flags) {
-        if (mService.mAtmService.getTransitionController().getTransitionPlayer() != null) {
+        if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
             return false;
         }
         mNextAppTransitionRequests.add(transit);
@@ -1725,7 +1603,8 @@
     }
 
     static boolean isChangeTransitOld(@TransitionOldType int transit) {
-        return transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
+        return transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE
+                || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
     }
 
     static boolean isClosingTransitOld(@TransitionOldType int transit) {
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index c869ec6..535a061 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -39,6 +39,9 @@
 import static android.view.WindowManager.TRANSIT_OLD_NONE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_BACK;
@@ -68,6 +71,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.os.Trace;
 import android.util.ArrayMap;
@@ -82,10 +86,13 @@
 import android.view.WindowManager.TransitionOldType;
 import android.view.WindowManager.TransitionType;
 import android.view.animation.Animation;
+import android.window.ITaskFragmentOrganizer;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.function.Predicate;
@@ -102,7 +109,22 @@
     private RemoteAnimationDefinition mRemoteAnimationDefinition = null;
     private static final int KEYGUARD_GOING_AWAY_ANIMATION_DURATION = 400;
 
+    private static final int TYPE_NONE = 0;
+    private static final int TYPE_ACTIVITY = 1;
+    private static final int TYPE_TASK_FRAGMENT = 2;
+    private static final int TYPE_TASK = 3;
+
+    @IntDef(prefix = { "TYPE_" }, value = {
+            TYPE_NONE,
+            TYPE_ACTIVITY,
+            TYPE_TASK_FRAGMENT,
+            TYPE_TASK
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface TransitContainerType {}
+
     private final ArrayMap<WindowContainer, Integer> mTempTransitionReasons = new ArrayMap<>();
+    private final ArrayList<WindowContainer> mTempTransitionWindows = new ArrayList<>();
 
     AppTransitionController(WindowManagerService service, DisplayContent displayContent) {
         mService = service;
@@ -144,8 +166,8 @@
     void handleAppTransitionReady() {
         mTempTransitionReasons.clear();
         if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
-                || !transitionGoodToGo(mDisplayContent.mChangingContainers,
-                        mTempTransitionReasons)) {
+                || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
+                || !transitionGoodToGoForTaskFragments()) {
             return;
         }
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady");
@@ -185,8 +207,8 @@
                 mDisplayContent.mOpeningApps);
 
         final @TransitionOldType int transit = getTransitCompatType(
-                mDisplayContent.mAppTransition,
-                mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                mDisplayContent.mAppTransition, mDisplayContent.mOpeningApps,
+                mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers,
                 mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(),
                 mDisplayContent.mSkipAppTransitionAnimation);
         mDisplayContent.mSkipAppTransitionAnimation = false;
@@ -213,7 +235,13 @@
         final ActivityRecord topChangingApp =
                 getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */);
         final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity);
-        overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes);
+
+        // Check if there is any override
+        if (!overrideWithTaskFragmentRemoteAnimation(transit, activityTypes)) {
+            // Unfreeze the windows that were previously frozen for TaskFragment animation.
+            unfreezeEmbeddedChangingWindows();
+            overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes);
+        }
 
         final boolean voiceInteraction = containsVoiceInteraction(mDisplayContent.mOpeningApps)
                 || containsVoiceInteraction(mDisplayContent.mOpeningApps);
@@ -267,6 +295,7 @@
      * @param appTransition {@link AppTransition} for managing app transition state.
      * @param openingApps {@link ActivityRecord}s which are becoming visible.
      * @param closingApps {@link ActivityRecord}s which are becoming invisible.
+     * @param changingContainers {@link WindowContainer}s which are changed in configuration.
      * @param wallpaperTarget If non-null, this is the currently visible window that is associated
      *                        with the wallpaper.
      * @param oldWallpaper The currently visible window that is associated with the wallpaper in
@@ -275,8 +304,8 @@
      */
     static @TransitionOldType int getTransitCompatType(AppTransition appTransition,
             ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps,
-            @Nullable WindowState wallpaperTarget, @Nullable WindowState oldWallpaper,
-            boolean skipAppTransitionAnimation) {
+            ArraySet<WindowContainer> changingContainers, @Nullable WindowState wallpaperTarget,
+            @Nullable WindowState oldWallpaper, boolean skipAppTransitionAnimation) {
 
         // Determine if closing and opening app token sets are wallpaper targets, in which case
         // special animations are needed.
@@ -309,8 +338,18 @@
 
         // Special transitions
         // TODO(new-app-transitions): Revisit if those can be rewritten by using flags.
-        if (appTransition.containsTransitRequest(TRANSIT_CHANGE)) {
-            return TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
+        if (appTransition.containsTransitRequest(TRANSIT_CHANGE) && !changingContainers.isEmpty()) {
+            @TransitContainerType int changingType =
+                    getTransitContainerType(changingContainers.valueAt(0));
+            switch (changingType) {
+                case TYPE_TASK:
+                    return TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
+                case TYPE_TASK_FRAGMENT:
+                    return TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+                default:
+                    throw new IllegalStateException(
+                            "TRANSIT_CHANGE with unrecognized changing type=" + changingType);
+            }
         }
         if ((flags & TRANSIT_FLAG_APP_CRASHED) != 0) {
             return TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
@@ -387,33 +426,38 @@
                 openingApps, closingApps, true /* visible */);
         final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
                 openingApps, closingApps, false /* visible */);
-        final boolean isActivityOpening = !openingWcs.isEmpty()
-                && openingWcs.valueAt(0).asActivityRecord() != null;
-        final boolean isActivityClosing = !closingWcs.isEmpty()
-                && closingWcs.valueAt(0).asActivityRecord() != null;
-        final boolean isTaskOpening = !openingWcs.isEmpty() && !isActivityOpening;
-        final boolean isTaskClosing = !closingWcs.isEmpty() && !isActivityClosing;
-
-        if (appTransition.containsTransitRequest(TRANSIT_TO_FRONT) && isTaskOpening) {
+        final WindowContainer<?> openingContainer = !openingWcs.isEmpty()
+                ? openingWcs.valueAt(0) : null;
+        final WindowContainer<?> closingContainer = !closingWcs.isEmpty()
+                ? closingWcs.valueAt(0) : null;
+        @TransitContainerType int openingType = getTransitContainerType(openingContainer);
+        @TransitContainerType int closingType = getTransitContainerType(closingContainer);
+        if (appTransition.containsTransitRequest(TRANSIT_TO_FRONT) && openingType == TYPE_TASK) {
             return TRANSIT_OLD_TASK_TO_FRONT;
         }
-        if (appTransition.containsTransitRequest(TRANSIT_TO_BACK) && isTaskClosing) {
+        if (appTransition.containsTransitRequest(TRANSIT_TO_BACK) && closingType == TYPE_TASK) {
             return TRANSIT_OLD_TASK_TO_BACK;
         }
         if (appTransition.containsTransitRequest(TRANSIT_OPEN)) {
-            if (isTaskOpening) {
+            if (openingType == TYPE_TASK) {
                 return (appTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0
                         ? TRANSIT_OLD_TASK_OPEN_BEHIND : TRANSIT_OLD_TASK_OPEN;
             }
-            if (isActivityOpening) {
+            if (openingType == TYPE_ACTIVITY) {
                 return TRANSIT_OLD_ACTIVITY_OPEN;
             }
+            if (openingType == TYPE_TASK_FRAGMENT) {
+                return TRANSIT_OLD_TASK_FRAGMENT_OPEN;
+            }
         }
         if (appTransition.containsTransitRequest(TRANSIT_CLOSE)) {
-            if (isTaskClosing) {
+            if (closingType == TYPE_TASK) {
                 return TRANSIT_OLD_TASK_CLOSE;
             }
-            if (isActivityClosing) {
+            if (closingType == TYPE_TASK_FRAGMENT) {
+                return TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+            }
+            if (closingType == TYPE_ACTIVITY) {
                 for (int i = closingApps.size() - 1; i >= 0; i--) {
                     if (closingApps.valueAt(i).visibleIgnoringKeyguard) {
                         return TRANSIT_OLD_ACTIVITY_CLOSE;
@@ -430,6 +474,24 @@
         return TRANSIT_OLD_NONE;
     }
 
+    @TransitContainerType
+    private static int getTransitContainerType(@Nullable WindowContainer<?> container) {
+        if (container == null) {
+            return TYPE_NONE;
+        }
+        if (container.asTask() != null) {
+            return TYPE_TASK;
+        }
+        if (container.asTaskFragment() != null) {
+            return TYPE_TASK_FRAGMENT;
+        }
+        if (container.asActivityRecord() != null) {
+            return TYPE_ACTIVITY;
+        }
+        return TYPE_NONE;
+    }
+
+    @Nullable
     private static WindowManager.LayoutParams getAnimLp(ActivityRecord activity) {
         final WindowState mainWindow = activity != null ? activity.findMainWindow() : null;
         return mainWindow != null ? mainWindow.mAttrs : null;
@@ -452,6 +514,151 @@
                 : null;
     }
 
+    private void unfreezeEmbeddedChangingWindows() {
+        final ArraySet<WindowContainer> changingContainers = mDisplayContent.mChangingContainers;
+        for (int i = changingContainers.size() - 1; i >= 0; i--) {
+            final WindowContainer wc = changingContainers.valueAt(i);
+            if (wc.isEmbedded()) {
+                wc.mSurfaceFreezer.unfreeze(wc.getSyncTransaction());
+            }
+        }
+    }
+
+    private boolean transitionMayContainNonAppWindows(@TransitionOldType int transit) {
+        // We don't want to have the client to animate any non-app windows.
+        // Having {@code transit} of those types doesn't mean it will contain non-app windows, but
+        // non-app windows will only be included with those transition types. And we don't currently
+        // have any use case of those for TaskFragment transition.
+        // @see NonAppWindowAnimationAdapter#startNonAppWindowAnimations
+        if (transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY
+                || transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER
+                || transit == TRANSIT_OLD_TASK_OPEN || transit == TRANSIT_OLD_TASK_TO_FRONT
+                || transit == TRANSIT_OLD_WALLPAPER_CLOSE) {
+            return true;
+        }
+
+        // Check if the wallpaper is going to participate in the transition. We don't want to have
+        // the client to animate the wallpaper windows.
+        // @see WallpaperAnimationAdapter#startWallpaperAnimations
+        return mDisplayContent.mWallpaperController.isWallpaperVisible();
+    }
+
+    /**
+     * Finds the common {@link android.window.TaskFragmentOrganizer} that organizes all app windows
+     * in the current transition.
+     * @return {@code null} if there is no such organizer, or if there are more than one.
+     */
+    @Nullable
+    private ITaskFragmentOrganizer findTaskFragmentOrganizerForAllWindows() {
+        mTempTransitionWindows.clear();
+        mTempTransitionWindows.addAll(mDisplayContent.mClosingApps);
+        mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps);
+        mTempTransitionWindows.addAll(mDisplayContent.mChangingContainers);
+
+        // It should only animated by the organizer if all windows are below the same leaf Task.
+        Task leafTask = null;
+        for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) {
+            final ActivityRecord r = getAppFromContainer(mTempTransitionWindows.get(i));
+            if (r == null) {
+                leafTask = null;
+                break;
+            }
+            // The activity may be a child of embedded Task, but we want to find the owner Task.
+            // As a result, find the organized TaskFragment first.
+            final TaskFragment organizedTaskFragment = r.getOrganizedTaskFragment();
+            // There are also cases where the Task contains non-embedded activity, such as launching
+            // split TaskFragments from a non-embedded activity.
+            // The hierarchy may looks like this:
+            // - Task
+            //    - Activity
+            //    - TaskFragment
+            //       - Activity
+            //    - TaskFragment
+            //       - Activity
+            // We also want to have the organizer handle the transition for such case.
+            final Task task = organizedTaskFragment != null
+                    ? organizedTaskFragment.getTask()
+                    : r.getTask();
+            if (task == null) {
+                leafTask = null;
+                break;
+            }
+            // We don't want the organizer to handle transition of other non-embedded Task.
+            if (leafTask != null && leafTask != task) {
+                leafTask = null;
+                break;
+            }
+            final ActivityRecord rootActivity = task.getRootActivity();
+            // We don't want the organizer to handle transition when the whole app is closing.
+            if (rootActivity == null) {
+                leafTask = null;
+                break;
+            }
+            // We don't want the organizer to handle transition of non-embedded activity of other
+            // app.
+            if (r.getUid() != rootActivity.getUid() && !r.isEmbedded()) {
+                leafTask = null;
+                break;
+            }
+            leafTask = task;
+        }
+        mTempTransitionWindows.clear();
+        if (leafTask == null) {
+            return null;
+        }
+
+        // We don't support remote animation for Task with multiple TaskFragmentOrganizers.
+        final ITaskFragmentOrganizer[] organizer = new ITaskFragmentOrganizer[1];
+        final boolean hasMultipleOrganizers = leafTask.forAllLeafTaskFragments(taskFragment -> {
+            final ITaskFragmentOrganizer tfOrganizer = taskFragment.getTaskFragmentOrganizer();
+            if (tfOrganizer == null) {
+                return false;
+            }
+            if (organizer[0] != null && !organizer[0].asBinder().equals(tfOrganizer.asBinder())) {
+                return true;
+            }
+            organizer[0] = tfOrganizer;
+            return false;
+        });
+        if (hasMultipleOrganizers) {
+            ProtoLog.e(WM_DEBUG_APP_TRANSITIONS, "We don't support remote animation for"
+                    + " Task with multiple TaskFragmentOrganizers.");
+            return null;
+        }
+        return organizer[0];
+    }
+
+    /**
+     * Overrides the pending transition with the remote animation defined by the
+     * {@link ITaskFragmentOrganizer} if all windows in the transition are children of
+     * {@link TaskFragment} that are organized by the same organizer.
+     *
+     * @return {@code true} if the transition is overridden.
+     */
+    private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit,
+            ArraySet<Integer> activityTypes) {
+        if (transitionMayContainNonAppWindows(transit)) {
+            return false;
+        }
+
+        final ITaskFragmentOrganizer organizer = findTaskFragmentOrganizerForAllWindows();
+        final RemoteAnimationDefinition definition = organizer != null
+                ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController
+                    .getRemoteAnimationDefinition(organizer)
+                : null;
+        final RemoteAnimationAdapter adapter = definition != null
+                ? definition.getAdapter(transit, activityTypes)
+                : null;
+        if (adapter == null) {
+            return false;
+        }
+        mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter);
+        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
+                "Override with TaskFragment remote animation for transit=%s",
+                AppTransition.appTransitionOldToString(transit));
+        return true;
+    }
+
     /**
      * Overrides the pending transition with the remote animation defined for the transition in the
      * set of defined remote animations in the app window token.
@@ -464,19 +671,28 @@
         }
         final RemoteAnimationAdapter adapter =
                 getRemoteAnimationOverride(animLpActivity, transit, activityTypes);
-        if (adapter != null) {
+        if (adapter != null
+                && mDisplayContent.mAppTransition.getRemoteAnimationController() == null) {
             mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter);
         }
     }
 
+    @Nullable
+    static Task findRootTaskFromContainer(WindowContainer wc) {
+        return wc.asTaskFragment() != null ? wc.asTaskFragment().getRootTask()
+                : wc.asActivityRecord().getRootTask();
+    }
+
+    @Nullable
     static ActivityRecord getAppFromContainer(WindowContainer wc) {
-        return wc.asTask() != null ? wc.asTask().getTopNonFinishingActivity()
+        return wc.asTaskFragment() != null ? wc.asTaskFragment().getTopNonFinishingActivity()
                 : wc.asActivityRecord();
     }
 
     /**
      * @return The window token that determines the animation theme.
      */
+    @Nullable
     private ActivityRecord findAnimLayoutParamsToken(@TransitionOldType int transit,
             ArraySet<Integer> activityTypes) {
         ActivityRecord result;
@@ -489,7 +705,7 @@
                 w -> w.getRemoteAnimationDefinition() != null
                         && w.getRemoteAnimationDefinition().hasTransition(transit, activityTypes));
         if (result != null) {
-            return getAppFromContainer(result);
+            return result;
         }
         result = lookForHighestTokenWithFilter(closingApps, openingApps, changingApps,
                 w -> w.fillsParent() && w.findMainWindow() != null);
@@ -632,6 +848,7 @@
             boolean canPromote = true;
 
             if (parent == null || !parent.canCreateRemoteAnimationTarget()
+                    || !parent.canBeAnimationTarget()
                     // We cannot promote the animation on Task's parent when the task is in
                     // clearing task in case the animating get stuck when performing the opening
                     // task that behind it.
@@ -727,7 +944,7 @@
 
         final AccessibilityController accessibilityController =
                 mDisplayContent.mWmService.mAccessibilityController;
-        if (accessibilityController != null) {
+        if (accessibilityController.hasCallbacks()) {
             accessibilityController.onAppWindowTransition(mDisplayContent.getDisplayId(), transit);
         }
     }
@@ -840,72 +1057,114 @@
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                 "Checking %d opening apps (frozen=%b timeout=%b)...", apps.size(),
                 mService.mDisplayFrozen, mDisplayContent.mAppTransition.isTimeout());
-
+        if (mDisplayContent.mAppTransition.isTimeout()) {
+            return true;
+        }
         final ScreenRotationAnimation screenRotationAnimation = mService.mRoot.getDisplayContent(
                 Display.DEFAULT_DISPLAY).getRotationAnimation();
 
-        if (!mDisplayContent.mAppTransition.isTimeout()) {
-            // Imagine the case where we are changing orientation due to an app transition, but a
-            // previous orientation change is still in progress. We won't process the orientation
-            // change for our transition because we need to wait for the rotation animation to
-            // finish.
-            // If we start the app transition at this point, we will interrupt it halfway with a
-            // new rotation animation after the old one finally finishes. It's better to defer the
-            // app transition.
-            if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()
-                    && mDisplayContent.getDisplayRotation().needsUpdate()) {
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                        "Delaying app transition for screen rotation animation to finish");
-                return false;
-            }
-            for (int i = 0; i < apps.size(); i++) {
-                WindowContainer wc = apps.valueAt(i);
-                final ActivityRecord activity = getAppFromContainer(wc);
-                if (activity == null) {
-                    continue;
-                }
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                        "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
-                                + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
-                        activity, activity.allDrawn, activity.startingDisplayed,
-                        activity.startingMoved, activity.isRelaunching(),
-                        activity.mStartingWindow);
-
-
-                final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
-                if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
-                    return false;
-                }
-                if (allDrawn) {
-                    outReasons.put(activity, APP_TRANSITION_WINDOWS_DRAWN);
-                } else {
-                    outReasons.put(activity,
-                            activity.mStartingData instanceof SplashScreenStartingData
-                                    ? APP_TRANSITION_SPLASH_SCREEN
-                                    : APP_TRANSITION_SNAPSHOT);
-                }
-            }
-
-            // We also need to wait for the specs to be fetched, if needed.
-            if (mDisplayContent.mAppTransition.isFetchingAppTransitionsSpecs()) {
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "isFetchingAppTransitionSpecs=true");
-                return false;
-            }
-
-            if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "unknownApps is not empty: %s",
-                            mDisplayContent.mUnknownAppVisibilityController.getDebugMessage());
-                return false;
-            }
-
-            // If the wallpaper is visible, we need to check it's ready too.
-            boolean wallpaperReady = !mWallpaperControllerLocked.isWallpaperVisible() ||
-                    mWallpaperControllerLocked.wallpaperTransitionReady();
-            if (wallpaperReady) {
-                return true;
-            }
+        // Imagine the case where we are changing orientation due to an app transition, but a
+        // previous orientation change is still in progress. We won't process the orientation
+        // change for our transition because we need to wait for the rotation animation to
+        // finish.
+        // If we start the app transition at this point, we will interrupt it halfway with a
+        // new rotation animation after the old one finally finishes. It's better to defer the
+        // app transition.
+        if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()
+                && mDisplayContent.getDisplayRotation().needsUpdate()) {
+            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
+                    "Delaying app transition for screen rotation animation to finish");
             return false;
         }
+        for (int i = 0; i < apps.size(); i++) {
+            WindowContainer wc = apps.valueAt(i);
+            final ActivityRecord activity = getAppFromContainer(wc);
+            if (activity == null) {
+                continue;
+            }
+            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
+                    "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
+                            + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
+                    activity, activity.allDrawn, activity.startingDisplayed,
+                    activity.startingMoved, activity.isRelaunching(),
+                    activity.mStartingWindow);
+
+            final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
+            if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
+                return false;
+            }
+            if (allDrawn) {
+                outReasons.put(activity, APP_TRANSITION_WINDOWS_DRAWN);
+            } else {
+                outReasons.put(activity,
+                        activity.mStartingData instanceof SplashScreenStartingData
+                                ? APP_TRANSITION_SPLASH_SCREEN
+                                : APP_TRANSITION_SNAPSHOT);
+            }
+        }
+
+        // We also need to wait for the specs to be fetched, if needed.
+        if (mDisplayContent.mAppTransition.isFetchingAppTransitionsSpecs()) {
+            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "isFetchingAppTransitionSpecs=true");
+            return false;
+        }
+
+        if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
+            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "unknownApps is not empty: %s",
+                    mDisplayContent.mUnknownAppVisibilityController.getDebugMessage());
+            return false;
+        }
+
+        // If the wallpaper is visible, we need to check it's ready too.
+        return !mWallpaperControllerLocked.isWallpaperVisible()
+                || mWallpaperControllerLocked.wallpaperTransitionReady();
+    }
+
+    private boolean transitionGoodToGoForTaskFragments() {
+        if (mDisplayContent.mAppTransition.isTimeout()) {
+            return true;
+        }
+
+        // Check all Tasks in this transition. This is needed because new TaskFragment created for
+        // launching activity may not be in the tracking lists, but we still want to wait for the
+        // activity launch to start the transition.
+        final ArraySet<Task> rootTasks = new ArraySet<>();
+        for (int i = mDisplayContent.mOpeningApps.size() - 1; i >= 0; i--) {
+            rootTasks.add(mDisplayContent.mOpeningApps.valueAt(i).getRootTask());
+        }
+        for (int i = mDisplayContent.mClosingApps.size() - 1; i >= 0; i--) {
+            rootTasks.add(mDisplayContent.mClosingApps.valueAt(i).getRootTask());
+        }
+        for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) {
+            rootTasks.add(
+                    findRootTaskFromContainer(mDisplayContent.mChangingContainers.valueAt(i)));
+        }
+
+        // Organized TaskFragment can be empty for two situations:
+        // 1. New created and is waiting for Activity launch. In this case, we want to wait for
+        //    the Activity launch to trigger the transition.
+        // 2. Last Activity is just removed. In this case, we want to wait for organizer to
+        //    remove the TaskFragment because it may also want to change other TaskFragments in
+        //    the same transition.
+        for (int i = rootTasks.size() - 1; i >= 0; i--) {
+            final Task rootTask = rootTasks.valueAt(i);
+            if (rootTask == null) {
+                // It is possible that one activity may have been removed from the hierarchy. No
+                // need to check for this case.
+                continue;
+            }
+            final boolean notReady = rootTask.forAllLeafTaskFragments(taskFragment -> {
+                if (!taskFragment.isReadyToTransit()) {
+                    ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Organized TaskFragment is not ready= %s",
+                            taskFragment);
+                    return true;
+                }
+                return false;
+            });
+            if (notReady) {
+                return false;
+            }
+        }
         return true;
     }
 
diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
index faeb4ba..4355b38 100644
--- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java
+++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
@@ -166,6 +166,10 @@
         setReady(id, true);
     }
 
+    boolean isReady(int id) {
+        return mActiveSyncs.get(id).mReady;
+    }
+
     /**
      * Aborts the sync (ie. it doesn't wait for ready or anything to finish)
      */
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index d52e9b6..5a2cf17 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -28,16 +28,20 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.activityTypeToString;
 import static android.app.WindowConfiguration.windowingModeToString;
+import static android.app.WindowConfigurationProto.WINDOWING_MODE;
+import static android.content.ConfigurationProto.WINDOW_CONFIGURATION;
 
 import static com.android.server.wm.ConfigurationContainerProto.FULL_CONFIGURATION;
 import static com.android.server.wm.ConfigurationContainerProto.MERGED_OVERRIDE_CONFIGURATION;
 import static com.android.server.wm.ConfigurationContainerProto.OVERRIDE_CONFIGURATION;
 
 import android.annotation.CallSuper;
+import android.annotation.NonNull;
 import android.app.WindowConfiguration;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.LocaleList;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -111,6 +115,7 @@
      * This method should be used for getting settings applied in each particular level of the
      * hierarchy.
      */
+    @NonNull
     public Configuration getConfiguration() {
         return mFullConfiguration;
     }
@@ -170,11 +175,13 @@
     }
 
     /** Returns requested override configuration applied to this configuration container. */
+    @NonNull
     public Configuration getRequestedOverrideConfiguration() {
         return mRequestedOverrideConfiguration;
     }
 
     /** Returns the resolved override configuration. */
+    @NonNull
     Configuration getResolvedOverrideConfiguration() {
         return mResolvedOverrideConfiguration;
     }
@@ -203,6 +210,7 @@
      * Get merged override configuration from the top of the hierarchy down to this particular
      * instance. This should be reported to client as override config.
      */
+    @NonNull
     public Configuration getMergedOverrideConfiguration() {
         return mMergedOverrideConfiguration;
     }
@@ -507,7 +515,7 @@
         return mFullConfiguration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM;
     }
 
-    /** Returns the activity type associated with the the configuration container. */
+    /** Returns the activity type associated with the configuration container. */
     /*@WindowConfiguration.ActivityType*/
     public int getActivityType() {
         return mFullConfiguration.windowConfiguration.getActivityType();
@@ -541,20 +549,48 @@
     }
 
     /**
+     * Applies app-specific nightMode and {@link LocaleList} on requested configuration.
+     * @return true if any of the requested configuration has been updated.
+     */
+    public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales) {
+        mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration());
+        boolean newNightModeSet = (nightMode != null) && setOverrideNightMode(mRequestsTmpConfig,
+                nightMode);
+        boolean newLocalesSet = (locales != null) && setOverrideLocales(mRequestsTmpConfig,
+                locales);
+        if (newNightModeSet || newLocalesSet) {
+            onRequestedOverrideConfigurationChanged(mRequestsTmpConfig);
+        }
+        return newNightModeSet || newLocalesSet;
+    }
+
+    /**
      * Overrides the night mode applied to this ConfigurationContainer.
      * @return true if the nightMode has been changed.
      */
-    public boolean setOverrideNightMode(int nightMode) {
+    private boolean setOverrideNightMode(Configuration requestsTmpConfig, int nightMode) {
         final int currentUiMode = mRequestedOverrideConfiguration.uiMode;
         final int currentNightMode = currentUiMode & Configuration.UI_MODE_NIGHT_MASK;
         final int validNightMode = nightMode & Configuration.UI_MODE_NIGHT_MASK;
         if (currentNightMode == validNightMode) {
             return false;
         }
-        mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration());
-        mRequestsTmpConfig.uiMode = validNightMode
+        requestsTmpConfig.uiMode = validNightMode
                 | (currentUiMode & ~Configuration.UI_MODE_NIGHT_MASK);
-        onRequestedOverrideConfigurationChanged(mRequestsTmpConfig);
+        return true;
+    }
+
+    /**
+     * Overrides the locales applied to this ConfigurationContainer.
+     * @return true if the LocaleList has been changed.
+     */
+    private boolean setOverrideLocales(Configuration requestsTmpConfig,
+            @NonNull LocaleList overrideLocales) {
+        if (mRequestedOverrideConfiguration.getLocales().equals(overrideLocales)) {
+            return false;
+        }
+        requestsTmpConfig.setLocales(overrideLocales);
+        requestsTmpConfig.userSetLocale = true;
         return true;
     }
 
@@ -661,22 +697,40 @@
     @CallSuper
     protected void dumpDebug(ProtoOutputStream proto, long fieldId,
             @WindowTraceLogLevel int logLevel) {
-        // Critical log level logs only visible elements to mitigate performance overheard
-        if (logLevel != WindowTraceLogLevel.ALL && !mHasOverrideConfiguration) {
-            return;
+        final long token = proto.start(fieldId);
+
+        if (logLevel == WindowTraceLogLevel.ALL || mHasOverrideConfiguration) {
+            mRequestedOverrideConfiguration.dumpDebug(proto, OVERRIDE_CONFIGURATION,
+                    logLevel == WindowTraceLogLevel.CRITICAL);
         }
 
-        final long token = proto.start(fieldId);
-        mRequestedOverrideConfiguration.dumpDebug(proto, OVERRIDE_CONFIGURATION,
-                logLevel == WindowTraceLogLevel.CRITICAL);
+        // Unless trace level is set to `WindowTraceLogLevel.ALL` don't dump anything that isn't
+        // required to mitigate performance overhead
         if (logLevel == WindowTraceLogLevel.ALL) {
             mFullConfiguration.dumpDebug(proto, FULL_CONFIGURATION, false /* critical */);
             mMergedOverrideConfiguration.dumpDebug(proto, MERGED_OVERRIDE_CONFIGURATION,
                     false /* critical */);
         }
+
+        if (logLevel == WindowTraceLogLevel.TRIM) {
+            // Required for Fass to automatically detect pip transitions in Winscope traces
+            dumpDebugWindowingMode(proto);
+        }
+
         proto.end(token);
     }
 
+    private void dumpDebugWindowingMode(ProtoOutputStream proto) {
+        final long fullConfigToken = proto.start(FULL_CONFIGURATION);
+        final long windowConfigToken = proto.start(WINDOW_CONFIGURATION);
+
+        int windowingMode = mFullConfiguration.windowConfiguration.getWindowingMode();
+        proto.write(WINDOWING_MODE, windowingMode);
+
+        proto.end(windowConfigToken);
+        proto.end(fullConfigToken);
+    }
+
     /**
      * Dumps the names of this container children in the input print writer indenting each
      * level with the input prefix.
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index baa27e3..99f6fd4 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -495,8 +495,10 @@
 
 
     DisplayAreaInfo getDisplayAreaInfo() {
-        DisplayAreaInfo info = new DisplayAreaInfo(mRemoteToken.toWindowContainerToken(),
+        final DisplayAreaInfo info = new DisplayAreaInfo(mRemoteToken.toWindowContainerToken(),
                 getDisplayContent().getDisplayId(), mFeatureId);
+        final RootDisplayArea root = getRootDisplayArea();
+        info.rootDisplayAreaId = root == null ? getDisplayContent().mFeatureId : root.mFeatureId;
         info.configuration.setTo(getConfiguration());
         return info;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicyBuilder.java b/services/core/java/com/android/server/wm/DisplayAreaPolicyBuilder.java
index 47d7c9d..3d7ac6c 100644
--- a/services/core/java/com/android/server/wm/DisplayAreaPolicyBuilder.java
+++ b/services/core/java/com/android/server/wm/DisplayAreaPolicyBuilder.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManagerPolicyConstants.APPLICATION_LAYER;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
 import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_LAST;
+import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
 
 import android.annotation.Nullable;
 import android.os.Bundle;
@@ -135,12 +136,6 @@
  */
 class DisplayAreaPolicyBuilder {
 
-    /**
-     * Key to specify the {@link RootDisplayArea} to attach the window to. Should be used by the
-     * function passed in from {@link #setSelectRootForWindowFunc(BiFunction)}
-     */
-    static final String KEY_ROOT_DISPLAY_AREA_ID = "root_display_area_id";
-
     @Nullable private HierarchyBuilder mRootHierarchyBuilder;
     private final ArrayList<HierarchyBuilder> mDisplayAreaGroupHierarchyBuilders =
             new ArrayList<>();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 9335846..0ecee66 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -61,7 +61,6 @@
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
-import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
@@ -77,6 +76,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.DisplayAreaOrganizer.FEATURE_ROOT;
@@ -88,13 +88,17 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IME;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LAYER_MIRRORING;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SCREEN_ON;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
+import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.DisplayContentProto.APP_TRANSITION;
 import static com.android.server.wm.DisplayContentProto.CLOSING_APPS;
 import static com.android.server.wm.DisplayContentProto.CURRENT_FOCUS;
@@ -117,7 +121,6 @@
 import static com.android.server.wm.DisplayContentProto.SCREEN_ROTATION_ANIMATION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowContainerChildProto.DISPLAY_CONTENT;
@@ -126,7 +129,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_STACK_CRAWLS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -134,7 +136,6 @@
 import static com.android.server.wm.WindowManagerService.H.WINDOW_HIDE_TIMEOUT;
 import static com.android.server.wm.WindowManagerService.LAYOUT_REPEAT_THRESHOLD;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_PLACING_SURFACES;
-import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_REMOVING_FOCUS;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_ASSIGN_LAYERS;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
 import static com.android.server.wm.WindowManagerService.WINDOWS_FREEZING_SCREENS_TIMEOUT;
@@ -202,6 +203,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
 import android.view.MagnificationSpec;
 import android.view.PrivacyIndicatorBounds;
 import android.view.RemoteAnimationDefinition;
@@ -296,7 +298,29 @@
      * 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.
      */
-    private final SurfaceControl mWindowingLayer;
+    private SurfaceControl mWindowingLayer;
+
+    /**
+     * The window token of the layer of the hierarchy to mirror, or null if this DisplayContent
+     * is not being used for layer mirroring.
+     */
+    @VisibleForTesting IBinder mTokenToMirror = null;
+
+    /**
+     * The surface for mirroring the contents of this hierarchy, or null if layer mirroring is
+     * temporarily disabled.
+     */
+    private SurfaceControl mMirroredSurface = null;
+
+    /**
+     * The last bounds of the DisplayArea to mirror.
+     */
+    private Rect mLastMirroredDisplayAreaBounds = null;
+
+    /**
+     * The last state of the display.
+     */
+    private int mLastDisplayState;
 
     // Contains all IME window containers. Note that the z-ordering of the IME windows will depend
     // on the IME target. We mainly have this container grouping so we can keep track of all the IME
@@ -306,7 +330,7 @@
     private final ImeContainer mImeWindowsContainer = new ImeContainer(mWmService);
 
     @VisibleForTesting
-    final DisplayAreaPolicy mDisplayAreaPolicy;
+    DisplayAreaPolicy mDisplayAreaPolicy;
 
     private WindowState mTmpWindow;
     private boolean mUpdateImeTarget;
@@ -362,6 +386,13 @@
     boolean mIsSizeForced = false;
 
     /**
+     * Overridden display size and metrics to activity window bounds. Set via
+     * "adb shell wm set-sandbox-display-apis". Default to true, since only disable for debugging.
+     * @see WindowManagerService#setSandboxDisplayApis(int, boolean)
+     */
+    private boolean mSandboxDisplayApis = true;
+
+    /**
      * Overridden display density for current user. Initialized with {@link #mInitialDisplayDensity}
      * but can be set from Settings or via shell command "adb shell wm density".
      * @see WindowManagerService#setForcedDisplayDensityForUser(int, int, int)
@@ -426,6 +457,7 @@
     // Accessed directly by all users.
     private boolean mLayoutNeeded;
     int pendingLayoutChanges;
+    boolean mLayoutAndAssignWindowLayersScheduled;
 
     /**
      * Used to gate application window layout until we have sent the complete configuration.
@@ -498,11 +530,6 @@
     WindowState mCurrentFocus = null;
 
     /**
-     * The last focused window that we've notified the client that the focus is changed.
-     */
-    WindowState mLastFocus = null;
-
-    /**
      * The foreground app of this display. Windows below this app cannot be the focused window. If
      * the user taps on the area outside of the task of the focused app, we will notify AM about the
      * new task the user wants to interact with.
@@ -553,6 +580,7 @@
      * Specifies the count to determine whether to defer updating the IME target until ready.
      */
     private int mDeferUpdateImeTargetCount;
+    private boolean mUpdateImeRequestedWhileDeferred;
 
     private MagnificationSpec mMagnificationSpec;
 
@@ -561,7 +589,7 @@
     /** Caches the value whether told display manager that we have content. */
     private boolean mLastHasContent;
 
-    private DisplayRotationUtil mRotationUtil = new DisplayRotationUtil();
+    private static DisplayRotationUtil sRotationUtil = new DisplayRotationUtil();
 
     /**
      * The input method window for this display.
@@ -690,6 +718,8 @@
     // well and thus won't change the top resumed / focused record
     boolean mDontMoveToTop;
 
+    private final ArrayList<ActivityRecord> mTmpActivityList = new ArrayList<>();
+
     private final Consumer<WindowState> mUpdateWindowsForAnimator = w -> {
         WindowStateAnimator winAnimator = w.mWinAnimator;
         final ActivityRecord activity = w.mActivityRecord;
@@ -771,6 +801,21 @@
                 mTmpWindow = null;
                 return true;
             }
+
+            // If the candidate activity is currently being embedded in the focused task, the
+            // activity cannot be focused unless it is on the same TaskFragment as the focusedApp's.
+            TaskFragment parent = activity.getTaskFragment();
+            if (parent != null && parent.isEmbedded()) {
+                Task hostTask = focusedApp.getTask();
+                if (hostTask.isEmbedded()) {
+                    // Use the hosting task if the current task is embedded.
+                    hostTask = hostTask.getParent().asTaskFragment().getTask();
+                }
+                if (activity.isDescendantOf(hostTask)
+                        && activity.getTaskFragment() != focusedApp.getTaskFragment()) {
+                    return false;
+                }
+            }
         }
 
         ProtoLog.v(WM_DEBUG_FOCUS_LIGHT, "findFocusedWindow: Found new focus @ %s", w);
@@ -959,8 +1004,8 @@
             final boolean committed = winAnimator.commitFinishDrawingLocked();
             if (isDefaultDisplay && committed) {
                 if (w.hasWallpaper()) {
-                    if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG,
-                            "First draw done in potential wallpaper target " + w);
+                    ProtoLog.v(WM_DEBUG_WALLPAPER,
+                            "First draw done in potential wallpaper target %s", w);
                     mWallpaperMayChange = true;
                     pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
                     if (DEBUG_LAYOUT_REPEATS) {
@@ -1020,6 +1065,8 @@
 
         mAppTransition = new AppTransition(mWmService.mContext, mWmService, this);
         mAppTransition.registerListenerLocked(mWmService.mActivityManagerAppTransitionNotifier);
+        mTransitionController.registerLegacyListener(
+                mWmService.mActivityManagerAppTransitionNotifier);
         mAppTransition.registerListenerLocked(mFixedRotationTransitionListener);
         mAppTransitionController = new AppTransitionController(mWmService, this);
         mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this);
@@ -1058,41 +1105,9 @@
         mDividerControllerLocked = new DockedTaskDividerController(this);
         mPinnedTaskController = new PinnedTaskController(mWmService, this);
 
-        final SurfaceControl.Builder b = mWmService.makeSurfaceBuilder(mSession)
-                .setOpaque(true)
-                .setContainerLayer()
-                .setCallsite("DisplayContent");
-        mSurfaceControl = b.setName("Root").setContainerLayer().build();
-
-        // Setup the policy and build the display area hierarchy.
-        mDisplayAreaPolicy = mWmService.getDisplayAreaPolicyProvider().instantiate(
-                mWmService, this /* content */, this /* root */, mImeWindowsContainer);
-
-        final List<DisplayArea<? extends WindowContainer>> areas =
-                mDisplayAreaPolicy.getDisplayAreas(FEATURE_WINDOWED_MAGNIFICATION);
-        final DisplayArea<?> area = areas.size() == 1 ? areas.get(0) : null;
-        if (area != null && area.getParent() == this) {
-            // The windowed magnification area should contain all non-overlay windows, so just use
-            // it as the windowing layer.
-            mWindowingLayer = area.mSurfaceControl;
-        } else {
-            // Need an additional layer for screen level animation, so move the layer containing
-            // the windows to the new root.
-            mWindowingLayer = mSurfaceControl;
-            mSurfaceControl = b.setName("RootWrapper").build();
-            getPendingTransaction().reparent(mWindowingLayer, mSurfaceControl)
-                    .show(mWindowingLayer);
-        }
-
-        mOverlayLayer = b.setName("Display Overlays").setParent(mSurfaceControl).build();
-
-        getPendingTransaction()
-                .setLayer(mSurfaceControl, 0)
-                .setLayerStack(mSurfaceControl, mDisplayId)
-                .show(mSurfaceControl)
-                .setLayer(mOverlayLayer, Integer.MAX_VALUE)
-                .show(mOverlayLayer);
-        getPendingTransaction().apply();
+        final Transaction pendingTransaction = getPendingTransaction();
+        configureSurfaces(pendingTransaction);
+        pendingTransaction.apply();
 
         // Sets the display content for the children.
         onDisplayChanged(this);
@@ -1106,6 +1121,77 @@
         mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this);
     }
 
+    @Override
+    void migrateToNewSurfaceControl(Transaction t) {
+        t.remove(mSurfaceControl);
+
+        mLastSurfacePosition.set(0, 0);
+
+        configureSurfaces(t);
+
+        for (int i = 0; i < mChildren.size(); i++)  {
+            SurfaceControl sc = mChildren.get(i).getSurfaceControl();
+            if (sc != null) {
+                t.reparent(sc, mSurfaceControl);
+            }
+        }
+
+        scheduleAnimation();
+    }
+
+    /**
+     * Configures the surfaces hierarchy for DisplayContent
+     * This method always recreates the main surface control but reparents the children
+     * if they are already created.
+     * @param transaction as part of which to perform the configuration
+     */
+    private void configureSurfaces(Transaction transaction) {
+        final SurfaceControl.Builder b = mWmService.makeSurfaceBuilder(mSession)
+                .setOpaque(true)
+                .setContainerLayer()
+                .setCallsite("DisplayContent");
+        mSurfaceControl = b.setName(getName()).setContainerLayer().build();
+
+        if (mDisplayAreaPolicy == null) {
+            // Setup the policy and build the display area hierarchy.
+            // Build the hierarchy only after creating the surface so it is reparented correctly
+            mDisplayAreaPolicy = mWmService.getDisplayAreaPolicyProvider().instantiate(
+                    mWmService, this /* content */, this /* root */,
+                    mImeWindowsContainer);
+        }
+
+        final List<DisplayArea<? extends WindowContainer>> areas =
+                mDisplayAreaPolicy.getDisplayAreas(FEATURE_WINDOWED_MAGNIFICATION);
+        final DisplayArea<?> area = areas.size() == 1 ? areas.get(0) : null;
+
+        if (area != null && area.getParent() == this) {
+            // The windowed magnification area should contain all non-overlay windows, so just use
+            // it as the windowing layer.
+            mWindowingLayer = area.mSurfaceControl;
+            transaction.reparent(mWindowingLayer, mSurfaceControl);
+        } else {
+            // Need an additional layer for screen level animation, so move the layer containing
+            // the windows to the new root.
+            mWindowingLayer = mSurfaceControl;
+            mSurfaceControl = b.setName("RootWrapper").build();
+            transaction.reparent(mWindowingLayer, mSurfaceControl)
+                    .show(mWindowingLayer);
+        }
+
+        if (mOverlayLayer == null) {
+            mOverlayLayer = b.setName("Display Overlays").setParent(mSurfaceControl).build();
+        } else {
+            transaction.reparent(mOverlayLayer, mSurfaceControl);
+        }
+
+        transaction
+                .setLayer(mSurfaceControl, 0)
+                .setLayerStack(mSurfaceControl, mDisplayId)
+                .show(mSurfaceControl)
+                .setLayer(mOverlayLayer, Integer.MAX_VALUE)
+                .show(mOverlayLayer);
+    }
+
     boolean isReady() {
         // The display is ready when the system and the individual display are both ready.
         return mWmService.mDisplayReady && mDisplayReady;
@@ -1234,17 +1320,11 @@
                 // removing from parent.
                 token.getParent().removeChild(token);
             }
-            if (token.hasChild(prevDc.mLastFocus)) {
-                // If the reparent window token contains previous display's last focus window, means
-                // it will end up to gain window focus on the target display, so it should not be
-                // notified that it lost focus from the previous display.
-                prevDc.mLastFocus = null;
-            }
         }
 
         addWindowToken(token.token, token);
 
-        if (mWmService.mAccessibilityController != null) {
+        if (mWmService.mAccessibilityController.hasCallbacks()) {
             final int prevDisplayId = prevDc != null ? prevDc.getDisplayId() : INVALID_DISPLAY;
             mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(prevDisplayId,
                     getDisplayId());
@@ -1350,11 +1430,16 @@
         final Configuration currentDisplayConfig = getConfiguration();
         mTmpConfiguration.setTo(currentDisplayConfig);
         computeScreenConfiguration(mTmpConfiguration);
-        configChanged |= currentDisplayConfig.diff(mTmpConfiguration) != 0;
+        final int changes = currentDisplayConfig.diff(mTmpConfiguration);
+        configChanged |= changes != 0;
 
         if (configChanged) {
             mWaitingForConfig = true;
-            mWmService.startFreezingDisplay(0 /* exitAnim */, 0 /* enterAnim */, this);
+            if (mTransitionController.isShellTransitionsEnabled()) {
+                requestChangeTransitionIfNeeded(changes);
+            } else {
+                mWmService.startFreezingDisplay(0 /* exitAnim */, 0 /* enterAnim */, this);
+            }
             sendNewConfiguration();
         }
 
@@ -1470,7 +1555,7 @@
         } else if (currentConfig != null
                 // If waiting for a remote rotation, don't prematurely update configuration.
                 && !(mDisplayRotation.isWaitingForRemoteRotation()
-                        || mAtmService.getTransitionController().isCollecting(this))) {
+                        || mTransitionController.isCollecting(this))) {
             // No obvious action we need to take, but if our current state mismatches the
             // activity manager's, update it, disregarding font scale, which should remain set
             // to the value of the previous configuration.
@@ -1621,7 +1706,7 @@
     }
 
     /** Returns {@code true} if the IME is possible to show on the launching activity. */
-    private boolean mayImeShowOnLaunchingActivity(@NonNull ActivityRecord r) {
+    boolean mayImeShowOnLaunchingActivity(@NonNull ActivityRecord r) {
         final WindowState win = r.findMainWindow();
         if (win == null) {
             return false;
@@ -1869,8 +1954,7 @@
      */
     private void applyRotation(final int oldRotation, final int rotation) {
         mDisplayRotation.applyCurrentRotation(rotation);
-        final boolean shellTransitions =
-                mWmService.mAtmService.getTransitionController().getTransitionPlayer() != null;
+        final boolean shellTransitions = mTransitionController.getTransitionPlayer() != null;
         final boolean rotateSeamlessly =
                 mDisplayRotation.isRotatingSeamlessly() && !shellTransitions;
         final Transaction transaction =
@@ -1919,10 +2003,6 @@
                 }
             }
         }
-
-        if (mWmService.mAccessibilityController != null) {
-            mWmService.mAccessibilityController.onRotationChanged(this);
-        }
     }
 
     void configureDisplayPolicy() {
@@ -2013,29 +2093,35 @@
         return mDisplayCutoutCache.getOrCompute(mInitialDisplayCutout, rotation);
     }
 
-    private WmDisplayCutout calculateDisplayCutoutForRotationUncached(
-            DisplayCutout cutout, int rotation) {
+    static WmDisplayCutout calculateDisplayCutoutForRotationAndDisplaySizeUncached(
+            DisplayCutout cutout, int rotation, int displayWidth, int displayHeight) {
         if (cutout == null || cutout == DisplayCutout.NO_CUTOUT) {
             return WmDisplayCutout.NO_CUTOUT;
         }
         if (rotation == ROTATION_0) {
             return WmDisplayCutout.computeSafeInsets(
-                    cutout, mInitialDisplayWidth, mInitialDisplayHeight);
+                    cutout, displayWidth, displayHeight);
         }
         final Insets waterfallInsets =
                 RotationUtils.rotateInsets(cutout.getWaterfallInsets(), rotation);
         final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
-        final Rect[] newBounds = mRotationUtil.getRotatedBounds(
+        final Rect[] newBounds = sRotationUtil.getRotatedBounds(
                 cutout.getBoundingRectsAll(),
-                rotation, mInitialDisplayWidth, mInitialDisplayHeight);
+                rotation, displayWidth, displayHeight);
         final CutoutPathParserInfo info = cutout.getCutoutPathParserInfo();
         final CutoutPathParserInfo newInfo = new CutoutPathParserInfo(
                 info.getDisplayWidth(), info.getDisplayHeight(), info.getDensity(),
                 info.getCutoutSpec(), rotation, info.getScale());
         return WmDisplayCutout.computeSafeInsets(
                 DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo),
-                rotated ? mInitialDisplayHeight : mInitialDisplayWidth,
-                rotated ? mInitialDisplayWidth : mInitialDisplayHeight);
+                rotated ? displayHeight : displayWidth,
+                rotated ? displayWidth : displayHeight);
+    }
+
+    private WmDisplayCutout calculateDisplayCutoutForRotationUncached(
+            DisplayCutout cutout, int rotation) {
+        return calculateDisplayCutoutForRotationAndDisplaySizeUncached(cutout, rotation,
+                mInitialDisplayWidth, mInitialDisplayHeight);
     }
 
     RoundedCorners calculateRoundedCornersForRotation(int rotation) {
@@ -2470,6 +2556,48 @@
         // Update IME parent if needed.
         updateImeParent();
 
+        // Update mirroring surface for MediaProjection, if this DisplayContent is being used
+        // for layer mirroring.
+        if (isCurrentlyMirroring() && mLastMirroredDisplayAreaBounds != null) {
+            // Mirroring has already begun, but update mirroring since the display is now on.
+            final WindowContainer wc = mWmService.mWindowContextListenerController.getContainer(
+                    mTokenToMirror);
+            if (wc == null) {
+                ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                        "Unable to retrieve window container to update layer mirroring for "
+                                + "display %d",
+                        mDisplayId);
+                return;
+            }
+
+            ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                    "Display %d was already layer mirroring, so apply transformations if necessary",
+                    mDisplayId);
+            // Retrieve the size of the DisplayArea to mirror, and continue with the update
+            // if the bounds or orientation has changed.
+            final Rect displayAreaBounds = wc.getDisplayContent().getBounds();
+            int displayAreaOrientation = wc.getDisplayContent().getOrientation();
+            if (!mLastMirroredDisplayAreaBounds.equals(displayAreaBounds)
+                    || lastOrientation != displayAreaOrientation) {
+                Point surfaceSize = fetchSurfaceSizeIfPresent();
+                if (surfaceSize != null) {
+                    ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                            "Going ahead with updating layer mirroring for display %d to new "
+                                    + "bounds %s and/or orientation %d.",
+                            mDisplayId, displayAreaBounds, displayAreaOrientation);
+                    updateMirroredSurface(mWmService.mTransactionFactory.get(),
+                            displayAreaBounds, surfaceSize);
+                } else {
+                    // If the surface removed, do nothing. We will handle this via onDisplayChanged
+                    // (the display will be off if the surface is removed).
+                    ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                            "Unable to update layer mirroring for display %d to new bounds %s"
+                            + " and/or orientation %d, since the surface is not available.",
+                            mDisplayId, displayAreaBounds, displayAreaOrientation);
+                }
+            }
+        }
+
         if (lastOrientation != getConfiguration().orientation) {
             getMetricsLogger().write(
                     new LogMaker(MetricsEvent.ACTION_PHONE_ORIENTATION_CHANGED)
@@ -2490,7 +2618,7 @@
 
     @Override
     boolean isVisibleRequested() {
-        return isVisible();
+        return isVisible() && !mRemoved && !mRemoving;
     }
 
     @Override
@@ -2984,7 +3112,10 @@
 
     @Override
     void removeIfPossible() {
-        if (isAnimating(TRANSITION | PARENTS)) {
+        if (isAnimating(TRANSITION | PARENTS)
+                // isAnimating is a legacy transition query and will be removed, so also add a
+                // check for whether this is in a shell-transition when not using legacy.
+                || mTransitionController.inTransition()) {
             mDeferredRemoval = true;
             return;
         }
@@ -3014,6 +3145,7 @@
             mOverlayLayer.release();
             mInputMonitor.onDisplayRemoved();
             mWmService.mDisplayNotificationController.dispatchDisplayRemoved(this);
+            mWmService.mAccessibilityController.onDisplayRemoved(mDisplayId);
         } finally {
             mDisplayReady = false;
         }
@@ -3085,6 +3217,35 @@
         return mScreenRotationAnimation;
     }
 
+    /**
+     * Requests to start a transition for the display configuration change. The given changes must
+     * be non-zero. This method is no-op if the display has been collected.
+     */
+    void requestChangeTransitionIfNeeded(@ActivityInfo.Config int changes) {
+        final TransitionController controller = mTransitionController;
+        if (controller.isCollecting()) {
+            if (!controller.isCollecting(this)) {
+                controller.collect(this);
+            }
+            return;
+        }
+        final Transition t = controller.requestTransitionIfNeeded(TRANSIT_CHANGE, this);
+        if (t != null) {
+            if (getRotation() != getWindowConfiguration().getRotation()) {
+                mWmService.mLatencyTracker.onActionStart(ACTION_ROTATE_SCREEN);
+                controller.mTransitionMetricsReporter.associate(t,
+                        startTime -> mWmService.mLatencyTracker.onActionEnd(ACTION_ROTATE_SCREEN));
+            }
+            t.setKnownConfigChanges(this, changes);
+        }
+    }
+
+    /** If the display is in transition, there should be a screenshot covering it. */
+    @Override
+    boolean inTransition() {
+        return mScreenRotationAnimation != null || super.inTransition();
+    }
+
     @Override
     public void dumpDebug(ProtoOutputStream proto, long fieldId,
             @WindowTraceLogLevel int logLevel) {
@@ -3105,7 +3266,11 @@
             screenRotationAnimation.dumpDebug(proto, SCREEN_ROTATION_ANIMATION);
         }
         mDisplayFrames.dumpDebug(proto, DISPLAY_FRAMES);
-        mAppTransition.dumpDebug(proto, APP_TRANSITION);
+        if (mTransitionController.isShellTransitionsEnabled()) {
+            mTransitionController.dumpDebugLegacy(proto, APP_TRANSITION);
+        } else {
+            mAppTransition.dumpDebug(proto, APP_TRANSITION);
+        }
         if (mFocusedApp != null) {
             mFocusedApp.writeNameToProto(proto, FOCUSED_APP);
         }
@@ -3193,9 +3358,6 @@
         pw.print(prefix); pw.print("mLayoutSeq="); pw.println(mLayoutSeq);
 
         pw.print("  mCurrentFocus="); pw.println(mCurrentFocus);
-        if (mLastFocus != mCurrentFocus) {
-            pw.print("  mLastFocus="); pw.println(mLastFocus);
-        }
         pw.print("  mFocusedApp="); pw.println(mFocusedApp);
         if (mFixedRotationLaunchingApp != null) {
             pw.println("  mFixedRotationLaunchingApp=" + mFixedRotationLaunchingApp);
@@ -3426,15 +3588,12 @@
             }
         }
 
-        onWindowFocusChanged(oldFocus, newFocus);
-
-        int focusChanged = getDisplayPolicy().focusChangedLw(oldFocus, newFocus);
+        getDisplayPolicy().focusChangedLw(oldFocus, newFocus);
 
         if (imWindowChanged && oldFocus != mInputMethodWindow) {
             // Focus of the input method window changed. Perform layout if needed.
             if (mode == UPDATE_FOCUS_PLACING_SURFACES) {
                 performLayout(true /*initial*/,  updateInputWindows);
-                focusChanged &= ~FINISH_LAYOUT_REDO_LAYOUT;
             } else if (mode == UPDATE_FOCUS_WILL_PLACE_SURFACES) {
                 // Client will do the layout, but we need to assign layers
                 // for handleNewWindowLocked() below.
@@ -3442,16 +3601,6 @@
             }
         }
 
-        if ((focusChanged & FINISH_LAYOUT_REDO_LAYOUT) != 0) {
-            // The change in focus caused us to need to do a layout.  Okay.
-            setLayoutNeeded();
-            if (mode == UPDATE_FOCUS_PLACING_SURFACES) {
-                performLayout(true /*initial*/, updateInputWindows);
-            } else if (mode == UPDATE_FOCUS_REMOVING_FOCUS) {
-                mWmService.mRoot.performSurfacePlacement();
-            }
-        }
-
         if (mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS) {
             // If we defer assigning layers, then the caller is responsible for doing this part.
             getInputMonitor().setInputFocusLw(newFocus, updateInputWindows);
@@ -3472,13 +3621,12 @@
         // focused one starts firing events.
         // TODO(b/151179149) investigate what info accessibility service needs before input can
         // dispatch focus to clients.
-        if (mWmService.mAccessibilityController != null) {
+        if (mWmService.mAccessibilityController.hasCallbacks()) {
             mWmService.mH.sendMessage(PooledLambda.obtainMessage(
                     this::updateAccessibilityOnWindowFocusChanged,
                     mWmService.mAccessibilityController));
         }
 
-        mLastFocus = mCurrentFocus;
         return true;
     }
 
@@ -3486,20 +3634,6 @@
         accessibilityController.onWindowFocusChangedNot(getDisplayId());
     }
 
-    private static void onWindowFocusChanged(WindowState oldFocus, WindowState newFocus) {
-        final Task focusedTask = newFocus != null ? newFocus.getTask() : null;
-        final Task unfocusedTask = oldFocus != null ? oldFocus.getTask() : null;
-        if (focusedTask == unfocusedTask) {
-            return;
-        }
-        if (focusedTask != null) {
-            focusedTask.onWindowFocusChanged(true /* hasFocus */);
-        }
-        if (unfocusedTask != null) {
-            unfocusedTask.onWindowFocusChanged(false /* hasFocus */);
-        }
-    }
-
     /**
      * Set the new focused app to this display.
      *
@@ -3523,7 +3657,14 @@
         }
         ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "setFocusedApp %s displayId=%d Callers=%s",
                 newFocus, getDisplayId(), Debug.getCallers(4));
+        final Task oldTask = mFocusedApp != null ? mFocusedApp.getTask() : null;
+        final Task newTask = newFocus != null ? newFocus.getTask() : null;
         mFocusedApp = newFocus;
+        if (oldTask != newTask) {
+            if (oldTask != null) oldTask.onAppFocusChanged(false);
+            if (newTask != null) newTask.onAppFocusChanged(true);
+        }
+
         getInputMonitor().setFocusedAppLw(newFocus);
         updateTouchExcludeRegion();
         return true;
@@ -3664,6 +3805,7 @@
         final WindowState curTarget = mImeLayeringTarget;
         if (!canUpdateImeTarget()) {
             if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Defer updating IME target");
+            mUpdateImeRequestedWhileDeferred = true;
             return curTarget;
         }
 
@@ -3714,7 +3856,11 @@
     }
 
     boolean shouldImeAttachedToApp() {
-        return isImeControlledByApp()
+        // Force attaching IME to the display when magnifying, or it would be magnified with
+        // target app together.
+        final boolean allowAttachToApp = (mMagnificationSpec == null);
+
+        return allowAttachToApp && isImeControlledByApp()
                 && mImeLayeringTarget != null
                 && mImeLayeringTarget.mActivityRecord != null
                 && mImeLayeringTarget.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
@@ -3796,6 +3942,23 @@
         return mWmService.mForceDesktopModeOnExternalDisplays && !isDefaultDisplay && !isPrivate();
     }
 
+    /** @see WindowManagerInternal#onToggleImeRequested */
+    void onShowImeRequested() {
+        if (mImeLayeringTarget == null || mInputMethodWindow == null) {
+            return;
+        }
+        // If IME window will be shown on the rotated activity, share the transformed state to
+        // IME window so it can compute rotated frame with rotated configuration.
+        if (mImeLayeringTarget.mToken.isFixedRotationTransforming()) {
+            mInputMethodWindow.mToken.linkFixedRotationTransform(mImeLayeringTarget.mToken);
+            // Hide the window until the rotation is done to avoid intermediate artifacts if the
+            // parent surface of IME container is changed.
+            if (mFadeRotationAnimationController != null) {
+                mFadeRotationAnimationController.hideImmediately(mInputMethodWindow.mToken);
+            }
+        }
+    }
+
     @VisibleForTesting
     void setImeLayeringTarget(WindowState target) {
         mImeLayeringTarget = target;
@@ -4036,14 +4199,10 @@
      */
     @VisibleForTesting
     SurfaceControl computeImeParent() {
-        // Force attaching IME to the display when magnifying, or it would be magnified with
-        // target app together.
-        final boolean allowAttachToApp = (mMagnificationSpec == null);
-
         // Attach it to app if the target is part of an app and such app is covering the entire
         // screen. If it's not covering the entire screen the IME might extend beyond the apps
         // bounds.
-        if (allowAttachToApp && shouldImeAttachedToApp()) {
+        if (shouldImeAttachedToApp()) {
             if (mImeLayeringTarget.mActivityRecord != mImeInputTarget.mActivityRecord) {
                 // Do not change parent if the window hasn't requested IME.
                 return null;
@@ -4224,7 +4383,6 @@
         if (DEBUG_INPUT_METHOD) {
             Slog.i(TAG_WM, "Desired input method target: " + imFocus);
             Slog.i(TAG_WM, "Current focus: " + mCurrentFocus + " displayId=" + mDisplayId);
-            Slog.i(TAG_WM, "Last focus: " + mLastFocus + " displayId=" + mDisplayId);
         }
 
         if (DEBUG_INPUT_METHOD) {
@@ -4349,6 +4507,8 @@
                     mTmpApplySurfaceChangesTransactionState.preferMinimalPostProcessing,
                     true /* inTraversal, must call performTraversalInTrans... below */);
         }
+        // If the display now has content, or no longer has content, update layer mirroring.
+        updateMirroring();
 
         final boolean wallpaperVisible = mWallpaperController.isWallpaperVisible();
         if (wallpaperVisible != mLastWallpaperVisible) {
@@ -4514,6 +4674,8 @@
             }
         }
 
+        // clear first just in case.
+        mTmpActivityList.clear();
         // Time to remove any exiting applications?
         forAllRootTasks(task -> {
             final ArrayList<ActivityRecord> activities = task.mExitingActivities;
@@ -4521,16 +4683,24 @@
                 final ActivityRecord activity = activities.get(j);
                 if (!activity.hasVisible && !mDisplayContent.mClosingApps.contains(activity)
                         && (!activity.mIsExiting || activity.isEmpty())) {
-                    // Make sure there is no animation running on this activity, so any windows
-                    // associated with it will be removed as soon as their animations are
-                    // complete.
-                    cancelAnimation();
-                    ProtoLog.v(WM_DEBUG_ADD_REMOVE,
-                            "performLayout: Activity exiting now removed %s", activity);
-                    activity.removeIfPossible();
+                    mTmpActivityList.add(activity);
                 }
             }
         });
+        if (!mTmpActivityList.isEmpty()) {
+            // Make sure there is no animation running on this activity, so any windows
+            // associated with it will be removed as soon as their animations are
+            // complete.
+            cancelAnimation();
+        }
+        for (int i = 0; i < mTmpActivityList.size(); ++i) {
+            final ActivityRecord activity = mTmpActivityList.get(i);
+            ProtoLog.v(WM_DEBUG_ADD_REMOVE,
+                    "performLayout: Activity exiting now removed %s", activity);
+            activity.removeIfPossible();
+        }
+        // Clear afterwards so we don't hold references.
+        mTmpActivityList.clear();
     }
 
     @Override
@@ -4601,7 +4771,9 @@
                 return true;
             }
 
-            if (task.isOrganized()) {
+            // TODO(b/165794880): Freeform task organizer doesn't support drag-resize yet. Remove
+            // the special case when it does.
+            if (task.isOrganized() && task.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
                 return true;
             }
 
@@ -4896,6 +5068,9 @@
      * Increment the deferral count to determine whether to update the IME target.
      */
     void deferUpdateImeTarget() {
+        if (mDeferUpdateImeTargetCount == 0) {
+            mUpdateImeRequestedWhileDeferred = false;
+        }
         mDeferUpdateImeTargetCount++;
     }
 
@@ -4909,7 +5084,7 @@
         }
 
         mDeferUpdateImeTargetCount--;
-        if (mDeferUpdateImeTargetCount == 0) {
+        if (mDeferUpdateImeTargetCount == 0 && mUpdateImeRequestedWhileDeferred) {
             computeImeTarget(true /* updateImeTarget */);
         }
     }
@@ -4954,10 +5129,18 @@
         }
     }
 
+    /**
+     * @deprecated new transition should use {@link #requestTransitionAndLegacyPrepare(int, int)}
+     */
+    @Deprecated
     void prepareAppTransition(@WindowManager.TransitionType int transit) {
         prepareAppTransition(transit, 0 /* flags */);
     }
 
+    /**
+     * @deprecated new transition should use {@link #requestTransitionAndLegacyPrepare(int, int)}
+     */
+    @Deprecated
     void prepareAppTransition(@WindowManager.TransitionType int transit,
             @WindowManager.TransitionFlags int flags) {
         final boolean prepared = mAppTransition.prepareAppTransition(transit, flags);
@@ -4970,26 +5153,27 @@
      * Helper that both requests a transition (using the new transition system) and prepares
      * the legacy transition system. Use this when both systems have the same start-point.
      *
-     * @see TransitionController#requestTransitionIfNeeded(int, int, WindowContainer)
+     * @see TransitionController#requestTransitionIfNeeded(int, int, WindowContainer,
+     *      WindowContainer)
      * @see AppTransition#prepareAppTransition
      */
     void requestTransitionAndLegacyPrepare(@WindowManager.TransitionType int transit,
             @WindowManager.TransitionFlags int flags) {
         prepareAppTransition(transit, flags);
-        mAtmService.getTransitionController().requestTransitionIfNeeded(transit, flags,
-                null /* trigger */);
+        mTransitionController.requestTransitionIfNeeded(transit, flags,
+                null /* trigger */, this);
     }
 
     /** @see #requestTransitionAndLegacyPrepare(int, int) */
     void requestTransitionAndLegacyPrepare(@WindowManager.TransitionType int transit,
             @Nullable WindowContainer trigger) {
         prepareAppTransition(transit);
-        mAtmService.getTransitionController().requestTransitionIfNeeded(transit, 0 /* flags */,
-                trigger);
+        mTransitionController.requestTransitionIfNeeded(transit, 0 /* flags */,
+                trigger, this);
     }
 
     void executeAppTransition() {
-        mAtmService.getTransitionController().setReady();
+        mTransitionController.setReady(this);
         if (mAppTransition.isTransitionSet()) {
             ProtoLog.w(WM_DEBUG_APP_TRANSITIONS,
                     "Execute app transition: %s, displayId: %d Callers=%s",
@@ -4999,6 +5183,11 @@
         }
     }
 
+    void cancelAppTransition() {
+        if (!mAppTransition.isTransitionSet() || mAppTransition.isRunning()) return;
+        mAppTransition.abort();
+    }
+
     /**
      * Update pendingLayoutChanges after app transition has finished.
      */
@@ -5018,9 +5207,7 @@
         onAppTransitionDone();
 
         changes |= FINISH_LAYOUT_REDO_LAYOUT;
-        if (DEBUG_WALLPAPER_LIGHT) {
-            Slog.v(TAG_WM, "Wallpaper layer changed: assigning layers + relayout");
-        }
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper layer changed: assigning layers + relayout");
         computeImeTarget(true /* updateImeTarget */);
         mWallpaperMayChange = true;
         // Since the window list has been rebuilt, focus might have to be recomputed since the
@@ -5032,6 +5219,12 @@
 
     /** Check if pending app transition is for activity / task launch. */
     boolean isNextTransitionForward() {
+        // TODO(b/191375840): decouple "forwardness" from transition system.
+        if (mTransitionController.isShellTransitionsEnabled()) {
+            @WindowManager.TransitionType int type =
+                    mTransitionController.getCollectingTransitionType();
+            return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT;
+        }
         return mAppTransition.containsTransitRequest(TRANSIT_OPEN)
                 || mAppTransition.containsTransitRequest(TRANSIT_TO_FRONT);
     }
@@ -5073,7 +5266,7 @@
         }
         if (!mLocationInParentWindow.equals(x, y)) {
             mLocationInParentWindow.set(x, y);
-            if (mWmService.mAccessibilityController != null) {
+            if (mWmService.mAccessibilityController.hasCallbacks()) {
                 mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(mDisplayId);
             }
             notifyLocationInParentDisplayChanged();
@@ -5415,6 +5608,15 @@
             } else if (displayState == Display.STATE_ON) {
                 mOffTokenAcquirer.release(mDisplayId);
             }
+            ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                    "Display %d state is now (%d), so update layer mirroring?",
+                    mDisplayId, displayState);
+            if (mLastDisplayState != displayState) {
+                // If state is on due to surface being added, then start layer mirroring.
+                // If state is off due to surface being removed, then stop layer mirroring.
+                updateMirroring();
+            }
+            mLastDisplayState = displayState;
         }
         mWmService.requestTraversal();
     }
@@ -5595,6 +5797,9 @@
             }
             mWmService.mDisplayNotificationController.dispatchDisplayChanged(
                     this, getConfiguration());
+            if (isReady() && mTransitionController.isShellTransitionsEnabled()) {
+                requestChangeTransitionIfNeeded(changes);
+            }
         }
         return changes;
     }
@@ -5604,7 +5809,8 @@
         final Configuration currOverrideConfig = getRequestedOverrideConfiguration();
         final int currRotation = currOverrideConfig.windowConfiguration.getRotation();
         final int overrideRotation = overrideConfiguration.windowConfiguration.getRotation();
-        if (currRotation != ROTATION_UNDEFINED && currRotation != overrideRotation) {
+        if (currRotation != ROTATION_UNDEFINED && overrideRotation != ROTATION_UNDEFINED
+                && currRotation != overrideRotation) {
             applyRotationAndFinishFixedRotation(currRotation, overrideRotation);
         }
         mCurrentOverrideConfigurationChanges = currOverrideConfig.diff(overrideConfiguration);
@@ -5615,6 +5821,14 @@
                 ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED);
     }
 
+    @Override
+    void onResize() {
+        super.onResize();
+        if (mWmService.mAccessibilityController.hasCallbacks()) {
+            mWmService.mAccessibilityController.onDisplaySizeChanged(this);
+        }
+    }
+
     /**
      * If the launching rotated activity ({@link #mFixedRotationLaunchingApp}) is null, it simply
      * applies the rotation to display. Otherwise because the activity has shown as rotated, the
@@ -5695,6 +5909,13 @@
         }
         mRemoved = true;
 
+        if (mMirroredSurface != null) {
+            // Do not wait for the mirrored surface to be garbage collected, but clean up
+            // immediately.
+            mWmService.mTransactionFactory.get().remove(mMirroredSurface).apply();
+            mMirroredSurface = null;
+        }
+
         // Only update focus/visibility for the last one because there may be many root tasks are
         // reparented and the intermediate states are unnecessary.
         if (lastReparentedRootTask != null) {
@@ -5857,6 +6078,197 @@
         return true;
     }
 
+    /**
+     * Sets if Display APIs should be sandboxed to the activity window bounds.
+     */
+    void setSandboxDisplayApis(boolean sandboxDisplayApis) {
+        mSandboxDisplayApis = sandboxDisplayApis;
+    }
+
+    /**
+     * Returns {@code true} is Display APIs should be sandboxed to the activity window bounds,
+     * {@code false} otherwise. Default to true, unless set for debugging purposes.
+     */
+    boolean sandboxDisplayApis() {
+        return mSandboxDisplayApis;
+    }
+
+    /**
+     * Start mirroring to this DisplayContent if it does not have its own content. Captures the
+     * content of a WindowContainer indicated by a WindowToken. If unable to start mirroring, falls
+     * back to original MediaProjection approach.
+     */
+    private void startMirrorIfNeeded() {
+        // Only mirror if this display does not have its own content, is not mirroring already,
+        // and if this display is on (it has a surface to write output to).
+        if (mLastHasContent || isCurrentlyMirroring() || mDisplay.getState() == Display.STATE_OFF) {
+            return;
+        }
+
+        // Given the WindowToken of the DisplayArea to mirror, retrieve the associated
+        // SurfaceControl.
+        IBinder tokenToMirror = mWmService.mDisplayManagerInternal.getWindowTokenClientToMirror(
+                mDisplayId);
+        if (tokenToMirror == null) {
+            // This DisplayContent instance is not involved in layer mirroring. If the display
+            // has been created for capturing, fall back to prior MediaProjection approach.
+            return;
+        }
+
+        final WindowContainer wc = mWmService.mWindowContextListenerController.getContainer(
+                tokenToMirror);
+        if (wc == null) {
+            // Un-set the window token to mirror for this VirtualDisplay, to fall back to the
+            // original MediaProjection approach.
+            mWmService.mDisplayManagerInternal.setWindowTokenClientToMirror(mDisplayId, null);
+            ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                    "Unable to retrieve window container to start layer mirroring for display %d",
+                    mDisplayId);
+            return;
+        }
+
+        Point surfaceSize = fetchSurfaceSizeIfPresent();
+        if (surfaceSize == null) {
+            ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                    "Unable to start layer mirroring for display %d since the surface is not "
+                            + "available.",
+                    mDisplayId);
+            return;
+        }
+        ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                "Display %d has no content and is on, so start layer mirroring for state %d",
+                mDisplayId, mDisplay.getState());
+
+        // Create a mirrored hierarchy for the SurfaceControl of the DisplayArea to capture.
+        SurfaceControl sc = wc.getDisplayContent().getSurfaceControl();
+        mMirroredSurface = SurfaceControl.mirrorSurface(sc);
+        SurfaceControl.Transaction transaction = mWmService.mTransactionFactory.get()
+                // Set the mMirroredSurface's parent to the root SurfaceControl for this
+                // DisplayContent. This brings the new mirrored hierarchy under this DisplayContent,
+                // so SurfaceControl will write the layers of this hierarchy to the output surface
+                // provided by the app.
+                .reparent(mMirroredSurface, mSurfaceControl)
+                // Reparent the SurfaceControl of this DisplayContent to null, to prevent content
+                // being added to it. This ensures that no app launched explicitly on the
+                // VirtualDisplay will show up as part of the mirrored content.
+                .reparent(mWindowingLayer, null);
+        // Retrieve the size of the DisplayArea to mirror.
+        updateMirroredSurface(transaction, wc.getDisplayContent().getBounds(), surfaceSize);
+        mTokenToMirror = tokenToMirror;
+
+        // No need to clean up. In SurfaceFlinger, parents hold references to their children. The
+        // mirrored SurfaceControl is alive since the parent DisplayContent SurfaceControl is
+        // holding a reference to it. Therefore, the mirrored SurfaceControl will be cleaned up
+        // when the VirtualDisplay is destroyed - which will clean up this DisplayContent.
+    }
+
+    /**
+     * Start mirroring if this DisplayContent no longer has content. Stop mirroring if it now
+     * has content or the display is not on.
+     */
+    private void updateMirroring() {
+        if (isCurrentlyMirroring() && (mLastHasContent
+                || mDisplay.getState() == Display.STATE_OFF)) {
+            ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                    "Display %d has content (%b) so disable layer mirroring", mDisplayId,
+                    mLastHasContent);
+            // If the display is not on and it is a virtual display, then it no longer has an
+            // associated surface to write output to.
+            // If the display now has content, stop mirroring to it.
+            mWmService.mTransactionFactory.get()
+                    // Remove the reference to mMirroredSurface, to clean up associated memory.
+                    .remove(mMirroredSurface)
+                    // Reparent the SurfaceControl of this DisplayContent back to mSurfaceControl,
+                    // to allow content to be added to it. This allows this DisplayContent to stop
+                    // mirroring and show content normally.
+                    .reparent(mWindowingLayer, mSurfaceControl).apply();
+            // Stop mirroring by destroying the reference to the mirrored layer.
+            mMirroredSurface = null;
+            // Do not un-set the token, in case content is removed and mirroring should begin again.
+        } else {
+            // Display no longer has content, or now has a surface to write to, so try to start
+            // mirroring to it.
+            startMirrorIfNeeded();
+        }
+    }
+
+    /**
+     * Apply transformations to the mirrored surface to ensure the captured contents are scaled to
+     * fit and centred in the output surface.
+     *
+     * @param transaction       the transaction to include transformations of mMirroredSurface
+     *                          to. Transaction is not applied before returning.
+     * @param displayAreaBounds bounds of the DisplayArea to mirror to the surface provided by
+     *                          the app.
+     * @param surfaceSize       the default size of the surface to write the display area content to
+     */
+    @VisibleForTesting
+    void updateMirroredSurface(SurfaceControl.Transaction transaction,
+            Rect displayAreaBounds, Point surfaceSize) {
+        // Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the
+        // output surface.
+        float scaleX = surfaceSize.x / (float) displayAreaBounds.width();
+        float scaleY = surfaceSize.y / (float) displayAreaBounds.height();
+        float scale = Math.min(scaleX, scaleY);
+        int scaledWidth = Math.round(scale * (float) displayAreaBounds.width());
+        int scaledHeight = Math.round(scale * (float) displayAreaBounds.height());
+
+        // Calculate the shift to apply to the root mirror SurfaceControl to centre the mirrored
+        // contents in the output surface.
+        int shiftedX = 0;
+        if (scaledWidth != surfaceSize.x) {
+            shiftedX = (surfaceSize.x - scaledWidth) / 2;
+        }
+        int shiftedY = 0;
+        if (scaledHeight != surfaceSize.y) {
+            shiftedY = (surfaceSize.y - scaledHeight) / 2;
+        }
+
+        transaction
+                // Crop the area to capture to exclude the 'extra' wallpaper that is used
+                // for parallax (b/189930234).
+                .setWindowCrop(mMirroredSurface, displayAreaBounds.width(),
+                        displayAreaBounds.height())
+                // Scale the root mirror SurfaceControl, based upon the size difference between the
+                // source (DisplayArea to capture) and output (surface the app reads images from).
+                .setMatrix(mMirroredSurface, scale, 0 /* dtdx */, 0 /* dtdy */, scale)
+                // Position needs to be updated when the mirrored DisplayArea has changed, since
+                // the content will no longer be centered in the output surface.
+                .setPosition(mMirroredSurface, shiftedX /* x */, shiftedY /* y */)
+                .apply();
+        mLastMirroredDisplayAreaBounds = new Rect(displayAreaBounds);
+    }
+
+    /**
+     * Returns a non-null {@link Point} if the surface is present, or null otherwise
+     */
+    Point fetchSurfaceSizeIfPresent() {
+        // Retrieve the default size of the surface the app provided to
+        // MediaProjection#createVirtualDisplay. Note the app is the consumer of the surface,
+        // since it reads out buffers from the surface, and SurfaceFlinger is the producer since
+        // it writes the mirrored layers to the buffers.
+        Point surfaceSize = mWmService.mDisplayManagerInternal.getDisplaySurfaceDefaultSize(
+                mDisplayId);
+        if (surfaceSize == null) {
+            // Layer mirroring started with a null surface, so do not apply any transformations yet.
+            // State of virtual display will change to 'ON' when the surface is set.
+            // will get event DISPLAY_DEVICE_EVENT_CHANGED
+            ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
+                    "Provided surface for layer mirroring on display %d is not present, so do not"
+                            + " update the surface",
+                    mDisplayId);
+            return null;
+        }
+        return surfaceSize;
+    }
+
+    /**
+     * Returns {@code true} if this DisplayContent is currently layer mirroring.
+     */
+    boolean isCurrentlyMirroring() {
+        return mTokenToMirror != null && mMirroredSurface != null;
+    }
+
     /** The entry for proceeding to handle {@link #mFixedRotationLaunchingApp}. */
     class FixedRotationTransitionListener extends WindowManagerInternal.AppTransitionListener {
 
@@ -6001,7 +6413,7 @@
 
     class RemoteInsetsControlTarget implements InsetsControlTarget {
         private final IDisplayWindowInsetsController mRemoteInsetsController;
-        private final InsetsState mRequestedInsetsState = new InsetsState();
+        private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
 
         RemoteInsetsControlTarget(IDisplayWindowInsetsController controller) {
             mRemoteInsetsController = controller;
@@ -6063,15 +6475,11 @@
             if (type == ITYPE_IME) {
                 return getInsetsStateController().getImeSourceProvider().isImeShowing();
             }
-            return mRequestedInsetsState.getSourceOrDefaultVisibility(type);
+            return mRequestedVisibilities.getVisibility(type);
         }
 
-        void updateRequestedVisibility(InsetsState state) {
-            for (int i = 0; i < InsetsState.SIZE; i++) {
-                final InsetsSource source = state.peekSource(i);
-                if (source == null) continue;
-                mRequestedInsetsState.addSource(source);
-            }
+        void setRequestedVisibilities(InsetsVisibilities requestedVisibilities) {
+            mRequestedVisibilities.set(requestedVisibilities);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index d0457b0..0c0d01c 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -16,12 +16,9 @@
 
 package com.android.server.wm;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
 import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
 import static android.util.RotationUtils.deltaRotation;
@@ -39,6 +36,7 @@
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES;
 import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT;
+import static android.view.ViewRootImpl.INSETS_LAYOUT_GENERALIZATION;
 import static android.view.ViewRootImpl.computeWindowBounds;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
@@ -142,6 +140,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.View;
 import android.view.ViewDebug;
@@ -173,6 +172,9 @@
 import com.android.server.wm.InputMonitor.EventReceiverInputConsumer;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 /**
@@ -313,17 +315,41 @@
 
     private WindowState mSystemUiControllingWindow;
 
+    // Candidate window to determine the color of navigation bar. The window needs to be top
+    // fullscreen-app windows or dim layers that are intersecting with the window frame of status
+    // bar.
+    private WindowState mNavBarColorWindowCandidate;
+
+    // The window to determine opacity and background of translucent navigation bar. The window
+    // needs to be opaque.
+    private WindowState mNavBarBackgroundWindow;
+
+    /**
+     * Windows to determine the color of status bar. See {@link #mNavBarColorWindowCandidate} for
+     * the conditions of being candidate window.
+     */
+    private final ArrayList<WindowState> mStatusBarColorWindows = new ArrayList<>();
+
+    /**
+     * Windows to determine opacity and background of translucent status bar. The window needs to be
+     * opaque
+     */
+    private final ArrayList<WindowState> mStatusBarBackgroundWindows = new ArrayList<>();
+
+    private String mFocusedApp;
     private int mLastDisableFlags;
     private int mLastAppearance;
-    private int mLastFullscreenAppearance;
-    private int mLastDockedAppearance;
     private int mLastBehavior;
-    private final Rect mNonDockedRootTaskBounds = new Rect();
-    private final Rect mDockedRootTaskBounds = new Rect();
-    private final Rect mLastNonDockedRootTaskBounds = new Rect();
-    private final Rect mLastDockedRootTaskBounds = new Rect();
+    private InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+    private AppearanceRegion[] mLastStatusBarAppearanceRegions;
 
-    // What we last reported to system UI about whether the focused window is fullscreen/immersive.
+    /** The union of checked bounds while fetching {@link #mStatusBarColorWindows}. */
+    private final Rect mStatusBarColorCheckedBounds = new Rect();
+
+    /** The union of checked bounds while fetching {@link #mStatusBarBackgroundWindows}. */
+    private final Rect mStatusBarBackgroundCheckedBounds = new Rect();
+
+    // What we last reported to input dispatcher about whether the focused window is fullscreen.
     private boolean mLastFocusIsFullscreen = false;
 
     // If nonzero, a panic gesture was performed at that time in uptime millis and is still pending.
@@ -333,19 +359,15 @@
     private static final Rect sTmpRect = new Rect();
     private static final Rect sTmpNavFrame = new Rect();
     private static final Rect sTmpStatusFrame = new Rect();
+    private static final Rect sTmpDecorFrame = new Rect();
     private static final Rect sTmpScreenDecorFrame = new Rect();
     private static final Rect sTmpLastParentFrame = new Rect();
     private static final Rect sTmpDisplayFrameBounds = new Rect();
 
     private WindowState mTopFullscreenOpaqueWindowState;
-    private WindowState mTopFullscreenOpaqueOrDimmingWindowState;
-    private WindowState mTopDockedOpaqueWindowState;
-    private WindowState mTopDockedOpaqueOrDimmingWindowState;
     private boolean mTopIsFullscreen;
     private boolean mForceStatusBar;
     private int mNavBarOpacityMode = NAV_BAR_OPAQUE_WHEN_FREEFORM_OR_DOCKED;
-    private boolean mForcingShowNavBar;
-    private int mForcingShowNavBarLayer;
     private boolean mForceShowSystemBars;
 
     private boolean mShowingDream;
@@ -432,8 +454,10 @@
 
         final int displayId = displayContent.getDisplayId();
 
-        mBarContentFrames.put(TYPE_STATUS_BAR, new Rect());
-        mBarContentFrames.put(TYPE_NAVIGATION_BAR, new Rect());
+        if (!INSETS_LAYOUT_GENERALIZATION) {
+            mBarContentFrames.put(TYPE_STATUS_BAR, new Rect());
+            mBarContentFrames.put(TYPE_NAVIGATION_BAR, new Rect());
+        }
 
         final Resources r = mContext.getResources();
         mCarDockEnablesAccelerometer = r.getBoolean(R.bool.config_carDockEnablesAccelerometer);
@@ -458,7 +482,8 @@
                             if (mStatusBar != null) {
                                 requestTransientBars(mStatusBar);
                             }
-                            checkAltBarSwipeForTransientBars(ALT_BAR_TOP);
+                            checkAltBarSwipeForTransientBars(ALT_BAR_TOP,
+                                    false /* allowForAllPositions */);
                         }
                     }
 
@@ -469,7 +494,8 @@
                                     && mNavigationBarPosition == NAV_BAR_BOTTOM) {
                                 requestTransientBars(mNavigationBar);
                             }
-                            checkAltBarSwipeForTransientBars(ALT_BAR_BOTTOM);
+                            checkAltBarSwipeForTransientBars(ALT_BAR_BOTTOM,
+                                    false /* allowForAllPositions */);
                         }
                     }
 
@@ -479,13 +505,13 @@
                         synchronized (mLock) {
                             mDisplayContent.calculateSystemGestureExclusion(
                                     excludedRegion, null /* outUnrestricted */);
-                            final boolean excluded =
-                                    mSystemGestures.currentGestureStartedInRegion(excludedRegion);
+                            final boolean allowSideSwipe = mNavigationBarAlwaysShowOnSideGesture &&
+                                    !mSystemGestures.currentGestureStartedInRegion(excludedRegion);
                             if (mNavigationBar != null && (mNavigationBarPosition == NAV_BAR_RIGHT
-                                    || !excluded && mNavigationBarAlwaysShowOnSideGesture)) {
+                                    || allowSideSwipe)) {
                                 requestTransientBars(mNavigationBar);
                             }
-                            checkAltBarSwipeForTransientBars(ALT_BAR_RIGHT);
+                            checkAltBarSwipeForTransientBars(ALT_BAR_RIGHT, allowSideSwipe);
                         }
                         excludedRegion.recycle();
                     }
@@ -496,13 +522,13 @@
                         synchronized (mLock) {
                             mDisplayContent.calculateSystemGestureExclusion(
                                     excludedRegion, null /* outUnrestricted */);
-                            final boolean excluded =
-                                    mSystemGestures.currentGestureStartedInRegion(excludedRegion);
+                            final boolean allowSideSwipe = mNavigationBarAlwaysShowOnSideGesture &&
+                                    !mSystemGestures.currentGestureStartedInRegion(excludedRegion);
                             if (mNavigationBar != null && (mNavigationBarPosition == NAV_BAR_LEFT
-                                    || !excluded && mNavigationBarAlwaysShowOnSideGesture)) {
+                                    || allowSideSwipe)) {
                                 requestTransientBars(mNavigationBar);
                             }
-                            checkAltBarSwipeForTransientBars(ALT_BAR_LEFT);
+                            checkAltBarSwipeForTransientBars(ALT_BAR_LEFT, allowSideSwipe);
                         }
                         excludedRegion.recycle();
                     }
@@ -592,7 +618,8 @@
             }
 
             @Override
-            public int onAppTransitionStartingLocked(boolean keyguardGoingAway, long duration,
+            public int onAppTransitionStartingLocked(boolean keyguardGoingAway,
+                    boolean keyguardOccluding, long duration,
                     long statusBarAnimationStartTime, long statusBarAnimationDuration) {
                 mHandler.post(() -> {
                     StatusBarManagerInternal statusBar = getStatusBarManagerInternal();
@@ -615,6 +642,7 @@
             }
         };
         displayContent.mAppTransition.registerListenerLocked(mAppTransitionListener);
+        displayContent.mTransitionController.registerLegacyListener(mAppTransitionListener);
         mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper,
                 mService.mVrModeEnabled);
 
@@ -654,17 +682,19 @@
         mHandler.post(mGestureNavigationSettingsObserver::register);
     }
 
-    private void checkAltBarSwipeForTransientBars(@WindowManagerPolicy.AltBarPosition int pos) {
-        if (mStatusBarAlt != null && mStatusBarAltPosition == pos) {
+    private void checkAltBarSwipeForTransientBars(@WindowManagerPolicy.AltBarPosition int pos,
+            boolean allowForAllPositions) {
+        if (mStatusBarAlt != null && (mStatusBarAltPosition == pos || allowForAllPositions)) {
             requestTransientBars(mStatusBarAlt);
         }
-        if (mNavigationBarAlt != null && mNavigationBarAltPosition == pos) {
+        if (mNavigationBarAlt != null
+                && (mNavigationBarAltPosition == pos || allowForAllPositions)) {
             requestTransientBars(mNavigationBarAlt);
         }
-        if (mClimateBarAlt != null && mClimateBarAltPosition == pos) {
+        if (mClimateBarAlt != null && (mClimateBarAltPosition == pos || allowForAllPositions)) {
             requestTransientBars(mClimateBarAlt);
         }
-        if (mExtraNavBarAlt != null && mExtraNavBarAltPosition == pos) {
+        if (mExtraNavBarAlt != null && (mExtraNavBarAltPosition == pos || allowForAllPositions)) {
             requestTransientBars(mExtraNavBarAlt);
         }
     }
@@ -869,15 +899,6 @@
                 // letterboxed. Hence always let them extend under the cutout.
                 attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
                 break;
-            case TYPE_NOTIFICATION_SHADE:
-                // If the Keyguard is in a hidden state (occluded by another window), we force to
-                // remove the wallpaper and keyguard flag so that any change in-flight after setting
-                // the keyguard as occluded wouldn't set these flags again.
-                // See {@link #processKeyguardSetHiddenResultLw}.
-                if (mService.mPolicy.isKeyguardOccluded()) {
-                    attrs.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-                }
-                break;
 
             case TYPE_TOAST:
                 // While apps should use the dedicated toast APIs to add such windows
@@ -1081,7 +1102,9 @@
                 mStatusBar = win;
                 final TriConsumer<DisplayFrames, WindowState, Rect> frameProvider =
                         (displayFrames, windowState, rect) -> {
-                            rect.bottom = rect.top + getStatusBarHeight(displayFrames);
+                            if (!INSETS_LAYOUT_GENERALIZATION) {
+                                rect.bottom = rect.top + getStatusBarHeight(displayFrames);
+                            }
                         };
                 final TriConsumer<DisplayFrames, WindowState, Rect> gestureFrameProvider =
                         (displayFrames, windowState, rect) -> {
@@ -1104,18 +1127,22 @@
                 mNavigationBar = win;
                 mDisplayContent.setInsetProvider(ITYPE_NAVIGATION_BAR, win,
                         (displayFrames, windowState, inOutFrame) -> {
-
-                            // In Gesture Nav, navigation bar frame is larger than frame to
-                            // calculate inset.
-                            if (navigationBarPosition(displayFrames.mDisplayWidth,
-                                    displayFrames.mDisplayHeight,
-                                    displayFrames.mRotation) == NAV_BAR_BOTTOM
-                                    && !mNavButtonForcedVisible) {
-                                sTmpRect.set(inOutFrame);
-                                sTmpRect.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
-                                inOutFrame.top = sTmpRect.bottom
-                                        - getNavigationBarHeight(displayFrames.mRotation,
-                                        mDisplayContent.getConfiguration().uiMode);
+                            if (INSETS_LAYOUT_GENERALIZATION) {
+                                inOutFrame.inset(windowState.getLayoutingAttrs(
+                                        displayFrames.mRotation).providedInternalInsets);
+                            } else {
+                                // In Gesture Nav, navigation bar frame is larger than frame to
+                                // calculate inset.
+                                if (navigationBarPosition(displayFrames.mDisplayWidth,
+                                        displayFrames.mDisplayHeight,
+                                        displayFrames.mRotation) == NAV_BAR_BOTTOM
+                                        && !mNavButtonForcedVisible) {
+                                    sTmpRect.set(inOutFrame);
+                                    sTmpRect.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
+                                    inOutFrame.top = sTmpRect.bottom
+                                            - getNavigationBarHeight(displayFrames.mRotation,
+                                            mDisplayContent.getConfiguration().uiMode);
+                                }
                             }
                         },
 
@@ -1158,6 +1185,12 @@
             default:
                 if (attrs.providesInsetsTypes != null) {
                     for (@InternalInsetsType int insetsType : attrs.providesInsetsTypes) {
+                        final TriConsumer<DisplayFrames, WindowState, Rect> imeFrameProvider =
+                                !attrs.providedInternalImeInsets.equals(Insets.NONE)
+                                    ? (displayFrames, windowState, inOutFrame) ->
+                                            inOutFrame.inset(windowState.getLayoutingAttrs(
+                                                displayFrames.mRotation).providedInternalImeInsets)
+                                    : null;
                         switch (insetsType) {
                             case ITYPE_STATUS_BAR:
                                 mStatusBarAlt = win;
@@ -1176,7 +1209,15 @@
                                 mExtraNavBarAltPosition = getAltBarPosition(attrs);
                                 break;
                         }
-                        mDisplayContent.setInsetProvider(insetsType, win, null);
+                        if (!INSETS_LAYOUT_GENERALIZATION) {
+                            mDisplayContent.setInsetProvider(insetsType, win, null,
+                                    imeFrameProvider);
+                        } else {
+                            mDisplayContent.setInsetProvider(insetsType, win, (displayFrames,
+                                    windowState, inOutFrame) -> inOutFrame.inset(
+                                            windowState.getLayoutingAttrs(displayFrames.mRotation)
+                                                    .providedInternalInsets), imeFrameProvider);
+                        }
                     }
                 }
                 break;
@@ -1267,8 +1308,17 @@
     }
 
     private int getStatusBarHeight(DisplayFrames displayFrames) {
-        return Math.max(mStatusBarHeightForRotation[displayFrames.mRotation],
-                displayFrames.mDisplayCutoutSafe.top);
+        int statusBarHeight;
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            if (mStatusBar != null) {
+                statusBarHeight = mStatusBar.getLayoutingAttrs(displayFrames.mRotation).height;
+            } else {
+                statusBarHeight = 0;
+            }
+        } else {
+            statusBarHeight = mStatusBarHeightForRotation[displayFrames.mRotation];
+        }
+        return Math.max(statusBarHeight, displayFrames.mDisplayCutoutSafe.top);
     }
 
     WindowState getStatusBar() {
@@ -1403,7 +1453,7 @@
     /**
      * @return true if the system bars are forced to stay visible
      */
-    public boolean areSystemBarsForcedShownLw(WindowState windowState) {
+    public boolean areSystemBarsForcedShownLw() {
         return mForceShowSystemBars;
     }
 
@@ -1438,13 +1488,30 @@
             WindowFrames simulatedWindowFrames, SparseArray<Rect> contentFrames,
             Consumer<Rect> layout) {
         win.setSimulatedWindowFrames(simulatedWindowFrames);
+        final int requestedHeight = win.mRequestedHeight;
+        final int requestedWidth = win.mRequestedWidth;
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            // Without a full layout process, in order to layout the system bars correctly, we need
+            // to set the requested size and the initial display frames to the window.
+            WindowManager.LayoutParams params = win.getLayoutingAttrs(displayFrames.mRotation);
+            win.setRequestedSize(params.width, params.height);
+            sTmpDecorFrame.set(0, 0, displayFrames.mDisplayWidth, displayFrames.mDisplayHeight);
+            simulatedWindowFrames.setFrames(sTmpDecorFrame /* parentFrame */,
+                    sTmpDecorFrame /* displayFrame */);
+            simulatedWindowFrames.mIsSimulatingDecorWindow = true;
+        }
         final Rect contentFrame = new Rect();
         try {
             layout.accept(contentFrame);
         } finally {
             win.setSimulatedWindowFrames(null);
+            if (INSETS_LAYOUT_GENERALIZATION) {
+                win.setRequestedSize(requestedWidth, requestedHeight);
+            }
         }
-        contentFrames.put(win.mAttrs.type, contentFrame);
+        if (!INSETS_LAYOUT_GENERALIZATION) {
+            contentFrames.put(win.mAttrs.type, contentFrame);
+        }
         mDisplayContent.getInsetsStateController().computeSimulatedState(
                 win, displayFrames, simulatedWindowFrames);
     }
@@ -1455,15 +1522,51 @@
      * some temporal states, but doesn't change the window frames used to show on screen.
      */
     void simulateLayoutDisplay(DisplayFrames displayFrames, SparseArray<Rect> barContentFrames) {
-        final WindowFrames simulatedWindowFrames = new WindowFrames();
-        if (mNavigationBar != null) {
-            simulateLayoutDecorWindow(mNavigationBar, displayFrames, simulatedWindowFrames,
-                    barContentFrames, contentFrame -> layoutNavigationBar(displayFrames,
-                            contentFrame));
-        }
-        if (mStatusBar != null) {
-            simulateLayoutDecorWindow(mStatusBar, displayFrames, simulatedWindowFrames,
-                    barContentFrames, contentFrame -> layoutStatusBar(displayFrames, contentFrame));
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            final InsetsStateController insetsStateController =
+                    mDisplayContent.getInsetsStateController();
+            for (int type = 0; type < InsetsState.SIZE; type++) {
+                final InsetsSourceProvider provider =
+                        insetsStateController.peekSourceProvider(type);
+                if (provider == null || !provider.hasWindow()
+                        || provider.mWin.getControllableInsetProvider() != provider) {
+                    continue;
+                }
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(provider.mWin, displayFrames, simulatedWindowFrames,
+                        barContentFrames,
+                        contentFrame -> simulateLayoutForContentFrame(displayFrames,
+                                provider.mWin, contentFrame));
+            }
+        } else {
+            if (mNavigationBar != null) {
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(mNavigationBar, displayFrames, simulatedWindowFrames,
+                        barContentFrames, contentFrame -> layoutNavigationBar(displayFrames,
+                                contentFrame));
+            }
+            if (mStatusBar != null) {
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(mStatusBar, displayFrames, simulatedWindowFrames,
+                        barContentFrames,
+                        contentFrame -> layoutStatusBar(displayFrames, contentFrame));
+            }
+            if (mExtraNavBarAlt != null) {
+                // There's no pre-defined behavior for the extra navigation bar, we need to use the
+                // new flexible insets logic anyway.
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(mExtraNavBarAlt, displayFrames, simulatedWindowFrames,
+                        barContentFrames,
+                        contentFrame -> simulateLayoutForContentFrame(displayFrames,
+                                mExtraNavBarAlt, contentFrame));
+            }
+            if (mClimateBarAlt != null) {
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(mClimateBarAlt, displayFrames, simulatedWindowFrames,
+                        barContentFrames,
+                        contentFrame -> simulateLayoutForContentFrame(displayFrames,
+                                mClimateBarAlt, contentFrame));
+            }
         }
     }
 
@@ -1482,7 +1585,7 @@
         windowFrames.setFrames(sTmpStatusFrame /* parentFrame */,
                 sTmpStatusFrame /* displayFrame */);
         // Let the status bar determine its size.
-        mStatusBar.computeFrameAndUpdateSourceFrame();
+        mStatusBar.computeFrameAndUpdateSourceFrame(displayFrames);
 
         // For layout, the status bar is always at the top with our fixed height.
         int statusBarBottom = displayFrames.mUnrestricted.top
@@ -1533,18 +1636,18 @@
         } else if (navBarPosition == NAV_BAR_RIGHT) {
             // Landscape screen; nav bar goes to the right.
             navigationFrame.left = Math.min(cutoutSafeUnrestricted.right, navigationFrame.right)
-                    - getNavigationBarWidth(rotation, uiMode);
+                    - getNavigationBarWidth(rotation, uiMode, navBarPosition);
         } else if (navBarPosition == NAV_BAR_LEFT) {
             // Seascape screen; nav bar goes to the left.
             navigationFrame.right = Math.max(cutoutSafeUnrestricted.left, navigationFrame.left)
-                    + getNavigationBarWidth(rotation, uiMode);
+                    + getNavigationBarWidth(rotation, uiMode, navBarPosition);
         }
 
         // Compute the final frame.
         final WindowFrames windowFrames = mNavigationBar.getLayoutingWindowFrames();
         windowFrames.setFrames(navigationFrame /* parentFrame */,
                 navigationFrame /* displayFrame */);
-        mNavigationBar.computeFrameAndUpdateSourceFrame();
+        mNavigationBar.computeFrameAndUpdateSourceFrame(displayFrames);
         sTmpRect.set(windowFrames.mFrame);
         sTmpRect.intersect(displayFrames.mDisplayCutoutSafe);
         contentFrame.set(sTmpRect);
@@ -1553,6 +1656,16 @@
         return navBarPosition;
     }
 
+    private void simulateLayoutForContentFrame(DisplayFrames displayFrames, WindowState win,
+            Rect simulatedContentFrame) {
+        layoutWindowLw(win, null /* attached */, displayFrames);
+        final Rect contentFrame = sTmpRect;
+        contentFrame.set(win.getLayoutingWindowFrames().mFrame);
+        // Excluding the display cutout before set to the simulated content frame.
+        contentFrame.intersect(displayFrames.mDisplayCutoutSafe);
+        simulatedContentFrame.set(contentFrame);
+    }
+
     private boolean canReceiveInput(WindowState win) {
         boolean notFocusable =
                 (win.getAttrs().flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) != 0;
@@ -1574,12 +1687,12 @@
      * @param displayFrames The display frames.
      */
     public void layoutWindowLw(WindowState win, WindowState attached, DisplayFrames displayFrames) {
-        if (win == mNavigationBar) {
+        if (win == mNavigationBar && !INSETS_LAYOUT_GENERALIZATION) {
             mNavigationBarPosition = layoutNavigationBar(displayFrames,
                     mBarContentFrames.get(TYPE_NAVIGATION_BAR));
             return;
         }
-        if ((win == mStatusBar && !canReceiveInput(win))) {
+        if ((win == mStatusBar && !canReceiveInput(win)) && !INSETS_LAYOUT_GENERALIZATION) {
             layoutStatusBar(displayFrames, mBarContentFrames.get(TYPE_STATUS_BAR));
             return;
         }
@@ -1587,7 +1700,7 @@
             // Skip layout of the window when in transition to pip mode.
             return;
         }
-        final WindowManager.LayoutParams attrs = win.getAttrs();
+        final WindowManager.LayoutParams attrs = win.getLayoutingAttrs(displayFrames.mRotation);
 
         final int type = attrs.type;
         final int fl = attrs.flags;
@@ -1595,7 +1708,7 @@
         final int sim = attrs.softInputMode;
 
         displayFrames = win.getDisplayFrames(displayFrames);
-        final WindowFrames windowFrames = win.getWindowFrames();
+        final WindowFrames windowFrames = win.getLayoutingWindowFrames();
 
         sTmpLastParentFrame.set(windowFrames.mParentFrame);
         final Rect pf = windowFrames.mParentFrame;
@@ -1606,7 +1719,13 @@
         final boolean layoutInsetDecor = (fl & FLAG_LAYOUT_INSET_DECOR) == FLAG_LAYOUT_INSET_DECOR;
 
         final InsetsState state = win.getInsetsState();
-        computeWindowBounds(attrs, state, win.mToken.getBounds(), df);
+        if (windowFrames.mIsSimulatingDecorWindow && INSETS_LAYOUT_GENERALIZATION) {
+            // Override the bounds in window token has many side effects. Directly use the display
+            // frame set for the simulated layout for this case.
+            computeWindowBounds(attrs, state, df, df);
+        } else {
+            computeWindowBounds(attrs, state, win.getBounds(), df);
+        }
         if (attached == null) {
             pf.set(df);
             if ((pfl & PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME) != 0) {
@@ -1706,7 +1825,17 @@
             windowFrames.setContentChanged(true);
         }
 
-        win.computeFrameAndUpdateSourceFrame();
+        win.computeFrameAndUpdateSourceFrame(displayFrames);
+        if (INSETS_LAYOUT_GENERALIZATION && attrs.type == TYPE_STATUS_BAR) {
+            if (displayFrames.mDisplayCutoutSafe.top > displayFrames.mUnrestricted.top) {
+                // Make sure that the zone we're avoiding for the cutout is at least as tall as the
+                // status bar; otherwise fullscreen apps will end up cutting halfway into the status
+                // bar.
+                displayFrames.mDisplayCutoutSafe.top = Math.max(
+                        displayFrames.mDisplayCutoutSafe.top,
+                        windowFrames.mFrame.bottom);
+            }
+        }
     }
 
     WindowState getTopFullscreenOpaqueWindow() {
@@ -1722,12 +1851,13 @@
      */
     public void beginPostLayoutPolicyLw() {
         mTopFullscreenOpaqueWindowState = null;
-        mTopFullscreenOpaqueOrDimmingWindowState = null;
-        mTopDockedOpaqueWindowState = null;
-        mTopDockedOpaqueOrDimmingWindowState = null;
+        mNavBarColorWindowCandidate = null;
+        mNavBarBackgroundWindow = null;
+        mStatusBarColorWindows.clear();
+        mStatusBarBackgroundWindows.clear();
+        mStatusBarColorCheckedBounds.setEmpty();
+        mStatusBarBackgroundCheckedBounds.setEmpty();
         mForceStatusBar = false;
-        mForcingShowNavBar = false;
-        mForcingShowNavBarLayer = -1;
 
         mAllowLockscreenWhenOn = false;
         mShowingDream = false;
@@ -1746,20 +1876,22 @@
         final boolean affectsSystemUi = win.canAffectSystemUiFlags();
         if (DEBUG_LAYOUT) Slog.i(TAG, "Win " + win + ": affectsSystemUi=" + affectsSystemUi);
         applyKeyguardPolicy(win, imeTarget);
-        final int fl = attrs.flags;
-        if (mTopFullscreenOpaqueWindowState == null && affectsSystemUi
-                && attrs.type == TYPE_INPUT_METHOD) {
-            mForcingShowNavBar = true;
-            mForcingShowNavBarLayer = win.getSurfaceLayer();
+
+        // Check if the freeform window overlaps with the navigation bar area.
+        final boolean isOverlappingWithNavBar = isOverlappingWithNavBar(win, mNavigationBar);
+        if (isOverlappingWithNavBar && !mIsFreeformWindowOverlappingWithNavBar
+                && win.inFreeformWindowingMode()) {
+            mIsFreeformWindowOverlappingWithNavBar = true;
+        }
+
+        if (!affectsSystemUi) {
+            return;
         }
 
         boolean appWindow = attrs.type >= FIRST_APPLICATION_WINDOW
                 && attrs.type < FIRST_SYSTEM_WINDOW;
-        final int windowingMode = win.getWindowingMode();
-        final boolean inFullScreenOrSplitScreenSecondaryWindowingMode =
-                windowingMode == WINDOWING_MODE_FULLSCREEN
-                        || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
-        if (mTopFullscreenOpaqueWindowState == null && affectsSystemUi) {
+        if (mTopFullscreenOpaqueWindowState == null) {
+            final int fl = attrs.flags;
             if ((fl & FLAG_FORCE_NOT_FULLSCREEN) != 0) {
                 mForceStatusBar = true;
             }
@@ -1772,68 +1904,60 @@
                 }
             }
 
-            // For app windows that are not attached, we decide if all windows in the app they
-            // represent should be hidden or if we should hide the lockscreen. For attached app
-            // windows we defer the decision to the window it is attached to.
-            if (appWindow && attached == null) {
-                if (attrs.isFullscreen() && inFullScreenOrSplitScreenSecondaryWindowingMode) {
-                    if (DEBUG_LAYOUT) Slog.v(TAG, "Fullscreen window: " + win);
-                    mTopFullscreenOpaqueWindowState = win;
-                    if (mTopFullscreenOpaqueOrDimmingWindowState == null) {
-                        mTopFullscreenOpaqueOrDimmingWindowState = win;
-                    }
-                    if ((fl & FLAG_ALLOW_LOCK_WHILE_SCREEN_ON) != 0) {
-                        mAllowLockscreenWhenOn = true;
-                    }
-                }
+            if (appWindow && attached == null && attrs.isFullscreen()
+                    && (fl & FLAG_ALLOW_LOCK_WHILE_SCREEN_ON) != 0) {
+                mAllowLockscreenWhenOn = true;
             }
         }
 
-        // Voice interaction overrides both top fullscreen and top docked.
-        if (affectsSystemUi && attrs.type == TYPE_VOICE_INTERACTION && attrs.isFullscreen()) {
+        // Check the windows that overlap with system bars to determine system bars' appearance.
+        if ((appWindow && attached == null && attrs.isFullscreen())
+                || attrs.type == TYPE_VOICE_INTERACTION) {
+            // Record the top-fullscreen-app-window which will be used to determine system UI
+            // controlling window.
             if (mTopFullscreenOpaqueWindowState == null) {
                 mTopFullscreenOpaqueWindowState = win;
-                if (mTopFullscreenOpaqueOrDimmingWindowState == null) {
-                    mTopFullscreenOpaqueOrDimmingWindowState = win;
+            }
+
+            // Cache app windows that is overlapping with the status bar to determine appearance
+            // of status bar.
+            if (mStatusBar != null
+                    && sTmpRect.setIntersect(win.getFrame(), mStatusBar.getFrame())
+                    && !mStatusBarBackgroundCheckedBounds.contains(sTmpRect)) {
+                mStatusBarBackgroundWindows.add(win);
+                mStatusBarBackgroundCheckedBounds.union(sTmpRect);
+                if (!mStatusBarColorCheckedBounds.contains(sTmpRect)) {
+                    mStatusBarColorWindows.add(win);
+                    mStatusBarColorCheckedBounds.union(sTmpRect);
                 }
             }
-            if (mTopDockedOpaqueWindowState == null) {
-                mTopDockedOpaqueWindowState = win;
-                if (mTopDockedOpaqueOrDimmingWindowState == null) {
-                    mTopDockedOpaqueOrDimmingWindowState = win;
+
+            // Cache app window that overlaps with the navigation bar area to determine opacity
+            // and appearance of the navigation bar. We only need to cache one window because
+            // there should be only one overlapping window if it's not in gesture navigation
+            // mode; if it's in gesture navigation mode, the navigation bar will be
+            // NAV_BAR_FORCE_TRANSPARENT and its appearance won't be decided by overlapping
+            // windows.
+            if (isOverlappingWithNavBar) {
+                if (mNavBarColorWindowCandidate == null) {
+                    mNavBarColorWindowCandidate = win;
+                }
+                if (mNavBarBackgroundWindow == null) {
+                    mNavBarBackgroundWindow = win;
                 }
             }
-        }
-
-        // Keep track of the window if it's dimming but not necessarily fullscreen.
-        if (mTopFullscreenOpaqueOrDimmingWindowState == null && affectsSystemUi
-                && win.isDimming() && inFullScreenOrSplitScreenSecondaryWindowingMode) {
-            mTopFullscreenOpaqueOrDimmingWindowState = win;
-        }
-
-        // We need to keep track of the top "fullscreen" opaque window for the docked root task
-        // separately, because both the "real fullscreen" opaque window and the one for the docked
-        // root task can control View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.
-        if (mTopDockedOpaqueWindowState == null && affectsSystemUi && appWindow && attached == null
-                && attrs.isFullscreen() && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
-            mTopDockedOpaqueWindowState = win;
-            if (mTopDockedOpaqueOrDimmingWindowState == null) {
-                mTopDockedOpaqueOrDimmingWindowState = win;
+        } else if (win.isDimming()) {
+            // For dimming window whose host bounds is overlapping with system bars, it can be
+            // used to determine colors but not opacity of system bars.
+            if (mStatusBar != null
+                    && sTmpRect.setIntersect(win.getBounds(), mStatusBar.getFrame())
+                    && !mStatusBarColorCheckedBounds.contains(sTmpRect)) {
+                mStatusBarColorWindows.add(win);
+                mStatusBarColorCheckedBounds.union(sTmpRect);
             }
-        }
-
-        // Check if the freeform window overlaps with the navigation bar area.
-        final WindowState navBarWin = hasNavigationBar() ? mNavigationBar : null;
-        if (!mIsFreeformWindowOverlappingWithNavBar && win.inFreeformWindowingMode()
-                && isOverlappingWithNavBar(win, navBarWin)) {
-            mIsFreeformWindowOverlappingWithNavBar = true;
-        }
-
-        // Also keep track of any windows that are dimming but not necessarily fullscreen in the
-        // docked root task.
-        if (mTopDockedOpaqueOrDimmingWindowState == null && affectsSystemUi && win.isDimming()
-                && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
-            mTopDockedOpaqueOrDimmingWindowState = win;
+            if (isOverlappingWithNavBar && mNavBarColorWindowCandidate == null) {
+                mNavBarColorWindowCandidate = win;
+            }
         }
     }
 
@@ -1897,11 +2021,7 @@
             mTopIsFullscreen = topIsFullscreen;
         }
 
-        if (updateSystemUiVisibilityLw()) {
-            // If the navigation bar has been hidden or shown, we need to do another
-            // layout pass to update that window.
-            changes |= FINISH_LAYOUT_REDO_LAYOUT;
-        }
+        updateSystemBarAttributes();
 
         if (mShowingDream != mLastShowingDream) {
             mLastShowingDream = mShowingDream;
@@ -2138,18 +2258,47 @@
         return mUiContext;
     }
 
-    private int getNavigationBarWidth(int rotation, int uiMode) {
-        if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) {
-            return mNavigationBarWidthForRotationInCarMode[rotation];
+    private int getNavigationBarWidth(int rotation, int uiMode, int position) {
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            if (mNavigationBar == null) {
+                return 0;
+            }
+            LayoutParams lp = mNavigationBar.mAttrs;
+            if (lp.paramsForRotation != null
+                    && lp.paramsForRotation.length == 4
+                    && lp.paramsForRotation[rotation] != null) {
+                lp = lp.paramsForRotation[rotation];
+            }
+            if (position == NAV_BAR_LEFT) {
+                if (lp.width > lp.providedInternalInsets.right) {
+                    return lp.width - lp.providedInternalInsets.right;
+                } else {
+                    return 0;
+                }
+            } else if (position == NAV_BAR_RIGHT) {
+                if (lp.width > lp.providedInternalInsets.left) {
+                    return lp.width - lp.providedInternalInsets.left;
+                } else {
+                    return 0;
+                }
+            }
+            return lp.width;
         } else {
-            return mNavigationBarWidthForRotationDefault[rotation];
+            if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) {
+                return mNavigationBarWidthForRotationInCarMode[rotation];
+            } else {
+                return mNavigationBarWidthForRotationDefault[rotation];
+            }
         }
     }
 
     void notifyDisplayReady() {
         mHandler.post(() -> {
             final int displayId = getDisplayId();
-            getStatusBarManagerInternal().onDisplayReady(displayId);
+            StatusBarManagerInternal statusBar = getStatusBarManagerInternal();
+            if (statusBar != null) {
+                statusBar.onDisplayReady(displayId);
+            }
             final WallpaperManagerInternal wpMgr = LocalServices
                     .getService(WallpaperManagerInternal.class);
             if (wpMgr != null) {
@@ -2169,7 +2318,7 @@
         if (hasNavigationBar()) {
             final int navBarPosition = navigationBarPosition(fullWidth, fullHeight, rotation);
             if (navBarPosition == NAV_BAR_LEFT || navBarPosition == NAV_BAR_RIGHT) {
-                width -= getNavigationBarWidth(rotation, uiMode);
+                width -= getNavigationBarWidth(rotation, uiMode, navBarPosition);
             }
         }
         if (displayCutout != null) {
@@ -2179,10 +2328,21 @@
     }
 
     private int getNavigationBarHeight(int rotation, int uiMode) {
-        if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) {
-            return mNavigationBarHeightForRotationInCarMode[rotation];
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            if (mNavigationBar == null) {
+                return 0;
+            }
+            LayoutParams lp = mNavigationBar.getLayoutingAttrs(rotation);
+            if (lp.height < lp.providedInternalInsets.top) {
+                return 0;
+            }
+            return lp.height - lp.providedInternalInsets.top;
         } else {
-            return mNavigationBarHeightForRotationDefault[rotation];
+            if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) {
+                return mNavigationBarHeightForRotationInCarMode[rotation];
+            } else {
+                return mNavigationBarHeightForRotationDefault[rotation];
+            }
         }
     }
 
@@ -2199,10 +2359,17 @@
      * @return navigation bar frame height
      */
     private int getNavigationBarFrameHeight(int rotation, int uiMode) {
-        if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) {
-            return mNavigationBarHeightForRotationInCarMode[rotation];
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            if (mNavigationBar == null) {
+                return 0;
+            }
+            return mNavigationBar.mAttrs.height;
         } else {
-            return mNavigationBarFrameHeightForRotationDefault[rotation];
+            if (ALTERNATE_CAR_MODE_NAV_SIZE && (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR) {
+                return mNavigationBarHeightForRotationInCarMode[rotation];
+            } else {
+                return mNavigationBarFrameHeightForRotationDefault[rotation];
+            }
         }
     }
 
@@ -2267,7 +2434,7 @@
      */
     float getWindowCornerRadius() {
         return mDisplayContent.getDisplay().getType() == TYPE_INTERNAL
-                ? ScreenDecorationsUtils.getWindowCornerRadius(mContext.getResources()) : 0f;
+                ? ScreenDecorationsUtils.getWindowCornerRadius(mContext) : 0f;
     }
 
     boolean isShowingDreamLw() {
@@ -2323,9 +2490,9 @@
             if (position == NAV_BAR_BOTTOM) {
                 outInsets.bottom = getNavigationBarHeight(displayRotation, uiMode);
             } else if (position == NAV_BAR_RIGHT) {
-                outInsets.right = getNavigationBarWidth(displayRotation, uiMode);
+                outInsets.right = getNavigationBarWidth(displayRotation, uiMode, position);
             } else if (position == NAV_BAR_LEFT) {
-                outInsets.left = getNavigationBarWidth(displayRotation, uiMode);
+                outInsets.left = getNavigationBarWidth(displayRotation, uiMode, position);
             }
         }
 
@@ -2351,6 +2518,17 @@
 
     @NavigationBarPosition
     int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
+        if (INSETS_LAYOUT_GENERALIZATION && mNavigationBar != null) {
+            final int gravity = mNavigationBar.getLayoutingAttrs(displayRotation).gravity;
+            switch (gravity) {
+                case Gravity.LEFT:
+                    return NAV_BAR_LEFT;
+                case Gravity.RIGHT:
+                    return NAV_BAR_RIGHT;
+                default:
+                    return NAV_BAR_BOTTOM;
+            }
+        }
         if (navigationBarCanMove() && displayWidth > displayHeight) {
             if (displayRotation == Surface.ROTATION_270) {
                 return NAV_BAR_LEFT;
@@ -2385,18 +2563,13 @@
     /**
      * A new window has been focused.
      */
-    public int focusChangedLw(WindowState lastFocus, WindowState newFocus) {
+    public void focusChangedLw(WindowState lastFocus, WindowState newFocus) {
         mFocusedWindow = newFocus;
         mLastFocusedWindow = lastFocus;
         if (mDisplayContent.isDefaultDisplay) {
             mService.mPolicy.onDefaultDisplayFocusChangedLw(newFocus);
         }
-        if (updateSystemUiVisibilityLw()) {
-            // If the navigation bar has been hidden or shown, we need to do another
-            // layout pass to update that window.
-            return FINISH_LAYOUT_REDO_LAYOUT;
-        }
-        return 0;
+        updateSystemBarAttributes();
     }
 
     private void requestTransientBars(WindowState swipeTarget) {
@@ -2472,21 +2645,18 @@
         return mDisplayContent.getInsetsPolicy();
     }
 
-    void resetSystemUiVisibilityLw() {
+    void resetSystemBarAttributes() {
         mLastDisableFlags = 0;
-        updateSystemUiVisibilityLw();
+        updateSystemBarAttributes();
     }
 
-    /**
-     * @return {@code true} if the update may affect the layout.
-     */
-    boolean updateSystemUiVisibilityLw() {
+    void updateSystemBarAttributes() {
         // If there is no window focused, there will be nobody to handle the events
         // anyway, so just hang on in whatever state we're in until things settle down.
         WindowState winCandidate = mFocusedWindow != null ? mFocusedWindow
                 : mTopFullscreenOpaqueWindowState;
         if (winCandidate == null) {
-            return false;
+            return;
         }
 
         // The immersive mode confirmation should never affect the system bar visibility, otherwise
@@ -2502,86 +2672,63 @@
                     : lastFocusCanReceiveKeys ? mLastFocusedWindow
                             : mTopFullscreenOpaqueWindowState;
             if (winCandidate == null) {
-                return false;
+                return;
             }
         }
         final WindowState win = winCandidate;
         mSystemUiControllingWindow = win;
 
-        mDisplayContent.getInsetsPolicy().updateBarControlTarget(win);
-
-        final boolean inSplitScreen =
-                mService.mRoot.getDefaultTaskDisplayArea().isSplitScreenModeActivated();
-        if (inSplitScreen) {
-            mService.getRootTaskBounds(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD,
-                    mDockedRootTaskBounds);
-        } else {
-            mDockedRootTaskBounds.setEmpty();
-        }
-        mService.getRootTaskBounds(inSplitScreen ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                        : WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_UNDEFINED, mNonDockedRootTaskBounds);
-        final int fullscreenAppearance = getStatusBarAppearance(mTopFullscreenOpaqueWindowState,
-                mTopFullscreenOpaqueOrDimmingWindowState);
-        final int dockedAppearance = getStatusBarAppearance(mTopDockedOpaqueWindowState,
-                mTopDockedOpaqueOrDimmingWindowState);
+        final int displayId = getDisplayId();
         final int disableFlags = win.getDisableFlags();
         final int opaqueAppearance = updateSystemBarsLw(win, disableFlags);
-        final WindowState navColorWin = chooseNavigationColorWindowLw(
-                mTopFullscreenOpaqueWindowState, mTopFullscreenOpaqueOrDimmingWindowState,
+        final WindowState navColorWin = chooseNavigationColorWindowLw(mNavBarColorWindowCandidate,
                 mDisplayContent.mInputMethodWindow, mNavigationBarPosition);
         final boolean isNavbarColorManagedByIme =
                 navColorWin != null && navColorWin == mDisplayContent.mInputMethodWindow;
-        final int appearance = updateLightNavigationBarLw(
-                win.mAttrs.insetsFlags.appearance, mTopFullscreenOpaqueWindowState,
-                mTopFullscreenOpaqueOrDimmingWindowState,
-                mDisplayContent.mInputMethodWindow, navColorWin) | opaqueAppearance;
+        final int appearance = updateLightNavigationBarLw(win.mAttrs.insetsFlags.appearance,
+                navColorWin) | opaqueAppearance;
         final int behavior = win.mAttrs.insetsFlags.behavior;
+        final String focusedApp = win.mAttrs.packageName;
         final boolean isFullscreen = !win.getRequestedVisibility(ITYPE_STATUS_BAR)
                 || !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR);
-        if (mLastDisableFlags == disableFlags
-                && mLastAppearance == appearance
-                && mLastFullscreenAppearance == fullscreenAppearance
-                && mLastDockedAppearance == dockedAppearance
+        final AppearanceRegion[] appearanceRegions =
+                new AppearanceRegion[mStatusBarColorWindows.size()];
+        for (int i = mStatusBarColorWindows.size() - 1; i >= 0; i--) {
+            final WindowState windowState = mStatusBarColorWindows.get(i);
+            appearanceRegions[i] = new AppearanceRegion(
+                    getStatusBarAppearance(windowState, windowState),
+                    new Rect(windowState.getFrame()));
+        }
+        if (mLastDisableFlags != disableFlags) {
+            mLastDisableFlags = disableFlags;
+            final String cause = win.toString();
+            callStatusBarSafely(statusBar -> statusBar.setDisableFlags(displayId, disableFlags,
+                    cause));
+        }
+        if (mLastAppearance == appearance
                 && mLastBehavior == behavior
+                && mRequestedVisibilities.equals(win.getRequestedVisibilities())
+                && Objects.equals(mFocusedApp, focusedApp)
                 && mLastFocusIsFullscreen == isFullscreen
-                && mLastNonDockedRootTaskBounds.equals(mNonDockedRootTaskBounds)
-                && mLastDockedRootTaskBounds.equals(mDockedRootTaskBounds)) {
-            return false;
+                && Arrays.equals(mLastStatusBarAppearanceRegions, appearanceRegions)) {
+            return;
         }
         if (mDisplayContent.isDefaultDisplay && mLastFocusIsFullscreen != isFullscreen
                 && ((mLastAppearance ^ appearance) & APPEARANCE_LOW_PROFILE_BARS) != 0) {
             mService.mInputManager.setSystemUiLightsOut(
                     isFullscreen || (appearance & APPEARANCE_LOW_PROFILE_BARS) != 0);
         }
-        mLastDisableFlags = disableFlags;
+        final InsetsVisibilities requestedVisibilities =
+                new InsetsVisibilities(win.getRequestedVisibilities());
         mLastAppearance = appearance;
-        mLastFullscreenAppearance = fullscreenAppearance;
-        mLastDockedAppearance = dockedAppearance;
         mLastBehavior = behavior;
+        mRequestedVisibilities = requestedVisibilities;
+        mFocusedApp = focusedApp;
         mLastFocusIsFullscreen = isFullscreen;
-        mLastNonDockedRootTaskBounds.set(mNonDockedRootTaskBounds);
-        mLastDockedRootTaskBounds.set(mDockedRootTaskBounds);
-        final Rect fullscreenRootTaskBounds = new Rect(mNonDockedRootTaskBounds);
-        final Rect dockedRootTaskBounds = new Rect(mDockedRootTaskBounds);
-        final AppearanceRegion[] appearanceRegions = inSplitScreen
-                ? new AppearanceRegion[]{
-                        new AppearanceRegion(fullscreenAppearance, fullscreenRootTaskBounds),
-                        new AppearanceRegion(dockedAppearance, dockedRootTaskBounds)}
-                : new AppearanceRegion[]{
-                        new AppearanceRegion(fullscreenAppearance, fullscreenRootTaskBounds)};
-        String cause = win.toString();
-        mHandler.post(() -> {
-            StatusBarManagerInternal statusBar = getStatusBarManagerInternal();
-            if (statusBar != null) {
-                final int displayId = getDisplayId();
-                statusBar.setDisableFlags(displayId, disableFlags, cause);
-                statusBar.onSystemBarAttributesChanged(displayId, appearance, appearanceRegions,
-                        isNavbarColorManagedByIme, behavior, isFullscreen);
-
-            }
-        });
-        return true;
+        mLastStatusBarAppearanceRegions = appearanceRegions;
+        callStatusBarSafely(statusBar -> statusBar.onSystemBarAttributesChanged(displayId,
+                appearance, appearanceRegions, isNavbarColorManagedByIme, behavior,
+                requestedVisibilities, focusedApp));
     }
 
     private int getStatusBarAppearance(WindowState opaque, WindowState opaqueOrDimming) {
@@ -2592,10 +2739,18 @@
                 : 0;
     }
 
+    private void callStatusBarSafely(Consumer<StatusBarManagerInternal> consumer) {
+        mHandler.post(() -> {
+            StatusBarManagerInternal statusBar = getStatusBarManagerInternal();
+            if (statusBar != null) {
+                consumer.accept(statusBar);
+            }
+        });
+    }
+
     @VisibleForTesting
     @Nullable
-    static WindowState chooseNavigationColorWindowLw(WindowState opaque,
-            WindowState opaqueOrDimming, WindowState imeWindow,
+    static WindowState chooseNavigationColorWindowLw(WindowState candidate, WindowState imeWindow,
             @NavigationBarPosition int navBarPosition) {
         // If the IME window is visible and FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is set, then IME
         // window can be navigation color window.
@@ -2604,71 +2759,59 @@
                 && navBarPosition == NAV_BAR_BOTTOM
                 && (imeWindow.mAttrs.flags
                         & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
-
-        if (opaque != null && opaqueOrDimming == opaque) {
-            // If the top fullscreen-or-dimming window is also the top fullscreen, respect it
-            // unless IME window is also eligible, since currently the IME window is always show
-            // above the opaque fullscreen app window, regardless of the IME target window.
-            // TODO(b/31559891): Maybe we need to revisit this condition once b/31559891 is fixed.
-            return imeWindowCanNavColorWindow ? imeWindow : opaque;
-        }
-
-        if (opaqueOrDimming == null || !opaqueOrDimming.isDimming()) {
-            // No dimming window is involved. Determine the result only with the IME window.
-            return imeWindowCanNavColorWindow ? imeWindow : null;
-        }
-
         if (!imeWindowCanNavColorWindow) {
-            // No IME window is involved. Determine the result only with opaqueOrDimming.
-            return opaqueOrDimming;
+            // No IME window is involved. Determine the result only with candidate window.
+            return candidate;
         }
 
-        // The IME window and the dimming window are competing.  Check if the dimming window can be
-        // IME target or not.
-        if (LayoutParams.mayUseInputMethod(opaqueOrDimming.mAttrs.flags)) {
-            // The IME window is above the dimming window.
-            return imeWindow;
-        } else {
-            // The dimming window is above the IME window.
-            return opaqueOrDimming;
+        if (candidate != null && candidate.isDimming()) {
+            // The IME window and the dimming window are competing. Check if the dimming window can
+            // be IME target or not.
+            if (LayoutParams.mayUseInputMethod(candidate.mAttrs.flags)) {
+                // The IME window is above the dimming window.
+                return imeWindow;
+            } else {
+                // The dimming window is above the IME window.
+                return candidate;
+            }
         }
+
+        return imeWindow;
     }
 
     @VisibleForTesting
-    int updateLightNavigationBarLw(int appearance, WindowState opaque,
-            WindowState opaqueOrDimming, WindowState imeWindow, WindowState navColorWin) {
-
-        if (navColorWin != null) {
-            if (navColorWin == imeWindow || navColorWin == opaque) {
-                // Respect the light flag.
-                appearance &= ~APPEARANCE_LIGHT_NAVIGATION_BARS;
-                appearance |= navColorWin.mAttrs.insetsFlags.appearance
-                        & APPEARANCE_LIGHT_NAVIGATION_BARS;
-            } else if (navColorWin == opaqueOrDimming && navColorWin.isDimming()) {
-                // Clear the light flag for dimming window.
-                appearance &= ~APPEARANCE_LIGHT_NAVIGATION_BARS;
-            }
-        }
-        if (!isLightBarAllowed(navColorWin, ITYPE_NAVIGATION_BAR)) {
+    int updateLightNavigationBarLw(int appearance, WindowState navColorWin) {
+        if (navColorWin == null || navColorWin.isDimming()
+                || !isLightBarAllowed(navColorWin, ITYPE_NAVIGATION_BAR)) {
+            // Clear the light flag while not allowed.
             appearance &= ~APPEARANCE_LIGHT_NAVIGATION_BARS;
+            return appearance;
         }
+
+        // Respect the light flag of navigation color window.
+        appearance &= ~APPEARANCE_LIGHT_NAVIGATION_BARS;
+        appearance |= navColorWin.mAttrs.insetsFlags.appearance
+                & APPEARANCE_LIGHT_NAVIGATION_BARS;
         return appearance;
     }
 
     private int updateSystemBarsLw(WindowState win, int disableFlags) {
-        final boolean dockedRootTaskVisible = mDisplayContent.getDefaultTaskDisplayArea()
-                .isRootTaskVisible(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        final boolean resizing = mDisplayContent.getDockedDividerController().isResizing();
+        final TaskDisplayArea defaultTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea();
+        final boolean multiWindowTaskVisible =
+                defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY)
+                        || defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_MULTI_WINDOW);
+        final boolean freeformRootTaskVisible =
+                defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_FREEFORM);
 
-        // We need to force system bars when the docked root task is visible, when the freeform
-        // root task is focused but also when we are resizing for the transitions when docked
-        // root task visibility changes.
-        mForceShowSystemBars = dockedRootTaskVisible || win.inFreeformWindowingMode() || resizing;
+        // We need to force showing system bars when the multi-window or freeform root task is
+        // visible.
+        mForceShowSystemBars = multiWindowTaskVisible || freeformRootTaskVisible;
+        mDisplayContent.getInsetsPolicy().updateBarControlTarget(win);
 
         int appearance = APPEARANCE_OPAQUE_NAVIGATION_BARS | APPEARANCE_OPAQUE_STATUS_BARS;
-
         appearance = configureStatusBarOpacity(appearance);
-        appearance = configureNavBarOpacity(appearance, dockedRootTaskVisible, resizing);
+        appearance = configureNavBarOpacity(appearance, multiWindowTaskVisible,
+                freeformRootTaskVisible);
 
         final boolean requestHideNavBar = !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR);
         final long now = SystemClock.uptimeMillis();
@@ -2714,7 +2857,24 @@
 
     private Rect getBarContentFrameForWindow(WindowState win, int windowType) {
         final Rect rotatedBarFrame = win.mToken.getFixedRotationBarContentFrame(windowType);
-        return rotatedBarFrame != null ? rotatedBarFrame : mBarContentFrames.get(windowType);
+        if (rotatedBarFrame != null) {
+            return rotatedBarFrame;
+        }
+        if (!INSETS_LAYOUT_GENERALIZATION) {
+            return mBarContentFrames.get(windowType);
+        }
+        // We only need a window specific information for the fixed rotation, use raw insets state
+        // for all other cases.
+        InsetsState insetsState = mDisplayContent.getInsetsStateController().getRawInsetsState();
+        final Rect tmpRect = new Rect();
+        if (windowType == TYPE_NAVIGATION_BAR) {
+            tmpRect.set(insetsState.getSource(InsetsState.ITYPE_NAVIGATION_BAR).getFrame());
+        }
+        if (windowType == TYPE_STATUS_BAR) {
+            tmpRect.set(insetsState.getSource(InsetsState.ITYPE_STATUS_BAR).getFrame());
+        }
+        tmpRect.intersect(mDisplayContent.mDisplayFrames.mDisplayCutoutSafe);
+        return tmpRect;
     }
 
     /**
@@ -2749,17 +2909,19 @@
 
     /** @return the current visibility flags with the status bar opacity related flags toggled. */
     private int configureStatusBarOpacity(int appearance) {
-        final boolean fullscreenDrawsBackground =
-                drawsBarBackground(mTopFullscreenOpaqueWindowState);
-        final boolean dockedDrawsBackground =
-                drawsBarBackground(mTopDockedOpaqueWindowState);
+        boolean drawBackground = true;
+        boolean isFullyTransparentAllowed = true;
+        for (int i = mStatusBarBackgroundWindows.size() - 1; i >= 0; i--) {
+            final WindowState window = mStatusBarBackgroundWindows.get(i);
+            drawBackground &= drawsBarBackground(window);
+            isFullyTransparentAllowed &= isFullyTransparentAllowed(window, TYPE_STATUS_BAR);
+        }
 
-        if (fullscreenDrawsBackground && dockedDrawsBackground) {
+        if (drawBackground) {
             appearance &= ~APPEARANCE_OPAQUE_STATUS_BARS;
         }
 
-        if (!isFullyTransparentAllowed(mTopFullscreenOpaqueWindowState, TYPE_STATUS_BAR)
-                || !isFullyTransparentAllowed(mTopDockedOpaqueWindowState, TYPE_STATUS_BAR)) {
+        if (!isFullyTransparentAllowed) {
             appearance |= APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS;
         }
 
@@ -2770,53 +2932,35 @@
      * @return the current visibility flags with the nav-bar opacity related flags toggled based
      *         on the nav bar opacity rules chosen by {@link #mNavBarOpacityMode}.
      */
-    private int configureNavBarOpacity(int appearance, boolean dockedRootTaskVisible,
-            boolean isDockedDividerResizing) {
-        final boolean freeformRootTaskVisible = mDisplayContent.getDefaultTaskDisplayArea()
-                .isRootTaskVisible(WINDOWING_MODE_FREEFORM);
-        final boolean fullscreenDrawsBackground =
-                drawsBarBackground(mTopFullscreenOpaqueWindowState);
-        final boolean dockedDrawsBackground =
-                drawsBarBackground(mTopDockedOpaqueWindowState);
+    private int configureNavBarOpacity(int appearance, boolean multiWindowTaskVisible,
+            boolean freeformRootTaskVisible) {
+        final boolean drawBackground = drawsBarBackground(mNavBarBackgroundWindow);
 
         if (mNavBarOpacityMode == NAV_BAR_FORCE_TRANSPARENT) {
-            if (fullscreenDrawsBackground && dockedDrawsBackground) {
+            if (drawBackground) {
                 appearance = clearNavBarOpaqueFlag(appearance);
-            } else if (dockedRootTaskVisible) {
-                appearance = setNavBarOpaqueFlag(appearance);
             }
         } else if (mNavBarOpacityMode == NAV_BAR_OPAQUE_WHEN_FREEFORM_OR_DOCKED) {
-            if (dockedRootTaskVisible || freeformRootTaskVisible || isDockedDividerResizing) {
+            if (multiWindowTaskVisible || freeformRootTaskVisible) {
                 if (mIsFreeformWindowOverlappingWithNavBar) {
                     appearance = clearNavBarOpaqueFlag(appearance);
-                } else {
-                    appearance = setNavBarOpaqueFlag(appearance);
                 }
-            } else if (fullscreenDrawsBackground) {
+            } else if (drawBackground) {
                 appearance = clearNavBarOpaqueFlag(appearance);
             }
         } else if (mNavBarOpacityMode == NAV_BAR_TRANSLUCENT_WHEN_FREEFORM_OPAQUE_OTHERWISE) {
-            if (isDockedDividerResizing) {
-                appearance = setNavBarOpaqueFlag(appearance);
-            } else if (freeformRootTaskVisible) {
+            if (freeformRootTaskVisible) {
                 appearance = clearNavBarOpaqueFlag(appearance);
-            } else {
-                appearance = setNavBarOpaqueFlag(appearance);
             }
         }
 
-        if (!isFullyTransparentAllowed(mTopFullscreenOpaqueWindowState, TYPE_NAVIGATION_BAR)
-                || !isFullyTransparentAllowed(mTopDockedOpaqueWindowState, TYPE_NAVIGATION_BAR)) {
+        if (!isFullyTransparentAllowed(mNavBarBackgroundWindow, TYPE_NAVIGATION_BAR)) {
             appearance |= APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS;
         }
 
         return appearance;
     }
 
-    private int setNavBarOpaqueFlag(int appearance) {
-        return appearance | APPEARANCE_OPAQUE_NAVIGATION_BARS;
-    }
-
     private int clearNavBarOpaqueFlag(int appearance) {
         return appearance & ~APPEARANCE_OPAQUE_NAVIGATION_BARS;
     }
@@ -2856,7 +3000,7 @@
                     return;
                 }
                 mPendingPanicGestureUptime = SystemClock.uptimeMillis();
-                updateSystemUiVisibilityLw();
+                updateSystemBarAttributes();
             }
         }
     };
@@ -2917,6 +3061,7 @@
     void dump(String prefix, PrintWriter pw) {
         pw.print(prefix); pw.println("DisplayPolicy");
         prefix += "  ";
+        final String prefixInner = prefix + "  ";
         pw.print(prefix);
         pw.print("mCarDockEnablesAccelerometer="); pw.print(mCarDockEnablesAccelerometer);
         pw.print(" mDeskDockEnablesAccelerometer=");
@@ -2984,14 +3129,27 @@
             pw.print(prefix); pw.print("mTopFullscreenOpaqueWindowState=");
             pw.println(mTopFullscreenOpaqueWindowState);
         }
-        if (mTopFullscreenOpaqueOrDimmingWindowState != null) {
-            pw.print(prefix); pw.print("mTopFullscreenOpaqueOrDimmingWindowState=");
-            pw.println(mTopFullscreenOpaqueOrDimmingWindowState);
+        if (mNavBarColorWindowCandidate != null) {
+            pw.print(prefix); pw.print("mNavBarColorWindowCandidate=");
+            pw.println(mNavBarColorWindowCandidate);
         }
-        if (mForcingShowNavBar) {
-            pw.print(prefix); pw.print("mForcingShowNavBar="); pw.println(mForcingShowNavBar);
-            pw.print(prefix); pw.print("mForcingShowNavBarLayer=");
-            pw.println(mForcingShowNavBarLayer);
+        if (mNavBarBackgroundWindow != null) {
+            pw.print(prefix); pw.print("mNavBarBackgroundWindow=");
+            pw.println(mNavBarBackgroundWindow);
+        }
+        if (!mStatusBarColorWindows.isEmpty()) {
+            pw.print(prefix); pw.println("mStatusBarColorWindows=");
+            for (int i = mStatusBarColorWindows.size() - 1; i >= 0; i--) {
+                final WindowState win = mStatusBarColorWindows.get(i);
+                pw.print(prefixInner); pw.println(win);
+            }
+        }
+        if (!mStatusBarBackgroundWindows.isEmpty()) {
+            pw.print(prefix); pw.println("mStatusBarBackgroundWindows=");
+            for (int i = mStatusBarBackgroundWindows.size() - 1; i >= 0; i--) {
+                final WindowState win = mStatusBarBackgroundWindows.get(i);
+                pw.print(prefixInner);  pw.println(win);
+            }
         }
         pw.print(prefix); pw.print("mTopIsFullscreen="); pw.println(mTopIsFullscreen);
         pw.print(prefix); pw.print("mForceStatusBar="); pw.print(mForceStatusBar);
@@ -3030,7 +3188,7 @@
                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
         lp.setFitInsetsTypes(0);
-        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
         if (ActivityManager.isHighEndGfx()) {
             lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
             lp.privateFlags |=
@@ -3074,13 +3232,17 @@
     }
 
     @VisibleForTesting
-    static boolean isOverlappingWithNavBar(WindowState targetWindow, WindowState navBarWindow) {
+    static boolean isOverlappingWithNavBar(@NonNull WindowState targetWindow,
+            WindowState navBarWindow) {
         if (navBarWindow == null || !navBarWindow.isVisible()
                 || targetWindow.mActivityRecord == null || !targetWindow.isVisible()) {
             return false;
         }
 
-        return Rect.intersects(targetWindow.getFrame(), navBarWindow.getFrame());
+        // When the window is dimming means it's requesting dim layer to its host container, so
+        // checking whether it's overlapping with navigation bar by its container's bounds.
+        return Rect.intersects(targetWindow.isDimming()
+                ? targetWindow.getBounds() : targetWindow.getFrame(), navBarWindow.getFrame());
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index c9db14d..34e8149 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -22,7 +22,6 @@
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
-import static android.view.WindowManager.TRANSIT_CHANGE;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.LID_OPEN;
@@ -414,7 +413,7 @@
      */
     boolean updateRotationUnchecked(boolean forceUpdate) {
         final boolean useShellTransitions =
-                mService.mAtmService.getTransitionController().getTransitionPlayer() != null;
+                mDisplayContent.mTransitionController.isShellTransitionsEnabled();
 
         final int displayId = mDisplayContent.getDisplayId();
         if (!forceUpdate && !useShellTransitions) {
@@ -443,7 +442,9 @@
                 return false;
             }
 
-            if (mDisplayContent.mFixedRotationTransitionListener
+            final RecentsAnimationController recentsAnimController =
+                    mService.getRecentsAnimationController();
+            if (recentsAnimController != null && mDisplayContent.mFixedRotationTransitionListener
                     .isTopFixedOrientationRecentsAnimating()
                     // If screen is off or the device is going to sleep, then still allow to update.
                     && mService.mPolicy.okToAnimate(false /* ignoreScreenOn */)) {
@@ -451,6 +452,7 @@
                 // In order to ignore its requested orientation to avoid a sensor led rotation (e.g
                 // user rotating the device while the recents animation is running), we ignore
                 // rotation update while the animation is running.
+                recentsAnimController.setCheckRotationAfterCleanup();
                 return false;
             }
         }
@@ -492,12 +494,6 @@
             recentsAnimationController.cancelAnimationForDisplayChange();
         }
 
-        final Transition t = (useShellTransitions
-                && !mService.mAtmService.getTransitionController().isCollecting())
-                ? mService.mAtmService.getTransitionController().createTransition(TRANSIT_CHANGE)
-                : null;
-        mService.mAtmService.getTransitionController().collect(mDisplayContent);
-
         ProtoLog.v(WM_DEBUG_ORIENTATION,
                 "Display id=%d rotation changed to %d from %d, lastOrientation=%d",
                         displayId, rotation, oldRotation, lastOrientation);
@@ -511,11 +507,10 @@
         mDisplayContent.setLayoutNeeded();
 
         if (useShellTransitions) {
-            if (t != null) {
-                // This created its own transition, so send a start request.
-                mService.mAtmService.getTransitionController().requestStartTransition(
-                        t, null /* trigger */, null /* remote */);
-            } else {
+            final boolean wasInTransition = mDisplayContent.inTransition();
+            mDisplayContent.requestChangeTransitionIfNeeded(
+                    ActivityInfo.CONFIG_WINDOW_CONFIGURATION);
+            if (wasInTransition) {
                 // Use remote-rotation infra since the transition has already been requested
                 // TODO(shell-transitions): Remove this once lifecycle management can cover all
                 //                          rotation cases.
@@ -591,17 +586,17 @@
             mService.mH.removeCallbacks(mDisplayRotationHandlerTimeout);
             mIsWaitingForRemoteRotation = false;
 
-            if (mService.mAtmService.getTransitionController().getTransitionPlayer() != null) {
-                if (!mService.mAtmService.getTransitionController().isCollecting()) {
+            if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
+                if (!mDisplayContent.mTransitionController.isCollecting()) {
                     throw new IllegalStateException("Trying to rotate outside a transition");
                 }
-                mService.mAtmService.getTransitionController().collect(mDisplayContent);
+                mDisplayContent.mTransitionController.collect(mDisplayContent);
                 // Go through all tasks and collect them before the rotation
                 // TODO(shell-transitions): move collect() to onConfigurationChange once wallpaper
                 //       handling is synchronized.
                 mDisplayContent.forAllTasks(task -> {
                     if (task.isVisible()) {
-                        mService.mAtmService.getTransitionController().collect(task);
+                        mDisplayContent.mTransitionController.collect(task);
                     }
                 });
                 mDisplayContent.getInsetsStateController().addProvidersToTransition();
diff --git a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java
index b627b33..4141090 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java
@@ -19,6 +19,7 @@
 import android.content.res.Configuration;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.util.IntArray;
 import android.view.IDisplayWindowListener;
 
 /**
@@ -28,23 +29,20 @@
 class DisplayWindowListenerController {
     RemoteCallbackList<IDisplayWindowListener> mDisplayListeners = new RemoteCallbackList<>();
 
-//    private final ArrayList<DisplayContainerListener> mDisplayListeners = new ArrayList<>();
     private final WindowManagerService mService;
 
     DisplayWindowListenerController(WindowManagerService service) {
         mService = service;
     }
 
-    void registerListener(IDisplayWindowListener listener) {
+    int[] registerListener(IDisplayWindowListener listener) {
         synchronized (mService.mGlobalLock) {
             mDisplayListeners.register(listener);
-            try {
-                for (int i = 0; i < mService.mAtmService.mRootWindowContainer.getChildCount();
-                        ++i) {
-                    DisplayContent d = mService.mAtmService.mRootWindowContainer.getChildAt(i);
-                    listener.onDisplayAdded(d.mDisplayId);
-                }
-            } catch (RemoteException e) { }
+            final IntArray displayIds = new IntArray();
+            mService.mAtmService.mRootWindowContainer.forAllDisplays((displayContent) -> {
+                displayIds.add(displayContent.mDisplayId);
+            });
+            return displayIds.toArray();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index 8fcdf2e..4a70fa3 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -264,8 +264,14 @@
 
     @NonNull
     private static AtomicFile getVendorSettingsFile() {
-        final File vendorFile = new File(Environment.getVendorDirectory(),
+        // First look under product path for treblized builds.
+        File vendorFile = new File(Environment.getProductDirectory(),
                 VENDOR_DISPLAY_SETTINGS_FILE_PATH);
+        if (!vendorFile.exists()) {
+            // Try and look in vendor path.
+            vendorFile = new File(Environment.getVendorDirectory(),
+                VENDOR_DISPLAY_SETTINGS_FILE_PATH);
+        }
         return new AtomicFile(vendorFile, WM_DISPLAY_COMMIT_TAG);
     }
 
diff --git a/services/core/java/com/android/server/wm/DockedTaskDividerController.java b/services/core/java/com/android/server/wm/DockedTaskDividerController.java
index fb9d064..925a6d8 100644
--- a/services/core/java/com/android/server/wm/DockedTaskDividerController.java
+++ b/services/core/java/com/android/server/wm/DockedTaskDividerController.java
@@ -46,7 +46,7 @@
     void setTouchRegion(Rect touchRegion) {
         mTouchRegion.set(touchRegion);
         // We need to report touchable region changes to accessibility.
-        if (mDisplayContent.mWmService.mAccessibilityController != null) {
+        if (mDisplayContent.mWmService.mAccessibilityController.hasCallbacks()) {
             mDisplayContent.mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(
                     mDisplayContent.getDisplayId());
         }
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index d12d07a..cc6a880 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -22,6 +22,7 @@
 
 import android.annotation.NonNull;
 import android.content.ClipData;
+import android.content.Context;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -32,6 +33,7 @@
 import android.view.IWindow;
 import android.view.SurfaceControl;
 import android.view.View;
+import android.view.accessibility.AccessibilityManager;
 
 import com.android.server.wm.WindowManagerInternal.IDragDropCallback;
 
@@ -43,7 +45,8 @@
  */
 class DragDropController {
     private static final float DRAG_SHADOW_ALPHA_TRANSPARENT = .7071f;
-    private static final long DRAG_TIMEOUT_MS = 5000;
+    static final long DRAG_TIMEOUT_MS = 5000;
+    private static final int A11Y_DRAG_TIMEOUT_DEFAULT_MS = 60000;
 
     // Messages for Handler.
     static final int MSG_DRAG_END_TIMEOUT = 0;
@@ -151,36 +154,48 @@
                     mDragState.mOriginalAlpha = alpha;
                     mDragState.mToken = dragToken;
                     mDragState.mDisplayContent = displayContent;
-
-                    final Display display = displayContent.getDisplay();
-                    if (!mCallback.get().registerInputChannel(
-                            mDragState, display, mService.mInputManager,
-                            callingWin.mInputChannel)) {
-                        Slog.e(TAG_WM, "Unable to transfer touch focus");
-                        return null;
-                    }
-
-                    final SurfaceControl surfaceControl = mDragState.mSurfaceControl;
                     mDragState.mData = data;
-                    mDragState.broadcastDragStartedLocked(touchX, touchY);
-                    mDragState.overridePointerIconLocked(touchSource);
-                    // remember the thumb offsets for later
-                    mDragState.mThumbOffsetX = thumbCenterX;
-                    mDragState.mThumbOffsetY = thumbCenterY;
 
-                    // Make the surface visible at the proper location
-                    if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM, ">>> OPEN TRANSACTION performDrag");
+                    if ((flags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) == 0) {
+                        final Display display = displayContent.getDisplay();
+                        if (!mCallback.get().registerInputChannel(
+                                mDragState, display, mService.mInputManager,
+                                callingWin.mInputChannel)) {
+                            Slog.e(TAG_WM, "Unable to transfer touch focus");
+                            return null;
+                        }
 
-                    final SurfaceControl.Transaction transaction = mDragState.mTransaction;
-                    transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha);
-                    transaction.setPosition(
-                            surfaceControl, touchX - thumbCenterX, touchY - thumbCenterY);
-                    transaction.show(surfaceControl);
-                    displayContent.reparentToOverlay(transaction, surfaceControl);
-                    callingWin.scheduleAnimation();
+                        final SurfaceControl surfaceControl = mDragState.mSurfaceControl;
+                        mDragState.broadcastDragStartedLocked(touchX, touchY);
+                        mDragState.overridePointerIconLocked(touchSource);
+                        // remember the thumb offsets for later
+                        mDragState.mThumbOffsetX = thumbCenterX;
+                        mDragState.mThumbOffsetY = thumbCenterY;
 
-                    if (SHOW_LIGHT_TRANSACTIONS) {
-                        Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag");
+                        // Make the surface visible at the proper location
+                        if (SHOW_LIGHT_TRANSACTIONS) {
+                            Slog.i(TAG_WM, ">>> OPEN TRANSACTION performDrag");
+                        }
+
+                        final SurfaceControl.Transaction transaction = mDragState.mTransaction;
+                        transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha);
+                        transaction.setPosition(
+                                surfaceControl, touchX - thumbCenterX, touchY - thumbCenterY);
+                        transaction.show(surfaceControl);
+                        displayContent.reparentToOverlay(transaction, surfaceControl);
+                        callingWin.scheduleAnimation();
+                        if (SHOW_LIGHT_TRANSACTIONS) {
+                            Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag");
+                        }
+                    } else {
+                        // Skip surface logic for a drag triggered by an AccessibilityAction
+                        mDragState.broadcastDragStartedLocked(touchX, touchY);
+
+                        // Timeout for the user to drop the content
+                        sendTimeoutMessage(MSG_DRAG_END_TIMEOUT, callingWin.mClient.asBinder(),
+                                getAccessibilityManager().getRecommendedTimeoutMillis(
+                                        A11Y_DRAG_TIMEOUT_DEFAULT_MS,
+                                        AccessibilityManager.FLAG_CONTENT_CONTROLS));
                     }
                 } finally {
                     if (surface != null) {
@@ -309,10 +324,10 @@
     /**
      * Sends a timeout message to the Handler managed by DragDropController.
      */
-    void sendTimeoutMessage(int what, Object arg) {
+    void sendTimeoutMessage(int what, Object arg, long timeoutMs) {
         mHandler.removeMessages(what, arg);
         final Message msg = mHandler.obtainMessage(what, arg);
-        mHandler.sendMessageDelayed(msg, DRAG_TIMEOUT_MS);
+        mHandler.sendMessageDelayed(msg, timeoutMs);
     }
 
     /**
@@ -332,6 +347,30 @@
         }
     }
 
+    boolean dropForAccessibility(IWindow window, float x, float y) {
+        synchronized (mService.mGlobalLock) {
+            final boolean isA11yEnabled = getAccessibilityManager().isEnabled();
+            if (!dragDropActiveLocked()) {
+                return false;
+            }
+            if (mDragState.isAccessibilityDragDrop() && isA11yEnabled) {
+                final WindowState winState = mService.windowForClientLocked(
+                        null, window, false);
+                if (!mDragState.isWindowNotified(winState)) {
+                    return false;
+                }
+                IBinder token = winState.mInputChannelToken;
+                return mDragState.reportDropWindowLock(token, x, y);
+            }
+            return false;
+        }
+    }
+
+    AccessibilityManager getAccessibilityManager() {
+        return (AccessibilityManager) mService.mContext.getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+    }
+
     private class DragHandler extends Handler {
         /**
          * Lock for window manager.
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index aa257f8..4fc123d 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -253,7 +253,7 @@
                 mTransaction.reparent(mSurfaceControl, null).apply();
             } else {
                 mDragDropController.sendTimeoutMessage(MSG_REMOVE_DRAG_SURFACE_TIMEOUT,
-                        mSurfaceControl);
+                        mSurfaceControl, DragDropController.DRAG_TIMEOUT_MS);
             }
             mSurfaceControl = null;
         }
@@ -276,9 +276,9 @@
      * Notify the drop target and tells it about the data. If the drop event is not sent to the
      * target, invokes {@code endDragLocked} immediately.
      */
-    void reportDropWindowLock(IBinder token, float x, float y) {
+    boolean reportDropWindowLock(IBinder token, float x, float y) {
         if (mAnimator != null) {
-            return;
+            return false;
         }
 
         final WindowState touchedWin = mService.mInputToWindowMap.get(token);
@@ -288,7 +288,7 @@
             mDragResult = false;
             endDragLocked();
             if (DEBUG_DRAG) Slog.d(TAG_WM, "Drop outside a valid window " + touchedWin);
-            return;
+            return false;
         }
 
         if (DEBUG_DRAG) Slog.d(TAG_WM, "sending DROP to " + touchedWin);
@@ -322,16 +322,19 @@
             touchedWin.mClient.dispatchDragEvent(event);
 
             // 5 second timeout for this window to respond to the drop
-            mDragDropController.sendTimeoutMessage(MSG_DRAG_END_TIMEOUT, clientToken);
+            mDragDropController.sendTimeoutMessage(MSG_DRAG_END_TIMEOUT, clientToken,
+                    DragDropController.DRAG_TIMEOUT_MS);
         } catch (RemoteException e) {
             Slog.w(TAG_WM, "can't send drop notification to win " + touchedWin);
             endDragLocked();
+            return false;
         } finally {
             if (myPid != touchedWin.mSession.mPid) {
                 event.recycle();
             }
         }
         mToken = clientToken;
+        return true;
     }
 
     class InputInterceptor {
@@ -553,7 +556,7 @@
         }
     }
 
-    private boolean isWindowNotified(WindowState newWin) {
+    boolean isWindowNotified(WindowState newWin) {
         for (WindowState ws : mNotifiedWindows) {
             if (ws == newWin) {
                 return true;
@@ -567,8 +570,10 @@
             return;
         }
         if (!mDragResult) {
-            mAnimator = createReturnAnimationLocked();
-            return;  // Will call closeLocked() when the animation is done.
+            if (!isAccessibilityDragDrop()) {
+                mAnimator = createReturnAnimationLocked();
+                return;  // Will call closeLocked() when the animation is done.
+            }
         }
         closeLocked();
     }
@@ -577,7 +582,7 @@
         if (mAnimator != null) {
             return;
         }
-        if (!mDragInProgress || skipAnimation) {
+        if (!mDragInProgress || skipAnimation || isAccessibilityDragDrop()) {
             // mDragInProgress is false if an app invokes Session#cancelDragAndDrop before
             // Session#performDrag. Reset the drag state without playing the cancel animation
             // because H.DRAG_START_TIMEOUT may be sent to WindowManagerService, which will cause
@@ -722,4 +727,8 @@
             mDragDropController.sendHandlerMessage(MSG_ANIMATION_END, null);
         }
     }
+
+    boolean isAccessibilityDragDrop() {
+        return (mFlags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) != 0;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index b08d6e1..fc317a1 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -127,7 +127,7 @@
         }
     }
 
-    static class EmbeddedWindow {
+    static class EmbeddedWindow implements InputTarget {
         final IWindow mClient;
         @Nullable final WindowState mHostWindowState;
         @Nullable final ActivityRecord mHostActivityRecord;
@@ -166,7 +166,8 @@
             mDisplayId = displayId;
         }
 
-        String getName() {
+        @Override
+        public String toString() {
             final String hostWindowName = (mHostWindowState != null)
                     ? mHostWindowState.getWindowTag().toString() : "Internal";
             return "EmbeddedWindow{ u" + UserHandle.getUserId(mOwnerUid) + " " + hostWindowName
@@ -183,7 +184,7 @@
         }
 
         InputChannel openInputChannel() {
-            final String name = getName();
+            final String name = toString();
             mInputChannel = mWmService.mInputManager.createInputChannel(name);
             return mInputChannel;
         }
@@ -195,5 +196,25 @@
                 mInputChannel = null;
             }
         }
+
+        @Override
+        public WindowState getWindowState() {
+            return mHostWindowState;
+        }
+
+        @Override
+        public int getDisplayId() {
+            return mDisplayId;
+        }
+
+        @Override
+        public IWindow getIWindow() {
+            return mClient;
+        }
+
+        @Override
+        public int getPid() {
+            return mOwnerPid;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index 316c20b..cddb1e7 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -16,30 +16,33 @@
 
 package com.android.server.wm;
 
+import static com.android.server.wm.ActivityRecord.State.INITIALIZING;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_VISIBILITY;
 import static com.android.server.wm.Task.TAG_VISIBILITY;
 
 import android.annotation.Nullable;
 import android.util.Slog;
 
+import java.util.ArrayList;
+
 /** Helper class to ensure activities are in the right visible state for a container. */
 class EnsureActivitiesVisibleHelper {
-    private final Task mTask;
+    private final TaskFragment mTaskFragment;
     private ActivityRecord mTop;
     private ActivityRecord mStarting;
     private boolean mAboveTop;
     private boolean mContainerShouldBeVisible;
-    private boolean mBehindFullscreenActivity;
+    private boolean mBehindFullyOccludedContainer;
     private int mConfigChanges;
     private boolean mPreserveWindows;
     private boolean mNotifyClients;
 
-    EnsureActivitiesVisibleHelper(Task container) {
-        mTask = container;
+    EnsureActivitiesVisibleHelper(TaskFragment container) {
+        mTaskFragment = container;
     }
 
     /**
-     * Update all attributes except {@link mTask} to use in subsequent calculations.
+     * Update all attributes except {@link mTaskFragment} to use in subsequent calculations.
      *
      * @param starting The activity that is being started
      * @param configChanges Parts of the configuration that changed for this activity for evaluating
@@ -51,12 +54,12 @@
     void reset(ActivityRecord starting, int configChanges, boolean preserveWindows,
             boolean notifyClients) {
         mStarting = starting;
-        mTop = mTask.topRunningActivity();
+        mTop = mTaskFragment.topRunningActivity();
         // If the top activity is not fullscreen, then we need to make sure any activities under it
         // are now visible.
         mAboveTop = mTop != null;
-        mContainerShouldBeVisible = mTask.shouldBeVisible(mStarting);
-        mBehindFullscreenActivity = !mContainerShouldBeVisible;
+        mContainerShouldBeVisible = mTaskFragment.shouldBeVisible(mStarting);
+        mBehindFullyOccludedContainer = !mContainerShouldBeVisible;
         mConfigChanges = configChanges;
         mPreserveWindows = preserveWindows;
         mNotifyClients = notifyClients;
@@ -85,22 +88,55 @@
             Slog.v(TAG_VISIBILITY, "ensureActivitiesVisible behind " + mTop
                     + " configChanges=0x" + Integer.toHexString(configChanges));
         }
-        if (mTop != null) {
-            mTask.checkTranslucentActivityWaiting(mTop);
+        if (mTop != null && mTaskFragment.asTask() != null) {
+            // TODO(14709632): Check if this needed to be implemented in TaskFragment.
+            mTaskFragment.asTask().checkTranslucentActivityWaiting(mTop);
         }
 
         // We should not resume activities that being launched behind because these
         // activities are actually behind other fullscreen activities, but still required
         // to be visible (such as performing Recents animation).
         final boolean resumeTopActivity = mTop != null && !mTop.mLaunchTaskBehind
-                && mTask.isTopActivityFocusable()
-                && (starting == null || !starting.isDescendantOf(mTask));
+                && mTaskFragment.isTopActivityFocusable()
+                && (starting == null || !starting.isDescendantOf(mTaskFragment));
 
-        mTask.forAllActivities(a -> {
-            setActivityVisibilityState(a, starting, resumeTopActivity);
-        });
-        if (mTask.mAtmService.getTransitionController().getTransitionPlayer() != null) {
-            mTask.getDisplayContent().mWallpaperController.adjustWallpaperWindows();
+        ArrayList<TaskFragment> adjacentTaskFragments = null;
+        for (int i = mTaskFragment.mChildren.size() - 1; i >= 0; --i) {
+            final WindowContainer child = mTaskFragment.mChildren.get(i);
+            final TaskFragment childTaskFragment = child.asTaskFragment();
+            if (childTaskFragment != null && childTaskFragment.topRunningActivity() != null) {
+                childTaskFragment.updateActivityVisibilities(starting, configChanges,
+                        preserveWindows, notifyClients);
+                mBehindFullyOccludedContainer |=
+                        childTaskFragment.getBounds().equals(mTaskFragment.getBounds());
+                if (mAboveTop && mTop.getTaskFragment() == childTaskFragment) {
+                    mAboveTop = false;
+                }
+
+                if (mBehindFullyOccludedContainer) {
+                    continue;
+                }
+
+                if (adjacentTaskFragments != null && adjacentTaskFragments.contains(
+                        childTaskFragment)) {
+                    // Everything behind two adjacent TaskFragments are occluded.
+                    mBehindFullyOccludedContainer = true;
+                    continue;
+                }
+
+                final TaskFragment adjacentTaskFrag = childTaskFragment.getAdjacentTaskFragment();
+                if (adjacentTaskFrag != null) {
+                    if (adjacentTaskFragments == null) {
+                        adjacentTaskFragments = new ArrayList<>();
+                    }
+                    adjacentTaskFragments.add(adjacentTaskFrag);
+                }
+            } else if (child.asActivityRecord() != null) {
+                setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity);
+            }
+        }
+        if (mTaskFragment.mTransitionController.isShellTransitionsEnabled()) {
+            mTaskFragment.getDisplayContent().mWallpaperController.adjustWallpaperWindows();
         }
     }
 
@@ -112,7 +148,7 @@
         }
         mAboveTop = false;
 
-        r.updateVisibilityIgnoringKeyguard(mBehindFullscreenActivity);
+        r.updateVisibilityIgnoringKeyguard(mBehindFullyOccludedContainer);
         final boolean reallyVisible = r.shouldBeVisibleUnchecked();
 
         // Check whether activity should be visible without Keyguard influence
@@ -122,12 +158,14 @@
                 if (DEBUG_VISIBILITY) {
                     Slog.v(TAG_VISIBILITY, "Fullscreen: at " + r
                             + " containerVisible=" + mContainerShouldBeVisible
-                            + " behindFullscreen=" + mBehindFullscreenActivity);
+                            + " behindFullyOccluded=" + mBehindFullyOccludedContainer);
                 }
-                mBehindFullscreenActivity = true;
+                mBehindFullyOccludedContainer = true;
             } else {
-                mBehindFullscreenActivity = false;
+                mBehindFullyOccludedContainer = false;
             }
+        } else if (r.isState(INITIALIZING)) {
+            r.cancelInitializing();
         }
 
         if (reallyVisible) {
@@ -173,24 +211,25 @@
                 Slog.v(TAG_VISIBILITY, "Make invisible? " + r
                         + " finishing=" + r.finishing + " state=" + r.getState()
                         + " containerShouldBeVisible=" + mContainerShouldBeVisible
-                        + " behindFullscreenActivity=" + mBehindFullscreenActivity
+                        + " behindFullyOccludedContainer=" + mBehindFullyOccludedContainer
                         + " mLaunchTaskBehind=" + r.mLaunchTaskBehind);
             }
             r.makeInvisible();
         }
 
-        if (!mBehindFullscreenActivity && mTask.isActivityTypeHome() && r.isRootOfTask()) {
+        if (!mBehindFullyOccludedContainer && mTaskFragment.isActivityTypeHome()
+                && r.isRootOfTask()) {
             if (DEBUG_VISIBILITY) {
-                Slog.v(TAG_VISIBILITY, "Home task: at " + mTask
+                Slog.v(TAG_VISIBILITY, "Home task: at " + mTaskFragment
                         + " containerShouldBeVisible=" + mContainerShouldBeVisible
-                        + " behindFullscreenActivity=" + mBehindFullscreenActivity);
+                        + " behindOccludedParentContainer=" + mBehindFullyOccludedContainer);
             }
             // No other task in the root home task should be visible behind the home activity.
             // Home activities is usually a translucent activity with the wallpaper behind
             // them. However, when they don't have the wallpaper behind them, we want to
             // show activities in the next application root task behind them vs. another
             // task in the root home task like recents.
-            mBehindFullscreenActivity = true;
+            mBehindFullyOccludedContainer = true;
         }
     }
 
@@ -219,7 +258,8 @@
             r.setVisibility(true);
         }
         if (r != starting) {
-            mTask.mTaskSupervisor.startSpecificActivity(r, andResume, true /* checkConfig */);
+            mTaskFragment.mTaskSupervisor.startSpecificActivity(r, andResume,
+                    true /* checkConfig */);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/FadeRotationAnimationController.java b/services/core/java/com/android/server/wm/FadeRotationAnimationController.java
index eab3f10..52a7ac7 100644
--- a/services/core/java/com/android/server/wm/FadeRotationAnimationController.java
+++ b/services/core/java/com/android/server/wm/FadeRotationAnimationController.java
@@ -42,6 +42,9 @@
     /** A runnable which gets called when the {@link #show()} is called. */
     private Runnable mOnShowRunnable;
 
+    /** Whether to use constant zero alpha animation. */
+    private boolean mHideImmediately;
+
     public FadeRotationAnimationController(DisplayContent displayContent) {
         super(displayContent);
         mService = displayContent.mWmService;
@@ -51,6 +54,10 @@
                 mService.mWindowPlacerLocked.performSurfacePlacement();
             }
         } : null;
+        if (mFrozenTimeoutRunnable != null) {
+            // Hide the windows immediately because screen should have been covered by screenshot.
+            mHideImmediately = true;
+        }
         final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
         final WindowState navigationBar = displayPolicy.getNavigationBar();
         if (navigationBar != null) {
@@ -120,6 +127,15 @@
         }
     }
 
+    /** Hides the window immediately until it is drawn in new rotation. */
+    void hideImmediately(WindowToken windowToken) {
+        final boolean original = mHideImmediately;
+        mHideImmediately = true;
+        mTargetWindowTokens.add(windowToken);
+        fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_FIXED_TRANSFORM);
+        mHideImmediately = original;
+    }
+
     /** Returns {@code true} if the window is handled by this controller. */
     boolean isHandledToken(WindowToken token) {
         return token == mNavBarToken || isTargetToken(token);
@@ -145,8 +161,7 @@
 
     @Override
     public Animation getFadeOutAnimation() {
-        if (mFrozenTimeoutRunnable != null) {
-            // Hide the window immediately because screen should have been covered by screenshot.
+        if (mHideImmediately) {
             return new AlphaAnimation(0 /* fromAlpha */, 0 /* toAlpha */);
         }
         return super.getFadeOutAnimation();
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index ed1e784..cbefe7f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -29,6 +29,7 @@
 import static com.android.server.wm.WindowManagerService.H.UPDATE_MULTI_WINDOW_STACKS;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Trace;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsSource;
@@ -91,6 +92,16 @@
     }
 
     @Override
+    void updateControlForTarget(@Nullable InsetsControlTarget target, boolean force) {
+        if (target != null && target.getWindow() != null) {
+            // ime control target could be a different window.
+            // Refer WindowState#getImeControlTarget().
+            target = target.getWindow().getImeControlTarget();
+        }
+        super.updateControlForTarget(target, force);
+    }
+
+    @Override
     protected boolean updateClientVisibility(InsetsControlTarget caller) {
         boolean changed = super.updateClientVisibility(caller);
         if (changed && caller.getRequestedVisibility(mSource.getType())) {
diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
index 747d365..f3b9cdf 100644
--- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
+++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
@@ -20,6 +20,7 @@
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
+import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
 
 import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
@@ -420,7 +421,7 @@
         }
 
         final Bundle options = new Bundle();
-        options.putInt(DisplayAreaPolicyBuilder.KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId);
+        options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId);
         return options;
     }
 
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index aa7e6c9..18a2c60 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -30,6 +30,7 @@
 import android.view.InputApplicationHandle;
 import android.view.KeyEvent;
 import android.view.WindowManager;
+import android.view.WindowManagerPolicyConstants;
 
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.input.InputManagerService;
@@ -181,8 +182,8 @@
     @Override
     public int getPointerLayer() {
         return mService.mPolicy.getWindowLayerFromTypeLw(WindowManager.LayoutParams.TYPE_POINTER)
-                * WindowManagerService.TYPE_LAYER_MULTIPLIER
-                + WindowManagerService.TYPE_LAYER_OFFSET;
+                * WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER
+                + WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
     }
 
     /** Callback to get pointer display id. */
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index d417d56..1e7b676 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -48,6 +48,9 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.WindowManagerService.LOGTAG_INPUT_FOCUS;
 
+import static java.lang.Integer.MAX_VALUE;
+
+import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Handler;
@@ -67,6 +70,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
 import java.util.Set;
 import java.util.function.Consumer;
 
@@ -101,6 +105,15 @@
     private final ArrayMap<String, InputConsumerImpl> mInputConsumers = new ArrayMap();
 
     /**
+     * Set when recents (overview) is active as part of a shell transition. While set, any focus
+     * going to the recents activity will be redirected to the Recents input consumer. Since we
+     * draw the live-tile above the recents activity, we also need to provide that activity as a
+     * z-layering reference so that we can place the recents input consumer above it.
+     */
+    private WeakReference<ActivityRecord> mActiveRecentsActivity = null;
+    private WeakReference<ActivityRecord> mActiveRecentsLayerRef = null;
+
+    /**
      * Representation of a input consumer that the policy has added to the window manager to consume
      * input events going to windows below it.
      */
@@ -279,6 +292,7 @@
         inputWindowHandle.setInputFeatures(w.mAttrs.inputFeatures);
         inputWindowHandle.setPaused(w.mActivityRecord != null && w.mActivityRecord.paused);
         inputWindowHandle.setVisible(w.isVisible());
+        inputWindowHandle.setWindowToken(w.mClient);
 
         final boolean focusable = w.canReceiveKeys()
                 && (mService.mPerDisplayFocusEnabled || mDisplayContent.isOnTop());
@@ -308,7 +322,10 @@
         boolean useSurfaceCrop = false;
         final Task task = w.getTask();
         if (task != null) {
-            if (task.isOrganized() && task.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+            // TODO(b/165794636): Remove the special case for freeform window once drag resizing is
+            // handled by WM shell.
+            if (task.isOrganized() && task.getWindowingMode() != WINDOWING_MODE_FULLSCREEN
+                        && !task.inFreeformWindowingMode()) {
                 // If the window is in a TaskManaged by a TaskOrganizer then most cropping will
                 // be applied using the SurfaceControl hierarchy from the Organizer. This means
                 // we need to make sure that these changes in crop are reflected in the input
@@ -393,6 +410,21 @@
     }
 
     /**
+     * Inform InputMonitor when recents is active so it can enable the recents input consumer.
+     * @param activity The active recents activity. {@code null} means recents is not active.
+     * @param layer An activity whose Z-layer is used as a reference for how to sort the consumer.
+     */
+    void setActiveRecents(@Nullable ActivityRecord activity, @Nullable ActivityRecord layer) {
+        final boolean clear = activity == null;
+        mActiveRecentsActivity = clear ? null : new WeakReference<>(activity);
+        mActiveRecentsLayerRef = clear ? null : new WeakReference<>(layer);
+    }
+
+    private static <T> T getWeak(WeakReference<T> ref) {
+        return ref != null ? ref.get() : null;
+    }
+
+    /**
      * Called when the current input focus changes.
      */
     private void updateInputFocusRequest(InputConsumerImpl recentsAnimationInputConsumer) {
@@ -402,8 +434,10 @@
         if (recentsAnimationInputConsumer != null && focus != null) {
             final RecentsAnimationController recentsAnimationController =
                     mService.getRecentsAnimationController();
-            final boolean shouldApplyRecentsInputConsumer = recentsAnimationController != null
-                    && recentsAnimationController.shouldApplyInputConsumer(focus.mActivityRecord);
+            final boolean shouldApplyRecentsInputConsumer = (recentsAnimationController != null
+                    && recentsAnimationController.shouldApplyInputConsumer(focus.mActivityRecord))
+                    // Shell transitions doesn't use RecentsAnimationController
+                    || getWeak(mActiveRecentsActivity) != null;
             if (shouldApplyRecentsInputConsumer) {
                 requestFocus(recentsAnimationInputConsumer.mWindowHandle.token,
                         recentsAnimationInputConsumer.mName);
@@ -503,6 +537,14 @@
             mInDrag = inDrag;
 
             resetInputConsumers(mInputTransaction);
+            // Update recents input consumer layer if active
+            if (mAddRecentsAnimationInputConsumerHandle
+                    && getWeak(mActiveRecentsActivity) != null) {
+                final WindowContainer layer = getWeak(mActiveRecentsLayerRef);
+                mRecentsAnimationInputConsumer.show(mInputTransaction,
+                        layer != null ? layer : getWeak(mActiveRecentsActivity));
+                mAddRecentsAnimationInputConsumerHandle = false;
+            }
             mDisplayContent.forAllWindows(this, true /* traverseTopToBottom */);
             updateInputFocusRequest(mRecentsAnimationInputConsumer);
 
@@ -537,11 +579,17 @@
 
             final int privateFlags = w.mAttrs.privateFlags;
 
+            // This only works for legacy transitions.
             if (mAddRecentsAnimationInputConsumerHandle && shouldApplyRecentsInputConsumer) {
                 if (recentsAnimationController.updateInputConsumerForApp(
                         mRecentsAnimationInputConsumer.mWindowHandle)) {
-                    mRecentsAnimationInputConsumer.show(mInputTransaction, w.mActivityRecord);
-                    mAddRecentsAnimationInputConsumerHandle = false;
+                    final DisplayArea targetDA =
+                            recentsAnimationController.getTargetAppDisplayArea();
+                    if (targetDA != null) {
+                        mRecentsAnimationInputConsumer.reparent(mInputTransaction, targetDA);
+                        mRecentsAnimationInputConsumer.show(mInputTransaction, MAX_VALUE - 1);
+                        mAddRecentsAnimationInputConsumerHandle = false;
+                    }
                 }
             }
 
@@ -552,7 +600,7 @@
                             rootTask.getSurfaceControl());
                     // We set the layer to z=MAX-1 so that it's always on top.
                     mPipInputConsumer.reparent(mInputTransaction, rootTask);
-                    mPipInputConsumer.show(mInputTransaction, Integer.MAX_VALUE - 1);
+                    mPipInputConsumer.show(mInputTransaction, MAX_VALUE - 1);
                     mAddPipInputConsumerHandle = false;
                 }
             }
diff --git a/services/core/java/com/android/server/wm/InputTarget.java b/services/core/java/com/android/server/wm/InputTarget.java
new file mode 100644
index 0000000..c7d328a
--- /dev/null
+++ b/services/core/java/com/android/server/wm/InputTarget.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.view.IWindow;
+
+/**
+ * Common interface between focusable objects.
+ *
+ * Both WindowState and EmbeddedWindows can receive input. This consolidates some common properties
+ * of both targets.
+ */
+interface InputTarget {
+    /* Get the WindowState associated with the target. */
+    WindowState getWindowState();
+
+    /* Display id of the target. */
+    int getDisplayId();
+
+    /* Client IWindow for the target. */
+    IWindow getIWindow();
+
+    /* Owning pid of the target. */
+    int getPid();
+}
+
diff --git a/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java b/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java
index 7a4d13c..0a24d3c 100644
--- a/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java
+++ b/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.graphics.Region;
 import android.os.IBinder;
+import android.view.IWindow;
 import android.view.InputApplicationHandle;
 import android.view.InputWindowHandle;
 import android.view.SurfaceControl;
@@ -275,6 +276,14 @@
         mChanged = true;
     }
 
+    void setWindowToken(IWindow windowToken) {
+        if (mHandle.getWindow() == windowToken) {
+            return;
+        }
+        mHandle.setWindowToken(windowToken);
+        mChanged = true;
+    }
+
     @Override
     public String toString() {
         return mHandle + ", changed=" + mChanged;
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index a8e1c1c..3d19f54 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -18,8 +18,6 @@
 
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.view.InsetsController.ANIMATION_TYPE_HIDE;
 import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
 import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
@@ -45,16 +43,20 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InternalInsetsAnimationController;
 import android.view.SurfaceControl;
 import android.view.SyncRtSurfaceTransactionApplier;
+import android.view.WindowInsets.Type;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.WindowInsetsAnimationControlListener;
+import android.view.WindowInsetsAnimationController;
 import android.view.WindowManager;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.DisplayThread;
+import com.android.server.statusbar.StatusBarManagerInternal;
 
 /**
  * Policy that implements who gets control over the windows generating insets.
@@ -135,15 +137,19 @@
             abortTransient();
         }
         mFocusedWin = focusedWin;
-        boolean forceShowsSystemBarsForWindowingMode = forceShowsSystemBarsForWindowingMode();
-        InsetsControlTarget statusControlTarget = getStatusControlTarget(focusedWin,
-                forceShowsSystemBarsForWindowingMode);
-        InsetsControlTarget navControlTarget = getNavControlTarget(focusedWin,
-                forceShowsSystemBarsForWindowingMode);
-        mStateController.onBarControlTargetChanged(statusControlTarget,
-                getFakeControlTarget(focusedWin, statusControlTarget),
+        final InsetsControlTarget statusControlTarget =
+                getStatusControlTarget(focusedWin, false /* fake */);
+        final InsetsControlTarget navControlTarget =
+                getNavControlTarget(focusedWin, false /* fake */);
+        mStateController.onBarControlTargetChanged(
+                statusControlTarget,
+                statusControlTarget == mDummyControlTarget
+                        ? getStatusControlTarget(focusedWin, true /* fake */)
+                        : null,
                 navControlTarget,
-                getFakeControlTarget(focusedWin, navControlTarget));
+                navControlTarget == mDummyControlTarget
+                        ? getNavControlTarget(focusedWin, true /* fake */)
+                        : null);
         mStatusBar.updateVisibility(statusControlTarget, ITYPE_STATUS_BAR);
         mNavBar.updateVisibility(navControlTarget, ITYPE_NAVIGATION_BAR);
     }
@@ -167,8 +173,12 @@
             changed = true;
         }
         if (changed) {
-            mPolicy.getStatusBarManagerInternal().showTransient(mDisplayContent.getDisplayId(),
-                    mShowingTransientTypes.toArray());
+            StatusBarManagerInternal statusBarManagerInternal =
+                    mPolicy.getStatusBarManagerInternal();
+            if (statusBarManagerInternal != null) {
+                statusBarManagerInternal.showTransient(
+                        mDisplayContent.getDisplayId(), mShowingTransientTypes.toArray());
+            }
             updateBarControlTarget(mFocusedWin);
 
             // The leashes can be created while updating bar control target. The surface transaction
@@ -303,9 +313,11 @@
                     abortTypes.add(type);
                 }
             }
-            if (abortTypes.size() > 0) {
-                mPolicy.getStatusBarManagerInternal().abortTransient(mDisplayContent.getDisplayId(),
-                        abortTypes.toArray());
+            StatusBarManagerInternal statusBarManagerInternal =
+                    mPolicy.getStatusBarManagerInternal();
+            if (abortTypes.size() > 0 && statusBarManagerInternal != null) {
+                statusBarManagerInternal.abortTransient(
+                        mDisplayContent.getDisplayId(), abortTypes.toArray());
             }
         }
     }
@@ -315,19 +327,17 @@
      * updateBarControlTarget(mFocusedWin) after this invocation.
      */
     private void abortTransient() {
-        mPolicy.getStatusBarManagerInternal().abortTransient(mDisplayContent.getDisplayId(),
-                mShowingTransientTypes.toArray());
+        StatusBarManagerInternal statusBarManagerInternal = mPolicy.getStatusBarManagerInternal();
+        if (statusBarManagerInternal != null) {
+            statusBarManagerInternal.abortTransient(
+                    mDisplayContent.getDisplayId(), mShowingTransientTypes.toArray());
+        }
         mShowingTransientTypes.clear();
     }
 
-    private @Nullable InsetsControlTarget getFakeControlTarget(@Nullable WindowState focused,
-            InsetsControlTarget realControlTarget) {
-        return realControlTarget == mDummyControlTarget ? focused : null;
-    }
-
     private @Nullable InsetsControlTarget getStatusControlTarget(@Nullable WindowState focusedWin,
-            boolean forceShowsSystemBarsForWindowingMode) {
-        if (mShowingTransientTypes.indexOf(ITYPE_STATUS_BAR) != -1) {
+            boolean fake) {
+        if (!fake && isShowingTransientTypes(Type.statusBars())) {
             return mDummyControlTarget;
         }
         final WindowState notificationShade = mPolicy.getNotificationShade();
@@ -340,13 +350,12 @@
                     focusedWin.mAttrs.packageName);
             return mDisplayContent.mRemoteInsetsControlTarget;
         }
-        if (forceShowsSystemBarsForWindowingMode) {
-            // Status bar is forcibly shown for the windowing mode which is a steady state.
-            // We don't want the client to control the status bar, and we will dispatch the real
-            // visibility of status bar to the client.
+        if (mPolicy.areSystemBarsForcedShownLw()) {
+            // Status bar is forcibly shown. We don't want the client to control the status bar, and
+            // we will dispatch the real visibility of status bar to the client.
             return null;
         }
-        if (forceShowsStatusBarTransiently()) {
+        if (forceShowsStatusBarTransiently() && !fake) {
             // Status bar is forcibly shown transiently, and its new visibility won't be
             // dispatched to the client so that we can keep the layout stable. We will dispatch the
             // fake control to the client, so that it can re-show the bar during this scenario.
@@ -372,13 +381,13 @@
     }
 
     private @Nullable InsetsControlTarget getNavControlTarget(@Nullable WindowState focusedWin,
-            boolean forceShowsSystemBarsForWindowingMode) {
+            boolean fake) {
         final WindowState imeWin = mDisplayContent.mInputMethodWindow;
         if (imeWin != null && imeWin.isVisible()) {
             // Force showing navigation bar while IME is visible.
             return null;
         }
-        if (mShowingTransientTypes.indexOf(ITYPE_NAVIGATION_BAR) != -1) {
+        if (!fake && isShowingTransientTypes(Type.navigationBars())) {
             return mDummyControlTarget;
         }
         if (focusedWin == mPolicy.getNotificationShade()) {
@@ -390,13 +399,12 @@
                     focusedWin.mAttrs.packageName);
             return mDisplayContent.mRemoteInsetsControlTarget;
         }
-        if (forceShowsSystemBarsForWindowingMode) {
-            // Navigation bar is forcibly shown for the windowing mode which is a steady state.
-            // We don't want the client to control the navigation bar, and we will dispatch the real
-            // visibility of navigation bar to the client.
+        if (mPolicy.areSystemBarsForcedShownLw()) {
+            // Navigation bar is forcibly shown. We don't want the client to control the navigation
+            // bar, and we will dispatch the real visibility of navigation bar to the client.
             return null;
         }
-        if (forceShowsNavigationBarTransiently()) {
+        if (forceShowsNavigationBarTransiently() && !fake) {
             // Navigation bar is forcibly shown transiently, and its new visibility won't be
             // dispatched to the client so that we can keep the layout stable. We will dispatch the
             // fake control to the client, so that it can re-show the bar during this scenario.
@@ -405,6 +413,16 @@
         return focusedWin;
     }
 
+    private boolean isShowingTransientTypes(@Type.InsetsType int types) {
+        final IntArray showingTransientTypes = mShowingTransientTypes;
+        for (int i = showingTransientTypes.size() - 1; i >= 0; i--) {
+            if ((InsetsState.toPublicType(showingTransientTypes.get(i)) & types) != 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Determines whether the remote insets controller should take control of system bars for all
      * windows.
@@ -438,19 +456,6 @@
                 && (win.mAttrs.privateFlags & PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION) != 0;
     }
 
-    private boolean forceShowsSystemBarsForWindowingMode() {
-        final boolean isDockedRootTaskVisible = mDisplayContent.getDefaultTaskDisplayArea()
-                .isRootTaskVisible(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        final boolean isFreeformRootTaskVisible = mDisplayContent.getDefaultTaskDisplayArea()
-                .isRootTaskVisible(WINDOWING_MODE_FREEFORM);
-        final boolean isResizing = mDisplayContent.getDockedDividerController().isResizing();
-
-        // We need to force system bars when the docked root task is visible, when the freeform
-        // root task is visible but also when we are resizing for the transitions when docked
-        // root task visibility changes.
-        return isDockedRootTaskVisible || isFreeformRootTaskVisible || isResizing;
-    }
-
     @VisibleForTesting
     void startAnimation(boolean show, Runnable callback) {
         int typesReady = 0;
@@ -495,8 +500,12 @@
             final int state = visible ? WINDOW_STATE_SHOWING : WINDOW_STATE_HIDDEN;
             if (mState != state) {
                 mState = state;
-                mPolicy.getStatusBarManagerInternal().setWindowState(
-                        mDisplayContent.getDisplayId(), mId, state);
+                StatusBarManagerInternal statusBarManagerInternal =
+                        mPolicy.getStatusBarManagerInternal();
+                if (statusBarManagerInternal != null) {
+                    statusBarManagerInternal.setWindowState(
+                            mDisplayContent.getDisplayId(), mId, state);
+                }
             }
         }
     }
@@ -588,8 +597,8 @@
             }
 
             @Override
-            public void startAnimation(InsetsAnimationControlImpl controller,
-                    WindowInsetsAnimationControlListener listener, int types,
+            public <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController>
+            void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types,
                     WindowInsetsAnimation animation,
                     Bounds bounds) {
             }
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 7daebff..d202587 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -22,7 +22,7 @@
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IME;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS;
 import static com.android.server.wm.InsetsSourceProviderProto.CAPTURED_LEASH;
 import static com.android.server.wm.InsetsSourceProviderProto.CLIENT_VISIBLE;
 import static com.android.server.wm.InsetsSourceProviderProto.CONTROL;
@@ -163,8 +163,10 @@
             // animate-out as new one animates-in.
             mWin.cancelAnimation();
             mWin.mProvidedInsetsSources.remove(mSource.getType());
+            mSeamlessRotating = false;
         }
-        ProtoLog.d(WM_DEBUG_IME, "InsetsSource setWin %s", win);
+        ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource setWin %s for type %s", win,
+                InsetsState.typeToString(mSource.getType()));
         mWin = win;
         mFrameProvider = frameProvider;
         mImeFrameProvider = imeFrameProvider;
@@ -266,7 +268,7 @@
                         && mWin.okToDisplay()) {
                     mWin.applyWithNextDraw(mSetLeashPositionConsumer);
                 } else {
-                    mSetLeashPositionConsumer.accept(mWin.getPendingTransaction());
+                    mSetLeashPositionConsumer.accept(mWin.getSyncTransaction());
                 }
             }
             if (mServerVisible && !mLastSourceFrame.equals(mSource.getFrame())) {
@@ -307,11 +309,6 @@
             // to control the window for now.
             return;
         }
-        if (target != null && target.getWindow() != null) {
-            // ime control target could be a different window.
-            // Refer WindowState#getImeControlTarget().
-            target = target.getWindow().getImeControlTarget();
-        }
 
         if (mWin != null && mWin.getSurfaceControl() == null) {
             // if window doesn't have a surface, set it null and return.
@@ -335,7 +332,7 @@
         if (getSource().getType() == ITYPE_IME) {
             setClientVisible(target.getRequestedVisibility(mSource.getType()));
         }
-        final Transaction t = mDisplayContent.getPendingTransaction();
+        final Transaction t = mDisplayContent.getSyncTransaction();
         mWin.startAnimation(t, mAdapter, !mClientVisible /* hidden */,
                 ANIMATION_TYPE_INSETS_CONTROL);
 
@@ -348,7 +345,7 @@
         updateVisibility();
         mControl = new InsetsSourceControl(mSource.getType(), leash, surfacePosition,
                 mSource.calculateInsets(mWin.getBounds(), true /* ignoreVisibility */));
-        ProtoLog.d(WM_DEBUG_IME,
+        ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
                 "InsetsSource Control %s for target %s", mControl, mControlTarget);
     }
 
@@ -381,8 +378,11 @@
             return;
         }
         mClientVisible = clientVisible;
-        mDisplayContent.mWmService.mH.obtainMessage(
-                LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED, mDisplayContent).sendToTarget();
+        if (!mDisplayContent.mLayoutAndAssignWindowLayersScheduled) {
+            mDisplayContent.mLayoutAndAssignWindowLayersScheduled = true;
+            mDisplayContent.mWmService.mH.obtainMessage(
+                    LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED, mDisplayContent).sendToTarget();
+        }
         updateVisibility();
     }
 
@@ -394,8 +394,9 @@
 
     protected void updateVisibility() {
         mSource.setVisible(mServerVisible && (isMirroredSource() || mClientVisible));
-        ProtoLog.d(WM_DEBUG_IME,
-                "InsetsSource updateVisibility serverVisible: %s clientVisible: %s",
+        ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
+                "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s",
+                InsetsState.typeToString(mSource.getType()),
                 mServerVisible, mClientVisible);
     }
 
@@ -534,14 +535,14 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
-                @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+                @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
             // TODO(b/166736352): Check if we still need to control the IME visibility here.
             if (mSource.getType() == ITYPE_IME) {
                 // TODO: use 0 alpha and remove t.hide() once b/138459974 is fixed.
                 t.setAlpha(animationLeash, 1 /* alpha */);
                 t.hide(animationLeash);
             }
-            ProtoLog.i(WM_DEBUG_IME,
+            ProtoLog.i(WM_DEBUG_WINDOW_INSETS,
                     "ControlAdapter startAnimation mSource: %s controlTarget: %s", mSource,
                     mControlTarget);
 
@@ -557,7 +558,7 @@
                 mControlTarget = null;
                 mAdapter = null;
                 setClientVisible(InsetsState.getDefaultVisibility(mSource.getType()));
-                ProtoLog.i(WM_DEBUG_IME,
+                ProtoLog.i(WM_DEBUG_WINDOW_INSETS,
                         "ControlAdapter onAnimationCancelled mSource: %s mControlTarget: %s",
                         mSource, mControlTarget);
             }
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 655007c..c4ca8e3 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -254,7 +254,7 @@
             if (p == null) continue;
             final WindowContainer wc = p.mWin;
             if (wc == null) continue;
-            mDisplayContent.mAtmService.getTransitionController().collect(wc);
+            mDisplayContent.mTransitionController.collect(wc);
         }
     }
 
@@ -385,7 +385,7 @@
         if (changed) {
             notifyInsetsChanged();
             mDisplayContent.updateSystemGestureExclusion();
-            mDisplayContent.getDisplayPolicy().updateSystemUiVisibilityLw();
+            mDisplayContent.getDisplayPolicy().updateSystemBarAttributes();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index cf0f973..bd41de3 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
@@ -27,6 +28,7 @@
 import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS;
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_SUBTLE_WINDOW_ANIMATIONS;
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_SHADE;
@@ -49,6 +51,7 @@
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
+import android.view.WindowManager;
 
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -158,6 +161,7 @@
         final boolean keyguardChanged = (keyguardShowing != mKeyguardShowing)
                 || (mKeyguardGoingAway && keyguardShowing && !aodChanged);
         if (!keyguardChanged && !aodChanged) {
+            setWakeTransitionReady();
             return;
         }
         EventLogTags.writeWmSetKeyguardShown(
@@ -190,8 +194,7 @@
 
         if (keyguardChanged) {
             // Irrelevant to AOD.
-            dismissMultiWindowModeForTaskIfNeeded(null /* currentTaskControllsingOcclusion */,
-                    false /* turningScreenOn */);
+            dismissMultiWindowModeForTaskIfNeeded(null /* currentTaskControllsingOcclusion */);
             mKeyguardGoingAway = false;
             if (keyguardShowing) {
                 mDismissalRequested = false;
@@ -203,6 +206,15 @@
         updateKeyguardSleepToken();
         mRootWindowContainer.ensureActivitiesVisible(null, 0, !PRESERVE_WINDOWS);
         InputMethodManagerInternal.get().updateImeWindowStatus(false /* disableImeIcon */);
+        setWakeTransitionReady();
+    }
+
+    private void setWakeTransitionReady() {
+        if (mWindowManager.mAtmService.getTransitionController().getCollectingTransitionType()
+                == WindowManager.TRANSIT_WAKE) {
+            mWindowManager.mAtmService.getTransitionController().setReady(
+                    mRootWindowContainer.getDefaultDisplay());
+        }
     }
 
     /**
@@ -224,8 +236,14 @@
                     mAodShowing ? 1 : 0,
                     1 /* keyguardGoingAway */,
                     "keyguardGoingAway");
-            mRootWindowContainer.getDefaultDisplay().requestTransitionAndLegacyPrepare(
-                    TRANSIT_KEYGUARD_GOING_AWAY, convertTransitFlags(flags));
+            final int transitFlags = convertTransitFlags(flags);
+            final DisplayContent dc = mRootWindowContainer.getDefaultDisplay();
+            dc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, transitFlags);
+            // We are deprecating TRANSIT_KEYGUARD_GOING_AWAY for Shell transition and use
+            // TRANSIT_FLAG_KEYGUARD_GOING_AWAY to indicate that it should animate keyguard going
+            // away.
+            dc.mAtmService.getTransitionController().requestTransitionIfNeeded(
+                    TRANSIT_TO_BACK, transitFlags, null /* trigger */, dc);
             updateKeyguardSleepToken();
 
             // Some stack visibility might change (e.g. docked stack)
@@ -265,7 +283,7 @@
     }
 
     private int convertTransitFlags(int keyguardGoingAwayFlags) {
-        int result = 0;
+        int result = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
         if ((keyguardGoingAwayFlags & KEYGUARD_GOING_AWAY_FLAG_TO_SHADE) != 0) {
             result |= TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
         }
@@ -334,6 +352,7 @@
         for (int displayNdx = mRootWindowContainer.getChildCount() - 1;
              displayNdx >= 0; displayNdx--) {
             final DisplayContent display = mRootWindowContainer.getChildAt(displayNdx);
+            if (display.isRemoving() || display.isRemoved()) continue;
             final KeyguardDisplayState state = getDisplayState(display.mDisplayId);
             state.updateVisibility(this, display);
             requestDismissKeyguard |= state.mRequestDismissKeyguard;
@@ -355,7 +374,7 @@
         // TODO(b/113840485): Handle app transition for individual display, and apply occluded
         // state change to secondary displays.
         // For now, only default display fully supports occluded change. Other displays only
-        // updates keygaurd sleep token on that display.
+        // updates keyguard sleep token on that display.
         if (displayId != DEFAULT_DISPLAY) {
             updateKeyguardSleepToken(displayId);
             return;
@@ -366,25 +385,18 @@
             mService.deferWindowLayout();
             try {
                 mRootWindowContainer.getDefaultDisplay()
-                        .prepareAppTransition(
+                        .requestTransitionAndLegacyPrepare(
                                 isDisplayOccluded(DEFAULT_DISPLAY)
                                         ? TRANSIT_KEYGUARD_OCCLUDE
-                                        : TRANSIT_KEYGUARD_UNOCCLUDE);
-                // When the occluding activity also turns on the display, visibility of the activity
-                // can be committed before KEYGUARD_OCCLUDE transition is handled.
-                // Set mRequestForceTransition flag to make sure that the app transition animation
-                // is applied for such case.
-                // TODO(b/194243906): Fix this before enabling the remote keyguard animation.
-                if (WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation
-                        && topActivity != null) {
-                    topActivity.mRequestForceTransition = true;
-                }
+                                        : TRANSIT_KEYGUARD_UNOCCLUDE, 0 /* flags */);
                 updateKeyguardSleepToken(DEFAULT_DISPLAY);
                 mWindowManager.executeAppTransition();
             } finally {
                 mService.continueWindowLayout();
             }
         }
+        dismissMultiWindowModeForTaskIfNeeded(topActivity != null
+                ? topActivity.getRootTask() : null);
     }
 
     /**
@@ -410,21 +422,6 @@
         }
     }
 
-    /**
-     * Called when somebody wants to turn screen on.
-     */
-    private void handleTurnScreenOn(int displayId) {
-        if (displayId != DEFAULT_DISPLAY) {
-            return;
-        }
-
-        mTaskSupervisor.wakeUp("handleTurnScreenOn");
-        if (mKeyguardShowing && canDismissKeyguard()) {
-            mWindowManager.dismissKeyguard(null /* callback */, null /* message */);
-            mDismissalRequested = true;
-        }
-    }
-
     boolean isDisplayOccluded(int displayId) {
         return getDisplayState(displayId).mOccluded;
     }
@@ -438,11 +435,9 @@
     }
 
     private void dismissMultiWindowModeForTaskIfNeeded(
-            @Nullable Task currentTaskControllingOcclusion, boolean turningScreenOn) {
-        // If turningScreenOn is true, it means that the visibility state has changed from
-        // currentTaskControllingOcclusion and we should update windowing mode.
+            @Nullable Task currentTaskControllingOcclusion) {
         // TODO(b/113840485): Handle docked stack for individual display.
-        if (!turningScreenOn && (!mKeyguardShowing || !isDisplayOccluded(DEFAULT_DISPLAY))) {
+        if (!mKeyguardShowing || !isDisplayOccluded(DEFAULT_DISPLAY)) {
             return;
         }
 
@@ -581,26 +576,17 @@
                     && controller.mWindowManager.isKeyguardSecure(
                     controller.mService.getCurrentUserId());
 
-            boolean occludingChange = false;
-            boolean turningScreenOn = false;
             if (mTopTurnScreenOnActivity != lastTurnScreenOnActivity
                     && mTopTurnScreenOnActivity != null
                     && !mService.mWindowManager.mPowerManager.isInteractive()
-                    && (mRequestDismissKeyguard || occludedByActivity
-                        || controller.canDismissKeyguard())) {
-                turningScreenOn = true;
-                controller.handleTurnScreenOn(mDisplayId);
+                    && (mRequestDismissKeyguard || occludedByActivity)) {
+                controller.mTaskSupervisor.wakeUp("handleTurnScreenOn");
                 mTopTurnScreenOnActivity.setCurrentLaunchCanTurnScreenOn(false);
             }
 
             if (lastOccluded != mOccluded) {
-                occludingChange = true;
                 controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
             }
-
-            if (occludingChange || turningScreenOn) {
-                controller.dismissMultiWindowModeForTaskIfNeeded(task, turningScreenOn);
-            }
         }
 
         /**
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index 3dbe79d..4b98013 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -19,19 +19,24 @@
 import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
 import static android.view.SurfaceControl.HIDDEN;
 
+import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.os.Process;
+import android.view.GestureDetector;
 import android.view.InputChannel;
+import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.InputWindowHandle;
+import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 
 import com.android.server.UiThread;
 
+import java.util.function.IntConsumer;
 import java.util.function.Supplier;
 
 /**
@@ -58,11 +63,15 @@
     private final LetterboxSurface mLeft = new LetterboxSurface("left");
     private final LetterboxSurface mBottom = new LetterboxSurface("bottom");
     private final LetterboxSurface mRight = new LetterboxSurface("right");
-    // Prevents wallpaper from peeking through near rounded corners. It's not included in
-    // mSurfaces array since it isn't needed in methods like notIntersectsOrFullyContains
-    // or attachInput.
-    private final LetterboxSurface mBehind = new LetterboxSurface("behind");
+    // One surface that fills the whole window is used over multiple surfaces to:
+    // - Prevents wallpaper from peeking through near rounded corners.
+    // - For "blurred wallpaper" background, to avoid having visible border between surfaces.
+    // One surface approach isn't always preferred over multiple surfaces due to rendering cost
+    // for overlaping an app window and letterbox surfaces.
+    private final LetterboxSurface mFullWindowSurface = new LetterboxSurface("fullWindow");
     private final LetterboxSurface[] mSurfaces = { mLeft, mTop, mRight, mBottom };
+    // Reachability gestures.
+    private final IntConsumer mDoubleTapCallback;
 
     /**
      * Constructs a Letterbox.
@@ -75,7 +84,8 @@
             Supplier<Color> colorSupplier,
             Supplier<Boolean> hasWallpaperBackgroundSupplier,
             Supplier<Integer> blurRadiusSupplier,
-            Supplier<Float> darkScrimAlphaSupplier) {
+            Supplier<Float> darkScrimAlphaSupplier,
+            IntConsumer doubleTapCallback) {
         mSurfaceControlFactory = surfaceControlFactory;
         mTransactionFactory = transactionFactory;
         mAreCornersRounded = areCornersRounded;
@@ -83,6 +93,7 @@
         mHasWallpaperBackgroundSupplier = hasWallpaperBackgroundSupplier;
         mBlurRadiusSupplier = blurRadiusSupplier;
         mDarkScrimAlphaSupplier = darkScrimAlphaSupplier;
+        mDoubleTapCallback = doubleTapCallback;
     }
 
     /**
@@ -104,7 +115,7 @@
         mLeft.layout(outer.left, outer.top, inner.left, outer.bottom, surfaceOrigin);
         mBottom.layout(outer.left, inner.bottom, outer.right, outer.bottom, surfaceOrigin);
         mRight.layout(inner.right, outer.top, outer.right, outer.bottom, surfaceOrigin);
-        mBehind.layout(inner.left, inner.top, inner.right, inner.bottom, surfaceOrigin);
+        mFullWindowSurface.layout(outer.left, outer.top, outer.right, outer.bottom, surfaceOrigin);
     }
 
 
@@ -168,37 +179,46 @@
         for (LetterboxSurface surface : mSurfaces) {
             surface.remove();
         }
-        mBehind.remove();
+        mFullWindowSurface.remove();
     }
 
     /** Returns whether a call to {@link #applySurfaceChanges} would change the surface. */
     public boolean needsApplySurfaceChanges() {
+        if (useFullWindowSurface()) {
+            return mFullWindowSurface.needsApplySurfaceChanges();
+        }
         for (LetterboxSurface surface : mSurfaces) {
             if (surface.needsApplySurfaceChanges()) {
                 return true;
             }
         }
-        if (mAreCornersRounded.get() && mBehind.needsApplySurfaceChanges()) {
-            return true;
-        }
         return false;
     }
 
     public void applySurfaceChanges(SurfaceControl.Transaction t) {
-        for (LetterboxSurface surface : mSurfaces) {
-            surface.applySurfaceChanges(t);
-        }
-        if (mAreCornersRounded.get()) {
-            mBehind.applySurfaceChanges(t);
+        if (useFullWindowSurface()) {
+            mFullWindowSurface.applySurfaceChanges(t);
+
+            for (LetterboxSurface surface : mSurfaces) {
+                surface.remove();
+            }
         } else {
-            mBehind.remove();
+            for (LetterboxSurface surface : mSurfaces) {
+                surface.applySurfaceChanges(t);
+            }
+
+            mFullWindowSurface.remove();
         }
     }
 
     /** Enables touches to slide into other neighboring surfaces. */
     void attachInput(WindowState win) {
-        for (LetterboxSurface surface : mSurfaces) {
-            surface.attachInput(win);
+        if (useFullWindowSurface()) {
+            mFullWindowSurface.attachInput(win);
+        } else {
+            for (LetterboxSurface surface : mSurfaces) {
+                surface.attachInput(win);
+            }
         }
     }
 
@@ -208,20 +228,61 @@
                 surface.mInputInterceptor.mWindowHandle.displayId = displayId;
             }
         }
+        if (mFullWindowSurface.mInputInterceptor != null) {
+            mFullWindowSurface.mInputInterceptor.mWindowHandle.displayId = displayId;
+        }
     }
 
-    private static class InputInterceptor {
-        final InputChannel mClientChannel;
-        final InputWindowHandle mWindowHandle;
-        final InputEventReceiver mInputEventReceiver;
-        final WindowManagerService mWmService;
-        final IBinder mToken;
+    /**
+     * Returns {@code true} when using {@link #mFullWindowSurface} instead of {@link mSurfaces}.
+     */
+    private boolean useFullWindowSurface() {
+        return mAreCornersRounded.get() || mHasWallpaperBackgroundSupplier.get();
+    }
+
+    private final class TapEventReceiver extends InputEventReceiver {
+
+        private final GestureDetector mDoubleTapDetector;
+        private final DoubleTapListener mDoubleTapListener;
+
+        TapEventReceiver(InputChannel inputChannel, Context context) {
+            super(inputChannel, UiThread.getHandler().getLooper());
+            mDoubleTapListener = new DoubleTapListener();
+            mDoubleTapDetector = new GestureDetector(
+                    context, mDoubleTapListener, UiThread.getHandler());
+        }
+
+        @Override
+        public void onInputEvent(InputEvent event) {
+            final MotionEvent motionEvent = (MotionEvent) event;
+            finishInputEvent(event, mDoubleTapDetector.onTouchEvent(motionEvent));
+        }
+    }
+
+    private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onDoubleTapEvent(MotionEvent e) {
+            if (e.getAction() == MotionEvent.ACTION_UP) {
+                mDoubleTapCallback.accept((int) e.getX());
+                return true;
+            }
+            return false;
+        }
+    }
+
+    private final class InputInterceptor {
+
+        private final InputChannel mClientChannel;
+        private final InputWindowHandle mWindowHandle;
+        private final InputEventReceiver mInputEventReceiver;
+        private final WindowManagerService mWmService;
+        private final IBinder mToken;
 
         InputInterceptor(String namePrefix, WindowState win) {
             mWmService = win.mWmService;
             final String name = namePrefix + (win.mActivityRecord != null ? win.mActivityRecord : win);
             mClientChannel = mWmService.mInputManager.createInputChannel(name);
-            mInputEventReceiver = new SimpleInputReceiver(mClientChannel);
+            mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService.mContext);
 
             mToken = mClientChannel.getToken();
 
@@ -259,12 +320,6 @@
             mInputEventReceiver.dispose();
             mClientChannel.dispose();
         }
-
-        private static class SimpleInputReceiver extends InputEventReceiver {
-            SimpleInputReceiver(InputChannel inputChannel) {
-                super(inputChannel, UiThread.getHandler().getLooper());
-            }
-        }
     }
 
     private class LetterboxSurface {
@@ -308,6 +363,10 @@
             mInputInterceptor = new InputInterceptor("Letterbox_" + mType + "_", win);
         }
 
+        boolean isRemoved() {
+            return mSurface != null || mInputInterceptor != null;
+        }
+
         public void remove() {
             if (mSurface != null) {
                 mTransactionFactory.get().remove(mSurface).apply();
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 7174e68..cbb473c 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -17,11 +17,11 @@
 package com.android.server.wm;
 
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
 
 import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -54,6 +54,27 @@
     /** Using wallpaper as a background which can be blurred or dimmed with dark scrim. */
     static final int LETTERBOX_BACKGROUND_WALLPAPER = 3;
 
+    /**
+     * Enum for Letterbox reachability position types.
+     *
+     * <p>Order from left to right is important since it's used in {@link
+     * #movePositionForReachabilityToNextRightStop} and {@link
+     * #movePositionForReachabilityToNextLeftStop}.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({LETTERBOX_REACHABILITY_POSITION_LEFT, LETTERBOX_REACHABILITY_POSITION_CENTER,
+            LETTERBOX_REACHABILITY_POSITION_RIGHT})
+    @interface LetterboxReachabilityPosition {};
+
+    /** Letterboxed app window is aligned to the left side. */
+    static final int LETTERBOX_REACHABILITY_POSITION_LEFT = 0;
+
+    /** Letterboxed app window is positioned in the horizontal center. */
+    static final int LETTERBOX_REACHABILITY_POSITION_CENTER = 1;
+
+    /** Letterboxed app window is aligned to the right side. */
+    static final int LETTERBOX_REACHABILITY_POSITION_RIGHT = 2;
+
     final Context mContext;
 
     // Aspect ratio of letterbox for fixed orientation, values <=
@@ -64,7 +85,10 @@
     private int mLetterboxActivityCornersRadius;
 
     // Color for {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} letterbox background type.
-    private Color mLetterboxBackgroundColor;
+    @Nullable private Color mLetterboxBackgroundColorOverride;
+
+    // Color resource id for {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} letterbox background type.
+    @Nullable private Integer mLetterboxBackgroundColorResourceIdOverride;
 
     @LetterboxBackgroundType
     private int mLetterboxBackgroundType;
@@ -82,21 +106,43 @@
     // side of the screen and 1.0 to the right side.
     private float mLetterboxHorizontalPositionMultiplier;
 
-    LetterboxConfiguration(Context context) {
-        mContext = context;
-        mFixedOrientationLetterboxAspectRatio = context.getResources().getFloat(
+    // Default horizontal position the letterboxed app window when reachability is enabled and
+    // an app is fullscreen in landscape device orientatio.
+    // It is used as a starting point for mLetterboxPositionForReachability.
+    @LetterboxReachabilityPosition
+    private int mDefaultPositionForReachability;
+
+    // Whether reachability repositioning is allowed for letterboxed fullscreen apps in landscape
+    // device orientation.
+    private boolean mIsReachabilityEnabled;
+
+    // Horizontal position of a center of the letterboxed app window which is global to prevent
+    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
+    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
+    // LetterboxUiController#getHorizontalPositionMultiplier which is called from
+    // ActivityRecord#updateResolvedBoundsHorizontalPosition.
+    // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
+    // Overview after changing position in another app.
+    @LetterboxReachabilityPosition
+    private volatile int mLetterboxPositionForReachability;
+
+    LetterboxConfiguration(Context systemUiContext) {
+        mContext = systemUiContext;
+        mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
                 R.dimen.config_fixedOrientationLetterboxAspectRatio);
-        mLetterboxActivityCornersRadius = context.getResources().getInteger(
+        mLetterboxActivityCornersRadius = mContext.getResources().getInteger(
                 R.integer.config_letterboxActivityCornersRadius);
-        mLetterboxBackgroundColor = Color.valueOf(context.getResources().getColor(
-                R.color.config_letterboxBackgroundColor));
-        mLetterboxBackgroundType = readLetterboxBackgroundTypeFromConfig(context);
-        mLetterboxBackgroundWallpaperBlurRadius = context.getResources().getDimensionPixelSize(
+        mLetterboxBackgroundType = readLetterboxBackgroundTypeFromConfig(mContext);
+        mLetterboxBackgroundWallpaperBlurRadius = mContext.getResources().getDimensionPixelSize(
                 R.dimen.config_letterboxBackgroundWallpaperBlurRadius);
-        mLetterboxBackgroundWallpaperDarkScrimAlpha = context.getResources().getFloat(
+        mLetterboxBackgroundWallpaperDarkScrimAlpha = mContext.getResources().getFloat(
                 R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha);
-        mLetterboxHorizontalPositionMultiplier = context.getResources().getFloat(
+        mLetterboxHorizontalPositionMultiplier = mContext.getResources().getFloat(
                 R.dimen.config_letterboxHorizontalPositionMultiplier);
+        mIsReachabilityEnabled = mContext.getResources().getBoolean(
+                R.bool.config_letterboxIsReachabilityEnabled);
+        mDefaultPositionForReachability = readLetterboxReachabilityPositionFromConfig(mContext);
+        mLetterboxPositionForReachability = mDefaultPositionForReachability;
     }
 
     /**
@@ -105,12 +151,20 @@
      * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and
      * the framework implementation will be used to determine the aspect ratio.
      */
-    @VisibleForTesting
     void setFixedOrientationLetterboxAspectRatio(float aspectRatio) {
         mFixedOrientationLetterboxAspectRatio = aspectRatio;
     }
 
     /**
+     * Resets the aspect ratio of letterbox for fixed orientation to {@link
+     * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio}.
+     */
+    void resetFixedOrientationLetterboxAspectRatio() {
+        mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
+                com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio);
+    }
+
+    /**
      * Gets the aspect ratio of letterbox for fixed orientation.
      */
     float getFixedOrientationLetterboxAspectRatio() {
@@ -118,10 +172,29 @@
     }
 
     /**
+     * Overrides corners raidus for activities presented in the letterbox mode. If given value < 0,
+     * both it and a value of {@link
+     * com.android.internal.R.integer.config_letterboxActivityCornersRadius} will be ignored and
+     * and corners of the activity won't be rounded.
+     */
+    void setLetterboxActivityCornersRadius(int cornersRadius) {
+        mLetterboxActivityCornersRadius = cornersRadius;
+    }
+
+    /**
+     * Resets corners raidus for activities presented in the letterbox mode to {@link
+     * com.android.internal.R.integer.config_letterboxActivityCornersRadius}.
+     */
+    void resetLetterboxActivityCornersRadius() {
+        mLetterboxActivityCornersRadius = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_letterboxActivityCornersRadius);
+    }
+
+    /**
      * Whether corners of letterboxed activities are rounded.
      */
     boolean isLetterboxActivityCornersRounded() {
-        return getLetterboxActivityCornersRadius() > 0;
+        return getLetterboxActivityCornersRadius() != 0;
     }
 
     /**
@@ -132,12 +205,48 @@
     }
 
     /**
-     * Gets color of letterbox background which is  used when {@link
+     * Gets color of letterbox background which is used when {@link
      * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
      * fallback for other backfround types.
      */
     Color getLetterboxBackgroundColor() {
-        return mLetterboxBackgroundColor;
+        if (mLetterboxBackgroundColorOverride != null) {
+            return mLetterboxBackgroundColorOverride;
+        }
+        int colorId = mLetterboxBackgroundColorResourceIdOverride != null
+                ? mLetterboxBackgroundColorResourceIdOverride
+                : R.color.config_letterboxBackgroundColor;
+        // Query color dynamically because material colors extracted from wallpaper are updated
+        // when wallpaper is changed.
+        return Color.valueOf(mContext.getResources().getColor(colorId));
+    }
+
+
+    /**
+     * Sets color of letterbox background which is used when {@link
+     * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
+     * fallback for other backfround types.
+     */
+    void setLetterboxBackgroundColor(Color color) {
+        mLetterboxBackgroundColorOverride = color;
+    }
+
+    /**
+     * Sets color ID of letterbox background which is used when {@link
+     * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
+     * fallback for other backfround types.
+     */
+    void setLetterboxBackgroundColorResourceId(int colorId) {
+        mLetterboxBackgroundColorResourceIdOverride = colorId;
+    }
+
+    /**
+     * Resets color of letterbox background to {@link
+     * com.android.internal.R.color.config_letterboxBackgroundColor}.
+     */
+    void resetLetterboxBackgroundColor() {
+        mLetterboxBackgroundColorOverride = null;
+        mLetterboxBackgroundColorResourceIdOverride = null;
     }
 
     /**
@@ -149,6 +258,19 @@
         return mLetterboxBackgroundType;
     }
 
+    /** Sets letterbox background type. */
+    void setLetterboxBackgroundType(@LetterboxBackgroundType int backgroundType) {
+        mLetterboxBackgroundType = backgroundType;
+    }
+
+    /**
+     * Resets cletterbox background type to {@link
+     * com.android.internal.R.integer.config_letterboxBackgroundType}.
+     */
+    void resetLetterboxBackgroundType() {
+        mLetterboxBackgroundType = readLetterboxBackgroundTypeFromConfig(mContext);
+    }
+
     /** Returns a string representing the given {@link LetterboxBackgroundType}. */
     static String letterboxBackgroundTypeToString(
             @LetterboxBackgroundType int backgroundType) {
@@ -178,6 +300,27 @@
     }
 
     /**
+     * Overrides alpha of a black scrim shown over wallpaper for {@link
+     * #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link mLetterboxBackgroundType}.
+     *
+     * <p>If given value is < 0 or >= 1, both it and a value of {@link
+     * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha} are ignored
+     * and 0.0 (transparent) is instead.
+     */
+    void setLetterboxBackgroundWallpaperDarkScrimAlpha(float alpha) {
+        mLetterboxBackgroundWallpaperDarkScrimAlpha = alpha;
+    }
+
+    /**
+     * Resets alpha of a black scrim shown over wallpaper letterbox background to {@link
+     * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha}.
+     */
+    void resetLetterboxBackgroundWallpaperDarkScrimAlpha() {
+        mLetterboxBackgroundWallpaperDarkScrimAlpha = mContext.getResources().getFloat(
+                com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha);
+    }
+
+    /**
      * Gets alpha of a black scrim shown over wallpaper letterbox background.
      */
     float getLetterboxBackgroundWallpaperDarkScrimAlpha() {
@@ -185,6 +328,28 @@
     }
 
     /**
+     * Overrides blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in
+     * {@link mLetterboxBackgroundType}.
+     *
+     * <p> If given value <= 0, both it and a value of {@link
+     * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius} are ignored
+     * and 0 is used instead.
+     */
+    void setLetterboxBackgroundWallpaperBlurRadius(int radius) {
+        mLetterboxBackgroundWallpaperBlurRadius = radius;
+    }
+
+    /**
+     * Resets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link
+     * mLetterboxBackgroundType} to {@link
+     * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius}.
+     */
+    void resetLetterboxBackgroundWallpaperBlurRadius() {
+        mLetterboxBackgroundWallpaperBlurRadius = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius);
+    }
+
+    /**
      * Gets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link
      * mLetterboxBackgroundType}.
      */
@@ -197,12 +362,12 @@
      * in {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier}
      * or via an ADB command. 0 corresponds to the left side of the screen and 1 to the
      * right side.
-     *
-     * <p>This value can be outside of [0, 1] range so clients need to check and default to the
-     * central position (0.5).
      */
     float getLetterboxHorizontalPositionMultiplier() {
-        return mLetterboxHorizontalPositionMultiplier;
+        return (mLetterboxHorizontalPositionMultiplier < 0.0f
+                || mLetterboxHorizontalPositionMultiplier > 1.0f)
+                        // Default to central position if invalid value is provided.
+                        ? 0.5f : mLetterboxHorizontalPositionMultiplier;
     }
 
     /**
@@ -211,9 +376,129 @@
      * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier} are ignored and
      * central position (0.5) is used.
      */
-    @VisibleForTesting
     void setLetterboxHorizontalPositionMultiplier(float multiplier) {
         mLetterboxHorizontalPositionMultiplier = multiplier;
     }
 
+    /**
+     * Resets horizontal position of a center of the letterboxed app window to {@link
+     * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier}.
+     */
+    void resetLetterboxHorizontalPositionMultiplier() {
+        mLetterboxHorizontalPositionMultiplier = mContext.getResources().getFloat(
+                com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier);
+    }
+
+    /*
+     * Whether reachability repositioning is allowed for letterboxed fullscreen apps in landscape
+     * device orientation.
+     */
+    boolean getIsReachabilityEnabled() {
+        return mIsReachabilityEnabled;
+    }
+
+    /**
+     * Overrides whether reachability repositioning is allowed for letterboxed fullscreen apps in
+     * landscape device orientation.
+     */
+    void setIsReachabilityEnabled(boolean enabled) {
+        mIsReachabilityEnabled = enabled;
+    }
+
+    /**
+     * Resets whether reachability repositioning is allowed for letterboxed fullscreen apps in
+     * landscape device orientation to {@link R.bool.config_letterboxIsReachabilityEnabled}.
+     */
+    void resetIsReachabilityEnabled() {
+        mIsReachabilityEnabled = mContext.getResources().getBoolean(
+                R.bool.config_letterboxIsReachabilityEnabled);
+    }
+
+    /*
+     * Gets default horizontal position of the letterboxed app window when reachability is enabled.
+     * Specified in {@link R.integer.config_letterboxDefaultPositionForReachability} or via an ADB
+     * command.
+     */
+    @LetterboxReachabilityPosition
+    int getDefaultPositionForReachability() {
+        return mDefaultPositionForReachability;
+    }
+
+    /**
+     * Overrides default horizonal position of the letterboxed app window when reachability
+     * is enabled.
+     */
+    void setDefaultPositionForReachability(@LetterboxReachabilityPosition int position) {
+        mDefaultPositionForReachability = position;
+    }
+
+    /**
+     * Resets default horizontal position of the letterboxed app window when reachability is
+     * enabled to {@link R.integer.config_letterboxDefaultPositionForReachability}.
+     */
+    void resetDefaultPositionForReachability() {
+        mDefaultPositionForReachability = readLetterboxReachabilityPositionFromConfig(mContext);
+    }
+
+    @LetterboxReachabilityPosition
+    private static int readLetterboxReachabilityPositionFromConfig(Context context) {
+        int position = context.getResources().getInteger(
+                R.integer.config_letterboxDefaultPositionForReachability);
+        return position == LETTERBOX_REACHABILITY_POSITION_LEFT
+                    || position == LETTERBOX_REACHABILITY_POSITION_CENTER
+                    || position == LETTERBOX_REACHABILITY_POSITION_RIGHT
+                    ? position : LETTERBOX_REACHABILITY_POSITION_CENTER;
+    }
+
+    /*
+     * Gets horizontal position of a center of the letterboxed app window when reachability
+     * is enabled specified. 0 corresponds to the left side of the screen and 1 to the right side.
+     *
+     * <p>The position multiplier is changed after each double tap in the letterbox area.
+     */
+    float getHorizontalMultiplierForReachability() {
+        switch (mLetterboxPositionForReachability) {
+            case LETTERBOX_REACHABILITY_POSITION_LEFT:
+                return 0.0f;
+            case LETTERBOX_REACHABILITY_POSITION_CENTER:
+                return 0.5f;
+            case LETTERBOX_REACHABILITY_POSITION_RIGHT:
+                return 1.0f;
+            default:
+                throw new AssertionError(
+                    "Unexpected letterbox position type: " + mLetterboxPositionForReachability);
+        }
+    }
+
+    /** Returns a string representing the given {@link LetterboxReachabilityPosition}. */
+    static String letterboxReachabilityPositionToString(
+            @LetterboxReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_REACHABILITY_POSITION_LEFT:
+                return "LETTERBOX_REACHABILITY_POSITION_LEFT";
+            case LETTERBOX_REACHABILITY_POSITION_CENTER:
+                return "LETTERBOX_REACHABILITY_POSITION_CENTER";
+            case LETTERBOX_REACHABILITY_POSITION_RIGHT:
+                return "LETTERBOX_REACHABILITY_POSITION_RIGHT";
+            default:
+                throw new AssertionError(
+                    "Unexpected letterbox position type: " + position);
+        }
+    }
+
+    /**
+     * Changes letterbox position for reachability to the next available one on the right side.
+     */
+    void movePositionForReachabilityToNextRightStop() {
+        mLetterboxPositionForReachability = Math.min(
+                mLetterboxPositionForReachability + 1, LETTERBOX_REACHABILITY_POSITION_RIGHT);
+    }
+
+    /**
+     * Changes letterbox position for reachability to the next available one on the left side.
+     */
+    void movePositionForReachabilityToNextLeftStop() {
+        mLetterboxPositionForReachability = Math.max(mLetterboxPositionForReachability - 1, 0);
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index b6b8ad1..7d07357 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -16,8 +16,12 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 
+import static com.android.server.wm.ActivityRecord.computeAspectRatio;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
@@ -28,14 +32,20 @@
 
 import android.annotation.Nullable;
 import android.app.ActivityManager.TaskDescription;
+import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.Slog;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.RoundedCorner;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.WindowManager;
 
+import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
 
@@ -55,6 +65,10 @@
     private final LetterboxConfiguration mLetterboxConfiguration;
     private final ActivityRecord mActivityRecord;
 
+    // Taskbar expanded height. Used to determine whether to crop an app window to display rounded
+    // corners above the taskbar.
+    private float mExpandedTaskBarHeight;
+
     private boolean mShowWallpaperForLetterboxBackground;
 
     @Nullable
@@ -66,6 +80,8 @@
         // is created in its constructor. It shouldn't be used in this constructor but it's safe
         // to use it after since controller is only used in ActivityRecord.
         mActivityRecord = activityRecord;
+        mExpandedTaskBarHeight =
+                getResources().getDimensionPixelSize(R.dimen.taskbar_frame_height);
     }
 
     /** Cleans up {@link Letterbox} if it exists.*/
@@ -138,7 +154,8 @@
                         this::getLetterboxBackgroundColor,
                         this::hasWallpaperBackgroudForLetterbox,
                         this::getLetterboxWallpaperBlurRadius,
-                        this::getLetterboxWallpaperDarkScrimAlpha);
+                        this::getLetterboxWallpaperDarkScrimAlpha,
+                        this::handleDoubleTap);
                 mLetterbox.attachInput(w);
             }
             mActivityRecord.getPosition(mTmpPoint);
@@ -158,6 +175,85 @@
         }
     }
 
+    float getHorizontalPositionMultiplier(Configuration parentConfiguration) {
+        // Don't check resolved configuration because it may not be updated yet during
+        // configuration change.
+        return isReachabilityEnabled(parentConfiguration)
+                // Using the last global dynamic position to avoid "jumps" when moving
+                // between apps or activities.
+                ? mLetterboxConfiguration.getHorizontalMultiplierForReachability()
+                : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier();
+    }
+
+    float getFixedOrientationLetterboxAspectRatio(Configuration parentConfiguration) {
+        // Don't check resolved windowing mode because it may not be updated yet during
+        // configuration change.
+        if (!isReachabilityEnabled(parentConfiguration)) {
+            return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+        }
+
+        int dividerWindowWidth =
+                getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_thickness);
+        int dividerInsets =
+                getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_insets);
+        int dividerSize = dividerWindowWidth - dividerInsets * 2;
+
+        // Getting the same aspect ratio that apps get in split screen.
+        Rect bounds = new Rect(parentConfiguration.windowConfiguration.getAppBounds());
+        bounds.inset(dividerSize, /* dy */ 0);
+        bounds.right = bounds.centerX();
+
+        return computeAspectRatio(bounds);
+    }
+
+    Resources getResources() {
+        return mActivityRecord.mWmService.mContext.getResources();
+    }
+
+    private void handleDoubleTap(int x) {
+        if (!isReachabilityEnabled() || mActivityRecord.isInTransition()) {
+            return;
+        }
+
+        if (mLetterbox.getInnerFrame().left <= x && mLetterbox.getInnerFrame().right >= x) {
+            // Only react to clicks at the sides of the letterboxed app window.
+            return;
+        }
+
+        if (mLetterbox.getInnerFrame().left > x) {
+            // Moving to the next stop on the left side of the app window: right > center > left.
+            mLetterboxConfiguration.movePositionForReachabilityToNextLeftStop();
+        } else if (mLetterbox.getInnerFrame().right < x) {
+            // Moving to the next stop on the right side of the app window: left > center > right.
+            mLetterboxConfiguration.movePositionForReachabilityToNextRightStop();
+        }
+
+        // TODO(197549949): Add animation for transition.
+        mActivityRecord.recomputeConfiguration();
+    }
+
+    /**
+     * Whether reachability is enabled for an activity in the curren configuration.
+     *
+     * <p>Conditions that needs to be met:
+     * <ul>
+     *   <li>Activity is portrait-only.
+     *   <li>Fullscreen window in landscape device orientation.
+     *   <li>Reachability is enabled.
+     * </ul>
+     */
+    private boolean isReachabilityEnabled(Configuration parentConfiguration) {
+        return mLetterboxConfiguration.getIsReachabilityEnabled()
+                && parentConfiguration.windowConfiguration.getWindowingMode()
+                        == WINDOWING_MODE_FULLSCREEN
+                && parentConfiguration.orientation == ORIENTATION_LANDSCAPE
+                && mActivityRecord.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT;
+    }
+
+    private boolean isReachabilityEnabled() {
+        return isReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
+    }
+
     @VisibleForTesting
     boolean shouldShowLetterboxUi(WindowState mainWindow) {
         return isSurfaceReadyAndVisible(mainWindow) && mainWindow.areAppWindowBoundsLetterboxed()
@@ -219,31 +315,81 @@
     }
 
     private void updateRoundedCorners(WindowState mainWindow) {
-        int cornersRadius =
-                // Don't round corners if letterboxed only for display cutout.
-                shouldShowLetterboxUi(mainWindow)
-                                && !mainWindow.isLetterboxedForDisplayCutout()
-                        ? Math.max(0, mLetterboxConfiguration.getLetterboxActivityCornersRadius())
-                        : 0;
-        setCornersRadius(mainWindow, cornersRadius);
-    }
-
-    private void setCornersRadius(WindowState mainWindow, int cornersRadius) {
         final SurfaceControl windowSurface = mainWindow.getClientViewRootSurface();
         if (windowSurface != null && windowSurface.isValid()) {
             Transaction transaction = mActivityRecord.getSyncTransaction();
-            transaction.setCornerRadius(windowSurface, cornersRadius);
+
+            if (!isLetterboxedNotForDisplayCutout(mainWindow)
+                    || !mLetterboxConfiguration.isLetterboxActivityCornersRounded()) {
+                transaction
+                        .setWindowCrop(windowSurface, null)
+                        .setCornerRadius(windowSurface, 0);
+                return;
+            }
+
+            final InsetsState insetsState = mainWindow.getInsetsState();
+            final InsetsSource taskbarInsetsSource =
+                    insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+
+            Rect cropBounds = null;
+
+            // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
+            // an insets frame is equal to a navigation bar which shouldn't affect position of
+            // rounded corners since apps are expected to handle navigation bar inset.
+            // This condition checks whether the taskbar is visible.
+            if (taskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+                cropBounds = new Rect(mActivityRecord.getBounds());
+                // Activity bounds are in screen coordinates while (0,0) for activity's surface
+                // control is at the top left corner of an app window so offsetting bounds
+                // accordingly.
+                cropBounds.offsetTo(0, 0);
+                // Rounded cornerners should be displayed above the taskbar.
+                cropBounds.bottom =
+                        Math.min(cropBounds.bottom, taskbarInsetsSource.getFrame().top);
+                if (mActivityRecord.inSizeCompatMode()
+                        && mActivityRecord.getSizeCompatScale() < 1.0f) {
+                    cropBounds.scale(1.0f / mActivityRecord.getSizeCompatScale());
+                }
+            }
+
+            transaction
+                    .setWindowCrop(windowSurface, cropBounds)
+                    .setCornerRadius(windowSurface, getRoundedCorners(insetsState));
         }
     }
 
+    // Returns rounded corners radius based on override in
+    // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii.
+    // Device corners can be different on the right and left sides but we use the same radius
+    // for all corners for consistency and pick a minimal bottom one for consistency with a
+    // taskbar rounded corners.
+    private int getRoundedCorners(InsetsState insetsState) {
+        if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
+            return mLetterboxConfiguration.getLetterboxActivityCornersRadius();
+        }
+        return Math.min(
+                getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
+                getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
+    }
+
+    private int getInsetsStateCornerRadius(
+                InsetsState insetsState, @RoundedCorner.Position int position) {
+        RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
+        return corner == null ? 0 : corner.getRadius();
+    }
+
+    private boolean isLetterboxedNotForDisplayCutout(WindowState mainWindow) {
+        return shouldShowLetterboxUi(mainWindow)
+                && !mainWindow.isLetterboxedForDisplayCutout();
+    }
+
     private void updateWallpaperForLetterbox(WindowState mainWindow) {
         @LetterboxBackgroundType int letterboxBackgroundType =
                 mLetterboxConfiguration.getLetterboxBackgroundType();
         boolean wallpaperShouldBeShown =
                 letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER
-                        && shouldShowLetterboxUi(mainWindow)
                         // Don't use wallpaper as a background if letterboxed for display cutout.
-                        && !mainWindow.isLetterboxedForDisplayCutout()
+                        && isLetterboxedNotForDisplayCutout(mainWindow)
                         // Check that dark scrim alpha or blur radius are provided
                         && (getLetterboxWallpaperBlurRadius() > 0
                                 || getLetterboxWallpaperDarkScrimAlpha() > 0)
@@ -285,7 +431,7 @@
         }
 
         pw.println(prefix + "  letterboxReason=" + getLetterboxReasonString(mainWin));
-        pw.println(prefix + "  letterboxAspectRatio="
+        pw.println(prefix + "  activityAspectRatio="
                 + mActivityRecord.computeAspectRatio(mActivityRecord.getBounds()));
 
         boolean shouldShowLetterboxUi = shouldShowLetterboxUi(mainWin);
@@ -299,6 +445,8 @@
         pw.println(prefix + "  letterboxBackgroundType="
                 + letterboxBackgroundTypeToString(
                         mLetterboxConfiguration.getLetterboxBackgroundType()));
+        pw.println(prefix + "  letterboxCornerRadius="
+                + getRoundedCorners(mainWin.getInsetsState()));
         if (mLetterboxConfiguration.getLetterboxBackgroundType()
                 == LETTERBOX_BACKGROUND_WALLPAPER) {
             pw.println(prefix + "  isLetterboxWallpaperBlurSupported="
@@ -308,8 +456,13 @@
             pw.println(prefix + "  letterboxBackgroundWallpaperBlurRadius="
                     + getLetterboxWallpaperBlurRadius());
         }
+
+        pw.println(prefix + "  isReachabilityEnabled=" + isReachabilityEnabled());
         pw.println(prefix + "  letterboxHorizontalPositionMultiplier="
-                + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier());
+                + getHorizontalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
+        pw.println(prefix + "  fixedOrientationLetterboxAspectRatio="
+                + getFixedOrientationLetterboxAspectRatio(
+                        mActivityRecord.getParent().getConfiguration()));
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/LocalAnimationAdapter.java b/services/core/java/com/android/server/wm/LocalAnimationAdapter.java
index 520bd8b..a3eb980 100644
--- a/services/core/java/com/android/server/wm/LocalAnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/LocalAnimationAdapter.java
@@ -19,6 +19,7 @@
 import static com.android.server.wm.AnimationAdapterProto.LOCAL;
 import static com.android.server.wm.LocalAnimationAdapterProto.ANIMATION_SPEC;
 
+import android.annotation.NonNull;
 import android.os.SystemClock;
 import android.util.proto.ProtoOutputStream;
 import android.view.SurfaceControl;
@@ -51,7 +52,7 @@
 
     @Override
     public void startAnimation(SurfaceControl animationLeash, Transaction t,
-            @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+            @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
         mAnimator.startAnimation(mSpec, animationLeash, t,
                 () -> finishCallback.onAnimationFinished(type, this));
     }
diff --git a/services/core/java/com/android/server/wm/LocaleOverlayHelper.java b/services/core/java/com/android/server/wm/LocaleOverlayHelper.java
new file mode 100644
index 0000000..a1a01db
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LocaleOverlayHelper.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.os.LocaleList;
+
+import java.util.Locale;
+
+/**
+ * Static utilities to overlay locales on top of another localeList.
+ *
+ * <p>This is used to overlay application-specific locales in
+ *  {@link com.android.server.wm.ActivityTaskManagerInternal.PackageConfigurationUpdater} on top of
+ *  system locales.
+ */
+final class LocaleOverlayHelper {
+
+    /**
+     * Combines the overlay locales and base locales.
+     * @return the combined {@link LocaleList} if the overlay locales is not empty/null else
+     * returns the empty/null LocaleList.
+     */
+    static LocaleList combineLocalesIfOverlayExists(LocaleList overlayLocales,
+            LocaleList baseLocales) {
+        if (overlayLocales == null || overlayLocales.isEmpty()) {
+            return overlayLocales;
+        }
+        return combineLocales(overlayLocales, baseLocales);
+    }
+
+    /**
+     * Creates a combined {@link LocaleList} by placing overlay locales before base locales and
+     * dropping duplicates from the base locales.
+     */
+    private static LocaleList combineLocales(LocaleList overlayLocales, LocaleList baseLocales) {
+        Locale[] combinedLocales = new Locale[overlayLocales.size() + baseLocales.size()];
+        for (int i = 0; i < overlayLocales.size(); i++) {
+            combinedLocales[i] = overlayLocales.get(i);
+        }
+        for (int i = 0; i < baseLocales.size(); i++) {
+            combinedLocales[i + overlayLocales.size()] = baseLocales.get(i);
+        }
+        // Constructor of {@link LocaleList} removes duplicates
+        return new LocaleList(combinedLocales);
+    }
+
+
+}
diff --git a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
index e50dc51..7abf3b8 100644
--- a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
+++ b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 
+import android.annotation.NonNull;
 import android.view.SurfaceControl;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
@@ -144,7 +145,7 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-                int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+                int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
             super.startAnimation(animationLeash, t, type, finishCallback);
             if (mParent != null && mParent.isValid()) {
                 t.reparent(animationLeash, mParent);
diff --git a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
index d230936..9f28509 100644
--- a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
@@ -27,6 +27,7 @@
 import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.util.proto.ProtoOutputStream;
@@ -138,14 +139,14 @@
         mTarget = new RemoteAnimationTarget(-1, -1, getLeash(), false,
                 new Rect(), null, mWindowContainer.getPrefixOrderIndex(),
                 mWindowContainer.getLastSurfacePosition(), mWindowContainer.getBounds(), null,
-                mWindowContainer.getWindowConfiguration(), true, null, null, null,
+                mWindowContainer.getWindowConfiguration(), true, null, null, null, false,
                 mWindowContainer.getWindowType());
         return mTarget;
     }
 
     @Override
     public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-            int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+            int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation");
         mCapturedLeash = animationLeash;
         mCapturedLeashFinishCallback = finishCallback;
diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java
index 1552a96..081a53e 100644
--- a/services/core/java/com/android/server/wm/PackageConfigPersister.java
+++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java
@@ -21,6 +21,7 @@
 
 import android.annotation.NonNull;
 import android.os.Environment;
+import android.os.LocaleList;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -54,12 +55,14 @@
     private static final String TAG_CONFIG = "config";
     private static final String ATTR_PACKAGE_NAME = "package_name";
     private static final String ATTR_NIGHT_MODE = "night_mode";
+    private static final String ATTR_LOCALES = "locale_list";
 
     private static final String PACKAGE_DIRNAME = "package_configs";
     private static final String SUFFIX_FILE_NAME = "_config.xml";
 
     private final PersisterQueue mPersisterQueue;
     private final Object mLock = new Object();
+    private final ActivityTaskManagerService mAtm;
 
     @GuardedBy("mLock")
     private final SparseArray<HashMap<String, PackageConfigRecord>> mPendingWrite =
@@ -72,8 +75,9 @@
         return new File(Environment.getDataSystemCeDirectory(userId), PACKAGE_DIRNAME);
     }
 
-    PackageConfigPersister(PersisterQueue queue) {
+    PackageConfigPersister(PersisterQueue queue, ActivityTaskManagerService atm) {
         mPersisterQueue = queue;
+        mAtm = atm;
     }
 
     @GuardedBy("mLock")
@@ -100,7 +104,8 @@
                     final TypedXmlPullParser in = Xml.resolvePullParser(is);
                     int event;
                     String packageName = null;
-                    int nightMode = MODE_NIGHT_AUTO;
+                    Integer nightMode = null;
+                    LocaleList locales = null;
                     while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
                             && event != XmlPullParser.END_TAG) {
                         final String name = in.getName();
@@ -120,6 +125,9 @@
                                         case ATTR_NIGHT_MODE:
                                             nightMode = Integer.parseInt(attrValue);
                                             break;
+                                        case ATTR_LOCALES:
+                                            locales = LocaleList.forLanguageTags(attrValue);
+                                            break;
                                     }
                                 }
                             }
@@ -130,6 +138,7 @@
                         final PackageConfigRecord initRecord =
                                 findRecordOrCreate(mModified, packageName, userId);
                         initRecord.mNightMode = nightMode;
+                        initRecord.mLocales = locales;
                         if (DEBUG) {
                             Slog.d(TAG, "loadPackages: load one package " + initRecord);
                         }
@@ -155,20 +164,28 @@
                         "updateConfigIfNeeded record " + container + " find? " + modifiedRecord);
             }
             if (modifiedRecord != null) {
-                container.setOverrideNightMode(modifiedRecord.mNightMode);
+                container.applyAppSpecificConfig(modifiedRecord.mNightMode,
+                        LocaleOverlayHelper.combineLocalesIfOverlayExists(
+                        modifiedRecord.mLocales, mAtm.getGlobalConfiguration().getLocales()));
             }
         }
     }
 
     @GuardedBy("mLock")
     void updateFromImpl(String packageName, int userId,
-            ActivityTaskManagerService.PackageConfigurationUpdaterImpl impl) {
+            PackageConfigurationUpdaterImpl impl) {
         synchronized (mLock) {
             PackageConfigRecord record = findRecordOrCreate(mModified, packageName, userId);
-            record.mNightMode = impl.getNightMode();
-
-            if (record.isResetNightMode()) {
-                removePackage(record.mName, record.mUserId);
+            if (impl.getNightMode() != null) {
+                record.mNightMode = impl.getNightMode();
+            }
+            if (impl.getLocales() != null) {
+                record.mLocales = impl.getLocales();
+            }
+            if ((record.mNightMode == null || record.isResetNightMode())
+                    && (record.mLocales == null || record.mLocales.isEmpty())) {
+                // if all values default to system settings, we can remove the package.
+                removePackage(packageName, userId);
             } else {
                 final PackageConfigRecord pendingRecord =
                         findRecord(mPendingWrite, record.mName, record.mUserId);
@@ -179,10 +196,11 @@
                 } else {
                     writeRecord = pendingRecord;
                 }
-                if (writeRecord.mNightMode == record.mNightMode) {
+
+                if (!updateNightMode(record, writeRecord) && !updateLocales(record, writeRecord)) {
                     return;
                 }
-                writeRecord.mNightMode = record.mNightMode;
+
                 if (DEBUG) {
                     Slog.d(TAG, "PackageConfigUpdater save config " + writeRecord);
                 }
@@ -191,6 +209,22 @@
         }
     }
 
+    private boolean updateNightMode(PackageConfigRecord record, PackageConfigRecord writeRecord) {
+        if (record.mNightMode == null || record.mNightMode.equals(writeRecord.mNightMode)) {
+            return false;
+        }
+        writeRecord.mNightMode = record.mNightMode;
+        return true;
+    }
+
+    private boolean updateLocales(PackageConfigRecord record, PackageConfigRecord writeRecord) {
+        if (record.mLocales == null || record.mLocales.equals(writeRecord.mLocales)) {
+            return false;
+        }
+        writeRecord.mLocales = record.mLocales;
+        return true;
+    }
+
     @GuardedBy("mLock")
     void removeUser(int userId) {
         synchronized (mLock) {
@@ -210,7 +244,7 @@
     @GuardedBy("mLock")
     void onPackageUninstall(String packageName) {
         synchronized (mLock) {
-            for (int i = mModified.size() - 1; i > 0; i--) {
+            for (int i = mModified.size() - 1; i >= 0; i--) {
                 final int userId = mModified.keyAt(i);
                 removePackage(packageName, userId);
             }
@@ -238,11 +272,30 @@
         }
     }
 
+    /**
+     * Retrieves and returns application configuration from persisted records if it exists, else
+     * returns null.
+     */
+    ActivityTaskManagerInternal.PackageConfig findPackageConfiguration(String packageName,
+            int userId) {
+        synchronized (mLock) {
+            PackageConfigRecord packageConfigRecord = findRecord(mModified, packageName, userId);
+            if (packageConfigRecord == null) {
+                Slog.w(TAG, "App-specific configuration not found for packageName: " + packageName
+                        + " and userId: " + userId);
+                return null;
+            }
+            return new ActivityTaskManagerInternal.PackageConfig(
+                    packageConfigRecord.mNightMode, packageConfigRecord.mLocales);
+        }
+    }
+
     // store a changed data so we don't need to get the process
     static class PackageConfigRecord {
         final String mName;
         final int mUserId;
-        int mNightMode;
+        Integer mNightMode;
+        LocaleList mLocales;
 
         PackageConfigRecord(String name, int userId) {
             mName = name;
@@ -256,7 +309,7 @@
         @Override
         public String toString() {
             return "PackageConfigRecord package name: " + mName + " userId " + mUserId
-                    + " nightMode " + mNightMode;
+                    + " nightMode " + mNightMode + " locales " + mLocales;
         }
     }
 
@@ -369,7 +422,13 @@
             }
             xmlSerializer.startTag(null, TAG_CONFIG);
             xmlSerializer.attribute(null, ATTR_PACKAGE_NAME, mRecord.mName);
-            xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode);
+            if (mRecord.mNightMode != null) {
+                xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode);
+            }
+            if (mRecord.mLocales != null) {
+                xmlSerializer.attribute(null, ATTR_LOCALES, mRecord.mLocales
+                        .toLanguageTags());
+            }
             xmlSerializer.endTag(null, TAG_CONFIG);
             xmlSerializer.endDocument();
             xmlSerializer.flush();
diff --git a/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java b/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java
new file mode 100644
index 0000000..8bbcf1f
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.LocaleList;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import java.util.Optional;
+
+/**
+ * An implementation of {@link ActivityTaskManagerInternal.PackageConfigurationUpdater}.
+ */
+final class PackageConfigurationUpdaterImpl implements
+        ActivityTaskManagerInternal.PackageConfigurationUpdater {
+    private static final String TAG = "PackageConfigurationUpdaterImpl";
+    private final Optional<Integer> mPid;
+    private Integer mNightMode;
+    private LocaleList mLocales;
+    private String mPackageName;
+    private int mUserId;
+    private ActivityTaskManagerService mAtm;
+
+    PackageConfigurationUpdaterImpl(int pid, ActivityTaskManagerService atm) {
+        mPid = Optional.of(pid);
+        mAtm = atm;
+    }
+
+    PackageConfigurationUpdaterImpl(String packageName, int userId,
+            ActivityTaskManagerService atm) {
+        mPackageName = packageName;
+        mUserId = userId;
+        mAtm = atm;
+        mPid = Optional.empty();
+    }
+
+    @Override
+    public ActivityTaskManagerInternal.PackageConfigurationUpdater setNightMode(int nightMode) {
+        synchronized (this) {
+            mNightMode = nightMode;
+        }
+        return this;
+    }
+
+    @Override
+    public ActivityTaskManagerInternal.PackageConfigurationUpdater
+            setLocales(LocaleList locales) {
+        synchronized (this) {
+            mLocales = locales;
+        }
+        return this;
+    }
+
+    @Override
+    public void commit() {
+        synchronized (this) {
+            synchronized (mAtm.mGlobalLock) {
+                final long ident = Binder.clearCallingIdentity();
+                try {
+                    final int uid;
+                    if (mPid.isPresent()) {
+                        WindowProcessController wpc = mAtm.mProcessMap.getProcess(mPid.get());
+                        if (wpc == null) {
+                            Slog.w(TAG, "commit: Override application configuration failed: "
+                                    + "cannot find pid " + mPid);
+                            return;
+                        }
+                        uid = wpc.mUid;
+                        mUserId = wpc.mUserId;
+                        mPackageName = wpc.mInfo.packageName;
+                    } else {
+                        uid = mAtm.getPackageManagerInternalLocked().getPackageUid(mPackageName,
+                                /* flags = */ PackageManager.MATCH_ALL, mUserId);
+                        if (uid < 0) {
+                            Slog.w(TAG, "commit: update of application configuration failed: "
+                                    + "userId or packageName not valid " + mUserId);
+                            return;
+                        }
+                    }
+                    updateConfig(uid, mPackageName);
+                    mAtm.mPackageConfigPersister.updateFromImpl(mPackageName, mUserId, this);
+
+                } finally {
+                    Binder.restoreCallingIdentity(ident);
+                }
+            }
+        }
+    }
+
+    private void updateConfig(int uid, String packageName) {
+        final ArraySet<WindowProcessController> processes = mAtm.mProcessMap.getProcesses(uid);
+        if (processes == null) return;
+        for (int i = processes.size() - 1; i >= 0; i--) {
+            final WindowProcessController wpc = processes.valueAt(i);
+            if (!wpc.mInfo.packageName.equals(packageName)) continue;
+            LocaleList localesOverride = LocaleOverlayHelper.combineLocalesIfOverlayExists(
+                    mLocales, mAtm.getGlobalConfiguration().getLocales());
+            wpc.applyAppSpecificConfig(mNightMode, localesOverride);
+            wpc.updateAppSpecificSettingsForAllActivities(mNightMode, localesOverride);
+        }
+    }
+
+    Integer getNightMode() {
+        return mNightMode;
+    }
+
+    LocaleList getLocales() {
+        return mLocales;
+    }
+}
diff --git a/services/core/java/com/android/server/wm/PinnedTaskController.java b/services/core/java/com/android/server/wm/PinnedTaskController.java
index 7e95e7d..b4963c5 100644
--- a/services/core/java/com/android/server/wm/PinnedTaskController.java
+++ b/services/core/java/com/android/server/wm/PinnedTaskController.java
@@ -211,7 +211,9 @@
         }
         mFreezingTaskConfig = true;
         mDestRotatedBounds = new Rect(bounds);
-        continueOrientationChange();
+        if (!mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
+            continueOrientationChange();
+        }
     }
 
     /**
@@ -317,15 +319,11 @@
     }
 
     /** Resets the states which were used to perform fixed rotation with PiP task. */
-    void onCancelFixedRotationTransform(Task task) {
+    void onCancelFixedRotationTransform() {
         mFreezingTaskConfig = false;
         mDeferOrientationChanging = false;
         mDestRotatedBounds = null;
         mPipTransaction = null;
-        if (!task.isOrganized()) {
-            // Force clearing Task#mForceNotOrganized because the display didn't rotate.
-            task.onConfigurationChanged(task.getParent().getConfiguration());
-        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
new file mode 100644
index 0000000..11a27c5
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_270;
+
+import android.hardware.display.DisplayManagerInternal;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.DisplayInfo;
+import android.view.Surface;
+
+import java.util.Set;
+
+/**
+ * Maintains a map of possible {@link DisplayInfo} for displays and states that may be encountered
+ * on a device. This is not guaranteed to include all possible device states for all displays.
+ *
+ * By 'possible', this class only handles device states for displays and display groups it is
+ * currently aware of. It can not handle all eventual states the system may enter, for example, if
+ * an external display is added, or a new display is added to the group.
+ */
+public class PossibleDisplayInfoMapper {
+    private static final String TAG = "PossibleDisplayInfoMapper";
+    private static final boolean DEBUG = false;
+
+    private final DisplayManagerInternal mDisplayManagerInternal;
+
+    /**
+     * Map of all logical displays, indexed by logical display id.
+     * Each logical display has multiple entries, one for each possible rotation and device
+     * state.
+     *
+     * Emptied and re-calculated when a display is added, removed, or changed.
+     */
+    private final SparseArray<Set<DisplayInfo>> mDisplayInfos = new SparseArray<>();
+
+    PossibleDisplayInfoMapper(DisplayManagerInternal displayManagerInternal) {
+        mDisplayManagerInternal = displayManagerInternal;
+    }
+
+
+    /**
+     * Returns, for the given displayId, a set of display infos. Set contains the possible rotations
+     * for each supported device state.
+     */
+    public Set<DisplayInfo> getPossibleDisplayInfos(int displayId) {
+        // Update display infos before returning, since any cached values would have been removed
+        // in response to any display event. This model avoids re-computing the cache for every
+        // display change event (which occurs extremely frequently in the normal usage of the
+        // device).
+        updatePossibleDisplayInfos(displayId);
+        if (!mDisplayInfos.contains(displayId)) {
+            return new ArraySet<>();
+        }
+        return Set.copyOf(mDisplayInfos.get(displayId));
+    }
+
+    /**
+     * Updates the possible {@link DisplayInfo}s for the given display, by calculating the
+     * DisplayInfo for each rotation across supported device states.
+     */
+    public void updatePossibleDisplayInfos(int displayId) {
+        Set<DisplayInfo> displayInfos = mDisplayManagerInternal.getPossibleDisplayInfo(displayId);
+        if (DEBUG) {
+            Slog.v(TAG, "updatePossibleDisplayInfos, calculate rotations for given DisplayInfo "
+                    + displayInfos.size() + " on display " + displayId);
+        }
+        updateDisplayInfos(displayInfos);
+    }
+
+    /**
+     * For the given displayId, removes all possible {@link DisplayInfo}.
+     */
+    public void removePossibleDisplayInfos(int displayId) {
+        if (DEBUG && mDisplayInfos.get(displayId) != null) {
+            Slog.v(TAG, "onDisplayRemoved, remove all DisplayInfo (" + mDisplayInfos.get(
+                    displayId).size() + ") with id " + displayId);
+        }
+        mDisplayInfos.remove(displayId);
+    }
+
+    private void updateDisplayInfos(Set<DisplayInfo> displayInfos) {
+        // Empty out cache before re-computing.
+        mDisplayInfos.clear();
+        DisplayInfo[] originalDisplayInfos = new DisplayInfo[displayInfos.size()];
+        displayInfos.toArray(originalDisplayInfos);
+        // Iterate over each logical display layout for the current state.
+        Set<DisplayInfo> rotatedDisplayInfos;
+        for (DisplayInfo di : originalDisplayInfos) {
+            rotatedDisplayInfos = new ArraySet<>();
+            // Calculate all possible rotations for each logical display.
+            for (int rotation = ROTATION_0; rotation <= ROTATION_270; rotation++) {
+                rotatedDisplayInfos.add(applyRotation(di, rotation));
+            }
+            // Combine all results under the logical display id.
+            Set<DisplayInfo> priorDisplayInfos = mDisplayInfos.get(di.displayId, new ArraySet<>());
+            priorDisplayInfos.addAll(rotatedDisplayInfos);
+            mDisplayInfos.put(di.displayId, priorDisplayInfos);
+        }
+    }
+
+    private static DisplayInfo applyRotation(DisplayInfo displayInfo,
+            @Surface.Rotation int rotation) {
+        DisplayInfo updatedDisplayInfo = new DisplayInfo();
+        updatedDisplayInfo.copyFrom(displayInfo);
+        // Apply rotations before updating width and height
+        updatedDisplayInfo.roundedCorners = updatedDisplayInfo.roundedCorners.rotate(rotation,
+                updatedDisplayInfo.logicalWidth, updatedDisplayInfo.logicalHeight);
+        updatedDisplayInfo.displayCutout =
+                DisplayContent.calculateDisplayCutoutForRotationAndDisplaySizeUncached(
+                        updatedDisplayInfo.displayCutout, rotation, updatedDisplayInfo.logicalWidth,
+                        updatedDisplayInfo.logicalHeight).getDisplayCutout();
+
+        updatedDisplayInfo.rotation = rotation;
+        final int naturalWidth = updatedDisplayInfo.getNaturalWidth();
+        final int naturalHeight = updatedDisplayInfo.getNaturalHeight();
+        updatedDisplayInfo.logicalWidth = naturalWidth;
+        updatedDisplayInfo.logicalHeight = naturalHeight;
+        return updatedDisplayInfo;
+    }
+}
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 455f568..dca0bbd 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -1108,13 +1108,15 @@
         }
 
         if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: trimming tasks for " + task);
-        removeForAddTask(task);
+        final int removedIndex = removeForAddTask(task);
 
         task.inRecents = true;
         if (!isAffiliated || needAffiliationFix) {
             // If this is a simple non-affiliated task, or we had some failure trying to
             // handle it as part of an affilated task, then just place it at the top.
-            mTasks.add(0, task);
+            // But if the list is frozen, adding the task to the removed index to keep the order.
+            int indexToAdd = mFreezeTaskListReordering && removedIndex != -1 ? removedIndex : 0;
+            mTasks.add(indexToAdd, task);
             notifyTaskAdded(task);
             if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: adding " + task);
         } else if (isAffiliated) {
@@ -1344,7 +1346,8 @@
                     + " activityType=" + task.getActivityType()
                     + " windowingMode=" + task.getWindowingMode()
                     + " isAlwaysOnTopWhenVisible=" + task.isAlwaysOnTopWhenVisible()
-                    + " intentFlags=" + task.getBaseIntent().getFlags());
+                    + " intentFlags=" + task.getBaseIntent().getFlags()
+                    + " isEmbedded=" + task.isEmbedded());
         }
 
         switch (task.getActivityType()) {
@@ -1390,6 +1393,11 @@
             return false;
         }
 
+        // Ignore the task if it is a embedded task
+        if (task.isEmbedded()) {
+            return false;
+        }
+
         return true;
     }
 
@@ -1482,14 +1490,14 @@
      * If needed, remove oldest existing entries in recents that are for the same kind
      * of task as the given one.
      */
-    private void removeForAddTask(Task task) {
+    private int removeForAddTask(Task task) {
         // The adding task will be in recents so it is not hidden.
         mHiddenTasks.remove(task);
 
         final int removeIndex = findRemoveIndexForAddTask(task);
         if (removeIndex == -1) {
             // Nothing to trim
-            return;
+            return removeIndex;
         }
 
         // There is a similar task that will be removed for the addition of {@param task}, but it
@@ -1511,6 +1519,7 @@
             }
         }
         notifyTaskPersisterLocked(removedTask, false /* flush */);
+        return removeIndex;
     }
 
     /**
@@ -1518,11 +1527,6 @@
      * list (if any).
      */
     private int findRemoveIndexForAddTask(Task task) {
-        if (mFreezeTaskListReordering) {
-            // Defer removing tasks due to the addition of new tasks until the task list is unfrozen
-            return -1;
-        }
-
         final int recentsCount = mTasks.size();
         final Intent intent = task.intent;
         final boolean document = intent != null && intent.isDocument();
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index b1bdc11..ee05523 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -25,6 +25,8 @@
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
 import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
@@ -123,6 +125,11 @@
                 ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "Updated config=%s",
                         targetActivity.getConfiguration());
             }
+        } else if (mDefaultTaskDisplayArea.getActivity(
+                ActivityRecord::occludesParent, false /* traverseTopToBottom */) == null) {
+            // Skip because none of above activities can occlude the target activity. The preload
+            // should be done silently in background without being visible.
+            return;
         } else {
             // Create the activity record. Because the activity is invisible, this doesn't really
             // start the client.
@@ -149,8 +156,7 @@
 
         // Invisible activity should be stopped. If the recents activity is alive and its doesn't
         // need to relaunch by current configuration, then it may be already in stopped state.
-        if (!targetActivity.isState(Task.ActivityState.STOPPING,
-                Task.ActivityState.STOPPED)) {
+        if (!targetActivity.isState(STOPPING, STOPPED)) {
             // Add to stopping instead of stop immediately. So the client has the chance to perform
             // traversal in non-stopped state (ViewRootImpl.mStopped) that would initialize more
             // things (e.g. the measure can be done earlier). The actual stop will be performed when
@@ -164,6 +170,13 @@
         ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "startRecentsActivity(): intent=%s", mTargetIntent);
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "RecentsAnimation#startRecentsActivity");
 
+        // Cancel any existing recents animation running synchronously (do not hold the
+        // WM lock) before starting the newly requested recents animation as they can not coexist
+        if (mWindowManager.getRecentsAnimationController() != null) {
+            mWindowManager.getRecentsAnimationController().forceCancelAnimation(
+                    REORDER_MOVE_TO_ORIGINAL_POSITION, "startRecentsActivity");
+        }
+
         // If the activity is associated with the root recents task, then try and get that first
         Task targetRootTask = mDefaultTaskDisplayArea.getRootTask(WINDOWING_MODE_UNDEFINED,
                 mTargetActivityType);
@@ -237,12 +250,7 @@
             targetActivity.intent.replaceExtras(mTargetIntent);
 
             // Fetch all the surface controls and pass them to the client to get the animation
-            // started. Cancel any existing recents animation running synchronously (do not hold the
-            // WM lock)
-            if (mWindowManager.getRecentsAnimationController() != null) {
-                mWindowManager.getRecentsAnimationController().forceCancelAnimation(
-                        REORDER_MOVE_TO_ORIGINAL_POSITION, "startRecentsActivity");
-            }
+            // started
             mWindowManager.initializeRecentsAnimation(mTargetActivityType, recentsAnimationRunner,
                     this, mDefaultTaskDisplayArea.getDisplayId(),
                     mTaskSupervisor.mRecentTasks.getRecentTaskIds(), targetActivity);
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
index e346e3e..67b3ec8 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java
@@ -123,6 +123,7 @@
     private final int mDisplayId;
     private boolean mWillFinishToHome = false;
     private final Runnable mFailsafeRunnable = this::onFailsafe;
+    private Runnable mCheckRotationAfterCleanup;
 
     // The recents component app token that is shown behind the visibile tasks
     private ActivityRecord mTargetActivityRecord;
@@ -168,8 +169,9 @@
      */
     final AppTransitionListener mAppTransitionListener = new AppTransitionListener() {
         @Override
-        public int onAppTransitionStartingLocked(boolean keyguardGoingAway, long duration,
-                long statusBarAnimationStartTime, long statusBarAnimationDuration) {
+        public int onAppTransitionStartingLocked(boolean keyguardGoingAway,
+                boolean keyguardOccluding, long duration, long statusBarAnimationStartTime,
+                long statusBarAnimationDuration) {
             continueDeferredCancel();
             return 0;
         }
@@ -581,7 +583,7 @@
                 contentInsets = targetAppMainWindow
                         .getInsetsStateWithVisibilityOverride()
                         .calculateInsets(mTargetActivityRecord.getBounds(), Type.systemBars(),
-                                false /* ignoreVisibility */);
+                                false /* ignoreVisibility */).toRect();
             } else {
                 // If the window for the activity had not yet been created, use the display insets.
                 mService.getStableInsets(mDisplayId, mTmpRect);
@@ -792,7 +794,7 @@
 
     private RemoteAnimationTarget[] createWallpaperAnimations() {
         ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "createWallpaperAnimations()");
-        return WallpaperAnimationAdapter.startWallpaperAnimations(mService, 0L, 0L,
+        return WallpaperAnimationAdapter.startWallpaperAnimations(mDisplayContent, 0L, 0L,
                 adapter -> {
                     synchronized (mService.mGlobalLock) {
                         // If the wallpaper animation is canceled, continue with the recents
@@ -921,6 +923,24 @@
     }
 
     /**
+     * If the display rotation change is ignored while recents animation is running, make sure that
+     * the pending rotation change will be applied after the animation finishes.
+     */
+    void setCheckRotationAfterCleanup() {
+        if (mCheckRotationAfterCleanup != null) return;
+        mCheckRotationAfterCleanup = () -> {
+            synchronized (mService.mGlobalLock) {
+                if (mDisplayContent.getDisplayRotation()
+                        .updateRotationAndSendNewConfigIfChanged()) {
+                    if (mTargetActivityRecord != null) {
+                        mTargetActivityRecord.finishFixedRotationTransform();
+                    }
+                }
+            }
+        };
+    }
+
+    /**
      * @return Whether we should defer the cancel from a root task order change until the next app
      * transition.
      */
@@ -1007,6 +1027,10 @@
         if (mStatusBar != null) {
             mStatusBar.onRecentsAnimationStateChanged(false /* running */);
         }
+        if (mCheckRotationAfterCleanup != null) {
+            mService.mH.post(mCheckRotationAfterCleanup);
+            mCheckRotationAfterCleanup = null;
+        }
     }
 
     void scheduleFailsafe() {
@@ -1102,6 +1126,13 @@
         return mTargetActivityRecord.findMainWindow();
     }
 
+    DisplayArea getTargetAppDisplayArea() {
+        if (mTargetActivityRecord == null) {
+            return null;
+        }
+        return mTargetActivityRecord.getDisplayArea();
+    }
+
     boolean isAnimatingTask(Task task) {
         for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
             if (task == mPendingAnimations.get(i).mTask) {
@@ -1192,7 +1223,7 @@
                 return null;
             }
             final Rect insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets(
-                    mBounds, Type.systemBars(), false /* ignoreVisibility */);
+                    mBounds, Type.systemBars(), false /* ignoreVisibility */).toRect();
             InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets());
             final int mode = topApp.getActivityType() == mTargetActivityType
                     ? MODE_OPENING
@@ -1201,7 +1232,8 @@
                     !topApp.fillsParent(), new Rect(),
                     insets, mTask.getPrefixOrderIndex(), new Point(mBounds.left, mBounds.top),
                     mLocalBounds, mBounds, mTask.getWindowConfiguration(),
-                    mIsRecentTaskInvisible, null, null, mTask.getTaskInfo());
+                    mIsRecentTaskInvisible, null, null, mTask.getTaskInfo(),
+                    topApp.checkEnterPictureInPictureAppOpsState());
             return mTarget;
         }
 
@@ -1289,7 +1321,7 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
-                @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+                @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
             // Restore position and root task crop until client has a chance to modify it.
             t.setPosition(animationLeash, mLocalBounds.left, mLocalBounds.top);
             mTmpRect.set(mLocalBounds);
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index 1a429f8..eeac230 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -22,6 +22,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
+import android.annotation.NonNull;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Binder;
@@ -65,7 +66,6 @@
             new ArrayList<>();
     @VisibleForTesting
     final ArrayList<NonAppWindowAnimationAdapter> mPendingNonAppAnimations = new ArrayList<>();
-    private final Rect mTmpRect = new Rect();
     private final Handler mHandler;
     private final Runnable mTimeoutRunnable = () -> cancelAnimation("timeoutRunnable");
 
@@ -85,18 +85,18 @@
      * Creates an animation record for each individual {@link WindowContainer}.
      *
      * @param windowContainer The windows to animate.
-     * @param position The position app bounds, in screen coordinates.
+     * @param position The position app bounds relative to its parent.
      * @param localBounds The bounds of the app relative to its parent.
-     * @param stackBounds The stack bounds of the app relative to position.
-     * @param startBounds The stack bounds before the transition, in screen coordinates
+     * @param endBounds The end bounds after the transition, in screen coordinates.
+     * @param startBounds The start bounds before the transition, in screen coordinates.
      * @return The record representing animation(s) to run on the app.
      */
     RemoteAnimationRecord createRemoteAnimationRecord(WindowContainer windowContainer,
-            Point position, Rect localBounds, Rect stackBounds, Rect startBounds) {
+            Point position, Rect localBounds, Rect endBounds, Rect startBounds) {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createAnimationAdapter(): container=%s",
                 windowContainer);
         final RemoteAnimationRecord adapters = new RemoteAnimationRecord(windowContainer, position,
-                localBounds, stackBounds, startBounds);
+                localBounds, endBounds, startBounds);
         mPendingAnimations.add(adapters);
         return adapters;
     }
@@ -208,7 +208,7 @@
                 if (wrappers.mThumbnailAdapter != null
                         && wrappers.mThumbnailAdapter.mCapturedFinishCallback != null) {
                     wrappers.mThumbnailAdapter.mCapturedFinishCallback
-                            .onAnimationFinished(wrappers.mAdapter.mAnimationType,
+                            .onAnimationFinished(wrappers.mThumbnailAdapter.mAnimationType,
                                     wrappers.mThumbnailAdapter);
                 }
                 mPendingAnimations.remove(i);
@@ -219,7 +219,7 @@
 
     private RemoteAnimationTarget[] createWallpaperAnimations() {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createWallpaperAnimations()");
-        return WallpaperAnimationAdapter.startWallpaperAnimations(mService,
+        return WallpaperAnimationAdapter.startWallpaperAnimations(mDisplayContent,
                 mRemoteAnimationAdapter.getDuration(),
                 mRemoteAnimationAdapter.getStatusBarTransitionDelay(),
                 adapter -> {
@@ -261,7 +261,7 @@
                     }
                     if (adapters.mThumbnailAdapter != null) {
                         adapters.mThumbnailAdapter.mCapturedFinishCallback
-                                .onAnimationFinished(adapters.mAdapter.mAnimationType,
+                                .onAnimationFinished(adapters.mThumbnailAdapter.mAnimationType,
                                         adapters.mThumbnailAdapter);
                     }
                     mPendingAnimations.remove(i);
@@ -396,6 +396,7 @@
         RemoteAnimationTarget mTarget;
         final WindowContainer mWindowContainer;
         final Rect mStartBounds;
+        private @RemoteAnimationTarget.Mode int mMode = RemoteAnimationTarget.MODE_CHANGING;
 
         RemoteAnimationRecord(WindowContainer windowContainer, Point endPos, Rect localBounds,
                 Rect endBounds, Rect startBounds) {
@@ -404,16 +405,17 @@
                 mStartBounds = new Rect(startBounds);
                 mAdapter = new RemoteAnimationAdapterWrapper(this, endPos, localBounds, endBounds,
                         mStartBounds);
-                mTmpRect.set(startBounds);
-                mTmpRect.offsetTo(0, 0);
                 if (mRemoteAnimationAdapter.getChangeNeedsSnapshot()) {
-                    mThumbnailAdapter =
-                            new RemoteAnimationAdapterWrapper(this, new Point(0, 0), localBounds,
-                                    mTmpRect, new Rect());
+                    final Rect thumbnailLocalBounds = new Rect(startBounds);
+                    thumbnailLocalBounds.offsetTo(0, 0);
+                    // Snapshot is located at (0,0) of the animation leash. It doesn't have size
+                    // change, so the startBounds is its end bounds, and no start bounds for it.
+                    mThumbnailAdapter = new RemoteAnimationAdapterWrapper(this, new Point(0, 0),
+                            thumbnailLocalBounds, startBounds, new Rect());
                 }
             } else {
                 mAdapter = new RemoteAnimationAdapterWrapper(this, endPos, localBounds, endBounds,
-                        new Rect(endPos.x, endPos.y, endBounds.right, endBounds.bottom));
+                        new Rect());
                 mStartBounds = null;
             }
         }
@@ -428,18 +430,25 @@
             return mTarget;
         }
 
+        void setMode(@RemoteAnimationTarget.Mode int mode) {
+            mMode = mode;
+        }
+
         int getMode() {
-            final DisplayContent dc = mWindowContainer.getDisplayContent();
-            final ActivityRecord topActivity = mWindowContainer.getTopMostActivity();
-            // Note that opening/closing transitions are per-activity while changing transitions
-            // are per-task.
-            if (dc.mOpeningApps.contains(topActivity)) {
-                return RemoteAnimationTarget.MODE_OPENING;
-            } else if (dc.mChangingContainers.contains(mWindowContainer)) {
-                return RemoteAnimationTarget.MODE_CHANGING;
-            } else {
-                return RemoteAnimationTarget.MODE_CLOSING;
+            return mMode;
+        }
+
+        /** Whether its parent is also an animation target in the same transition. */
+        boolean hasAnimatingParent() {
+            // mOpeningApps and mClosingApps are only activities, so only need to check
+            // mChangingContainers.
+            for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) {
+                if (mWindowContainer.isDescendantOf(
+                        mDisplayContent.mChangingContainers.valueAt(i))) {
+                    return true;
+                }
             }
+            return false;
         }
     }
 
@@ -450,15 +459,15 @@
         private @AnimationType int mAnimationType;
         final Point mPosition = new Point();
         final Rect mLocalBounds;
-        final Rect mRootTaskBounds = new Rect();
+        final Rect mEndBounds = new Rect();
         final Rect mStartBounds = new Rect();
 
         RemoteAnimationAdapterWrapper(RemoteAnimationRecord record, Point position,
-                Rect localBounds, Rect rootTaskBounds, Rect startBounds) {
+                Rect localBounds, Rect endBounds, Rect startBounds) {
             mRecord = record;
             mPosition.set(position.x, position.y);
             mLocalBounds = localBounds;
-            mRootTaskBounds.set(rootTaskBounds);
+            mEndBounds.set(endBounds);
             mStartBounds.set(startBounds);
         }
 
@@ -469,15 +478,20 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
-                @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+                @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
             ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation");
 
-            // Restore position and stack crop until client has a chance to modify it.
             if (mStartBounds.isEmpty()) {
-                t.setPosition(animationLeash, 0, 0);
-                t.setWindowCrop(animationLeash, -1, -1);
+                // Restore position and stack crop until client has a chance to modify it.
+                t.setPosition(animationLeash, mPosition.x, mPosition.y);
+                t.setWindowCrop(animationLeash, mEndBounds.width(), mEndBounds.height());
             } else {
-                t.setPosition(animationLeash, mStartBounds.left, mStartBounds.top);
+                // Offset the change animation leash to the relative start position in parent.
+                // (mPosition) is the relative end position in parent container.
+                // (mStartBounds - mEndBounds) is the position difference between start and end.
+                // (mPosition + mStartBounds - mEndBounds) will be the relative start position.
+                t.setPosition(animationLeash, mPosition.x + mStartBounds.left - mEndBounds.left,
+                        mPosition.y + mStartBounds.top - mEndBounds.top);
                 t.setWindowCrop(animationLeash, mStartBounds.width(), mStartBounds.height());
             }
             mCapturedLeash = animationLeash;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 52da4b8..e3600e6 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -37,10 +37,10 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE;
 import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
 import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
-import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
 import static android.view.WindowManager.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT;
@@ -48,11 +48,17 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_SURFACE_ALLOC;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
 import static com.android.server.policy.PhoneWindowManager.SYSTEM_DIALOG_REASON_ASSIST;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
+import static com.android.server.wm.ActivityRecord.State.FINISHING;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ROOT_TASK;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH;
@@ -67,19 +73,14 @@
 import static com.android.server.wm.ActivityTaskSupervisor.dumpHistoryList;
 import static com.android.server.wm.ActivityTaskSupervisor.printThisActivity;
 import static com.android.server.wm.KeyguardController.KEYGUARD_SLEEP_TOKEN_TAG;
+import static com.android.server.wm.RootWindowContainerProto.DEFAULT_MIN_SIZE_RESIZABLE_TASK;
 import static com.android.server.wm.RootWindowContainerProto.IS_HOME_RECENTS_COMPONENT;
 import static com.android.server.wm.RootWindowContainerProto.KEYGUARD_CONTROLLER;
 import static com.android.server.wm.RootWindowContainerProto.WINDOW_CONTAINER;
-import static com.android.server.wm.Task.ActivityState.FINISHING;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
-import static com.android.server.wm.Task.ActivityState.STOPPED;
-import static com.android.server.wm.Task.ActivityState.STOPPING;
 import static com.android.server.wm.Task.REPARENT_LEAVE_ROOT_TASK_IN_PLACE;
 import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT;
-import static com.android.server.wm.Task.TASK_VISIBILITY_INVISIBLE;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_INVISIBLE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_TRACE;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -365,8 +366,26 @@
                 return false;
             }
 
+            if (matchingCandidate(task)) {
+                return true;
+            }
+
+            // Looking for the embedded tasks (if any)
+            return !task.isLeafTaskFragment() && task.forAllLeafTaskFragments(
+                    this::matchingCandidate);
+        }
+
+        boolean matchingCandidate(TaskFragment taskFragment) {
+            final Task task = taskFragment.asTask();
+            if (task == null) {
+                return false;
+            }
+
             // Overlays should not be considered as the task's logical top activity.
-            final ActivityRecord r = task.getTopNonFinishingActivity(false /* includeOverlays */);
+            // Activities of the tasks that embedded from this one should not be used.
+            final ActivityRecord r = task.getTopNonFinishingActivity(false /* includeOverlays */,
+                    false /* includingEmbeddedTask */);
+
             if (r == null || r.finishing || r.mUserId != userId
                     || r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
                 ProtoLog.d(WM_DEBUG_TASKS, "Skipping %s: mismatch root %s", task, r);
@@ -486,6 +505,7 @@
             mTopFocusedDisplayId = topFocusedDisplayId;
             mWmService.mInputManager.setFocusedDisplay(topFocusedDisplayId);
             mWmService.mPolicy.setTopFocusedDisplay(topFocusedDisplayId);
+            mWmService.mAccessibilityController.setFocusedDisplay(topFocusedDisplayId);
             ProtoLog.d(WM_DEBUG_FOCUS_LIGHT, "New topFocusedDisplayId=%d", topFocusedDisplayId);
         }
         return changed;
@@ -509,6 +529,11 @@
         mTaskSupervisor.updateTopResumedActivityIfNeeded();
     }
 
+    @Override
+    boolean isAttached() {
+        return true;
+    }
+
     /**
      * Called when DisplayWindowSettings values may change.
      */
@@ -817,8 +842,7 @@
         }
 
         // Initialize state of exiting tokens.
-        final int numDisplays = mChildren.size();
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
+        for (int displayNdx = 0; displayNdx < mChildren.size(); ++displayNdx) {
             final DisplayContent displayContent = mChildren.get(displayNdx);
             displayContent.setExitingTokensHasVisible(false);
         }
@@ -855,6 +879,7 @@
 
         // Send any pending task-info changes that were queued-up during a layout deferment
         mWmService.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
+        mWmService.mAtmService.mTaskFragmentOrganizerController.dispatchPendingEvents();
         mWmService.mSyncEngine.onSurfacePlacement();
         mWmService.mAnimator.executeAfterPrepareSurfacesRunnables();
 
@@ -867,10 +892,10 @@
             recentsAnimationController.checkAnimationReady(defaultDisplay.mWallpaperController);
         }
 
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
+        for (int displayNdx = 0; displayNdx < mChildren.size(); ++displayNdx) {
             final DisplayContent displayContent = mChildren.get(displayNdx);
             if (displayContent.mWallpaperMayChange) {
-                if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "Wallpaper may change!  Adjusting");
+                ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper may change!  Adjusting");
                 displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
                 if (DEBUG_LAYOUT_REPEATS) {
                     surfacePlacer.debugLayoutRepeats("WallpaperMayChange",
@@ -929,12 +954,12 @@
         }
 
         // Time to remove any exiting tokens?
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
+        for (int displayNdx = mChildren.size() - 1; displayNdx >= 0; --displayNdx) {
             final DisplayContent displayContent = mChildren.get(displayNdx);
             displayContent.removeExistingTokensIfPossible();
         }
 
-        for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
+        for (int displayNdx = 0; displayNdx < mChildren.size(); ++displayNdx) {
             final DisplayContent displayContent = mChildren.get(displayNdx);
             if (displayContent.pendingLayoutChanges != 0) {
                 displayContent.setLayoutNeeded();
@@ -1230,6 +1255,11 @@
         pw.println(mTopFocusedDisplayId);
     }
 
+    void dumpDefaultMinSizeOfResizableTask(PrintWriter pw) {
+        pw.print("  mDefaultMinSizeOfResizeableTaskDp=");
+        pw.println(mDefaultMinSizeOfResizeableTaskDp);
+    }
+
     void dumpLayoutNeededDisplayIds(PrintWriter pw) {
         if (!isLayoutNeeded()) {
             return;
@@ -1276,7 +1306,7 @@
         mTaskSupervisor.getKeyguardController().dumpDebug(proto, KEYGUARD_CONTROLLER);
         proto.write(IS_HOME_RECENTS_COMPONENT,
                 mTaskSupervisor.mRecentTasks.isRecentsComponentHomeActivity(mCurrentUser));
-
+        proto.write(DEFAULT_MIN_SIZE_RESIZABLE_TASK, mDefaultMinSizeOfResizeableTaskDp);
         proto.end(token);
     }
 
@@ -1865,7 +1895,7 @@
         if (focusedRootTask == null) {
             return null;
         }
-        final ActivityRecord resumedActivity = focusedRootTask.getResumedActivity();
+        final ActivityRecord resumedActivity = focusedRootTask.getTopResumedActivity();
         if (resumedActivity != null && resumedActivity.app != null) {
             return resumedActivity;
         }
@@ -1887,11 +1917,11 @@
         // foreground.
         WindowProcessController fgApp = getItemFromRootTasks(rootTask -> {
             if (isTopDisplayFocusedRootTask(rootTask)) {
-                final ActivityRecord resumedActivity = rootTask.getResumedActivity();
+                final ActivityRecord resumedActivity = rootTask.getTopResumedActivity();
                 if (resumedActivity != null) {
                     return resumedActivity.app;
-                } else if (rootTask.getPausingActivity() != null) {
-                    return rootTask.getPausingActivity().app;
+                } else if (rootTask.getTopPausingActivity() != null) {
+                    return rootTask.getTopPausingActivity().app;
                 }
             }
             return null;
@@ -1917,7 +1947,8 @@
                     return;
                 }
 
-                if (rootTask.getVisibility(null /*starting*/) == TASK_VISIBILITY_INVISIBLE) {
+                if (rootTask.getVisibility(null /* starting */)
+                        == TASK_FRAGMENT_VISIBILITY_INVISIBLE) {
                     return;
                 }
 
@@ -1941,7 +1972,7 @@
 
     private boolean startActivityForAttachedApplicationIfNeeded(ActivityRecord r,
             WindowProcessController app, ActivityRecord top) {
-        if (r.finishing || !r.okToShowLocked() || !r.visibleIgnoringKeyguard || r.app != null
+        if (r.finishing || !r.showToCurrentUser() || !r.visibleIgnoringKeyguard || r.app != null
                 || app.mUid != r.info.applicationInfo.uid || !app.mName.equals(r.processName)) {
             return false;
         }
@@ -1974,7 +2005,8 @@
      */
     void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
             boolean preserveWindows, boolean notifyClients) {
-        if (mTaskSupervisor.inActivityVisibilityUpdate()) {
+        if (mTaskSupervisor.inActivityVisibilityUpdate()
+                || mTaskSupervisor.isRootVisibilityUpdateDeferred()) {
             // Don't do recursive work.
             return;
         }
@@ -2189,7 +2221,7 @@
                 // display area, so reparent.
                 rootTask.reparent(taskDisplayArea, true /* onTop */);
             }
-            mService.getTransitionController().requestTransitionIfNeeded(TRANSIT_CHANGE, rootTask);
+            rootTask.mTransitionController.requestTransitionIfNeeded(TRANSIT_PIP, rootTask);
 
             // Defer the windowing mode change until after the transition to prevent the activity
             // from doing work and changing the activity visuals while animating
@@ -2394,7 +2426,9 @@
                 if (displayShouldSleep) {
                     rootTask.goToSleepIfPossible(false /* shuttingDown */);
                 } else {
-                    rootTask.awakeFromSleepingLocked();
+                    rootTask.forAllLeafTasksAndLeafTaskFragments(
+                            taskFragment -> taskFragment.awakeFromSleeping(),
+                            true /* traverseTopToBottom */);
                     if (rootTask.isFocusedRootTaskOnDisplay()
                             && !mTaskSupervisor.getKeyguardController()
                             .isKeyguardOrAodShowing(display.mDisplayId)) {
@@ -2553,6 +2587,9 @@
             if (mService.isBooted() || mService.isBooting()) {
                 startSystemDecorations(display.mDisplayContent);
             }
+            // Drop any cached DisplayInfos associated with this display id - the values are now
+            // out of date given this display added event.
+            mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId);
         }
     }
 
@@ -2573,8 +2610,8 @@
             if (displayContent == null) {
                 return;
             }
-
             displayContent.remove();
+            mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId);
         }
     }
 
@@ -2586,6 +2623,9 @@
             if (displayContent != null) {
                 displayContent.onDisplayChanged();
             }
+            // Drop any cached DisplayInfos associated with this display id - the values are now
+            // out of date given this display changed event.
+            mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId);
         }
     }
 
@@ -2771,8 +2811,8 @@
 
         if (DEBUG_SWITCH) {
             Slog.v(TAG_SWITCH, "Destroying " + r + " in state " + r.getState()
-                    + " resumed=" + r.getTask().getResumedActivity() + " pausing="
-                    + r.getTask().getPausingActivity() + " for reason "
+                    + " resumed=" + r.getTask().getTopResumedActivity() + " pausing="
+                    + r.getTask().getTopPausingActivity() + " for reason "
                     + mDestroyAllActivitiesReason);
         }
 
@@ -2806,7 +2846,6 @@
         Slog.w(TAG, "  Force finishing activity "
                 + r.intent.getComponent().flattenToShortString());
         r.detachFromProcess();
-        r.mDisplayContent.prepareAppTransition(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED);
         r.mDisplayContent.requestTransitionAndLegacyPrepare(TRANSIT_CLOSE,
                 TRANSIT_FLAG_APP_CRASHED);
         r.destroyIfPossible("handleAppCrashed");
@@ -3359,7 +3398,7 @@
             if (rootTask == null || !rootTask.hasActivity()) {
                 continue;
             }
-            final ActivityRecord resumedActivity = rootTask.getResumedActivity();
+            final ActivityRecord resumedActivity = rootTask.getTopResumedActivity();
             if (resumedActivity == null || !resumedActivity.idle) {
                 ProtoLog.d(WM_DEBUG_STATES, "allResumedActivitiesIdle: rootTask=%d %s "
                         + "not idle", rootTask.getRootTaskId(), resumedActivity);
@@ -3374,7 +3413,7 @@
     boolean allResumedActivitiesVisible() {
         boolean[] foundResumed = {false};
         final boolean foundInvisibleResumedActivity = forAllRootTasks(rootTask -> {
-            final ActivityRecord r = rootTask.getResumedActivity();
+            final ActivityRecord r = rootTask.getTopResumedActivity();
             if (r != null) {
                 if (!r.nowVisible) {
                     return true;
@@ -3392,7 +3431,7 @@
     boolean allPausedActivitiesComplete() {
         boolean[] pausing = {true};
         final boolean hasActivityNotCompleted = forAllLeafTasks(task -> {
-            final ActivityRecord r = task.getPausingActivity();
+            final ActivityRecord r = task.getTopPausingActivity();
             if (r != null && !r.isState(PAUSED, STOPPED, STOPPING, FINISHING)) {
                 ProtoLog.d(WM_DEBUG_STATES, "allPausedActivitiesComplete: "
                         + "r=%s state=%s", r, r.getState());
@@ -3435,14 +3474,6 @@
         }, true /* traverseTopToBottom */);
     }
 
-    void cancelInitializingActivities() {
-        forAllRootTasks(task -> {
-            // We don't want to clear starting window for activities that aren't occluded
-            // as we need to display their starting window until they are done initializing.
-            task.forAllOccludedActivities(ActivityRecord::cancelInitializing);
-        });
-    }
-
     Task anyTaskForId(int id) {
         return anyTaskForId(id, MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE);
     }
@@ -3522,11 +3553,6 @@
         return task;
     }
 
-    ActivityRecord isInAnyTask(IBinder token) {
-        final ActivityRecord r = ActivityRecord.forTokenLocked(token);
-        return (r != null && r.isDescendantOf(this)) ? r : null;
-    }
-
     @VisibleForTesting
     void getRunningTasks(int maxNum, List<ActivityManager.RunningTaskInfo> list,
             int flags, int callingUid, ArraySet<Integer> profileIds) {
@@ -3535,14 +3561,7 @@
     }
 
     void startPowerModeLaunchIfNeeded(boolean forceSend, ActivityRecord targetActivity) {
-        final boolean sendPowerModeLaunch;
-
-        if (forceSend) {
-            sendPowerModeLaunch = true;
-        } else if (targetActivity == null || targetActivity.app == null) {
-            // Set power mode if we don't know what we're launching yet.
-            sendPowerModeLaunch = true;
-        } else {
+        if (!forceSend && targetActivity != null && targetActivity.app != null) {
             // Set power mode when the activity's process is different than the current top resumed
             // activity on all display areas, or if there are no resumed activities in the system.
             boolean[] noResumedActivities = {true};
@@ -3558,15 +3577,32 @@
                             !resumedActivityProcess.equals(targetActivity.app);
                 }
             });
-            sendPowerModeLaunch = noResumedActivities[0] || allFocusedProcessesDiffer[0];
+            if (!noResumedActivities[0] && !allFocusedProcessesDiffer[0]) {
+                // All focused activities are resumed and the process of the target activity is
+                // the same as them, e.g. delivering new intent to the current top.
+                return;
+            }
         }
 
-        if (sendPowerModeLaunch) {
-            mService.startLaunchPowerMode(
-                    ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY);
+        int reason = ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY;
+        // If the activity is launching while keyguard is locked (including occluded), the activity
+        // may be visible until its first relayout is done (e.g. apply show-when-lock flag). To
+        // avoid power mode from being cleared before that, add a special reason to consider whether
+        // the unknown visibility is resolved. The case from SystemUI is excluded because it should
+        // rely on keyguard-going-away.
+        if (mService.mKeyguardController.isKeyguardLocked() && targetActivity != null
+                && !targetActivity.isLaunchSourceType(ActivityRecord.LAUNCH_SOURCE_TYPE_SYSTEMUI)) {
+            final ActivityOptions opts = targetActivity.getOptions();
+            if (opts == null || opts.getSourceInfo() == null
+                    || opts.getSourceInfo().type != ActivityOptions.SourceInfo.TYPE_LOCKSCREEN) {
+                reason |= ActivityTaskManagerService.POWER_MODE_REASON_UNKNOWN_VISIBILITY;
+            }
         }
+        mService.startLaunchPowerMode(reason);
     }
 
+    // TODO(b/191434136): handle this properly when we add multi-window support on secondary
+    //  display.
     private void calculateDefaultMinimalSizeOfResizeableTasks() {
         final Resources res = mService.mContext.getResources();
         final float minimalSize = res.getDimension(
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index 4892005..2d4aef6 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -78,6 +78,18 @@
     }
 
     /**
+     * Constructs a new instance from a bundle and provided pid/uid.
+     *
+     * @param bOptions The {@link ActivityOptions} as {@link Bundle}.
+     */
+    static SafeActivityOptions fromBundle(Bundle bOptions, int callingPid, int callingUid) {
+        return bOptions != null
+                ? new SafeActivityOptions(ActivityOptions.fromBundle(bOptions),
+                        callingPid, callingUid)
+                : null;
+    }
+
+    /**
      * Constructs a new instance and records {@link Binder#getCallingPid}/
      * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing
      * this object.
@@ -91,6 +103,17 @@
     }
 
     /**
+     * Constructs a new instance.
+     *
+     * @param options The options to wrap.
+     */
+    private SafeActivityOptions(@Nullable ActivityOptions options, int callingPid, int callingUid) {
+        mOriginalCallingPid = callingPid;
+        mOriginalCallingUid = callingUid;
+        mOriginalOptions = options;
+    }
+
+    /**
      * Overrides options with options from a caller and records {@link Binder#getCallingPid}/
      * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when calling this
      * method.
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 58363f2..d1460f4 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -18,6 +18,7 @@
 
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.util.RotationUtils.deltaRotation;
+import static android.view.WindowManagerPolicyConstants.SCREEN_FREEZE_LAYER_BASE;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_SURFACE_ALLOC;
@@ -30,8 +31,6 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-import static com.android.server.wm.WindowManagerService.TYPE_LAYER_MULTIPLIER;
-import static com.android.server.wm.WindowStateAnimator.WINDOW_FREEZE_LAYER;
 
 import android.animation.ArgbEvaluator;
 import android.content.Context;
@@ -91,13 +90,6 @@
 class ScreenRotationAnimation {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "ScreenRotationAnimation" : TAG_WM;
 
-    /*
-     * Layers for screen rotation animation. We put these layers above
-     * WINDOW_FREEZE_LAYER so that screen freeze will cover all windows.
-     */
-    private static final int SCREEN_FREEZE_LAYER_BASE = WINDOW_FREEZE_LAYER + TYPE_LAYER_MULTIPLIER;
-    private static final int SCREEN_FREEZE_LAYER_ENTER = SCREEN_FREEZE_LAYER_BASE;
-
     private final Context mContext;
     private final DisplayContent mDisplayContent;
     private final float[] mTmpFloats = new float[9];
@@ -423,7 +415,7 @@
                         finalWidth * 2, finalHeight * 2);
                 Rect inner = new Rect(0, 0, finalWidth, finalHeight);
                 mEnteringBlackFrame = new BlackFrame(mService.mTransactionFactory, t, outer, inner,
-                        SCREEN_FREEZE_LAYER_ENTER, mDisplayContent, false, mEnterBlackFrameLayer);
+                        SCREEN_FREEZE_LAYER_BASE, mDisplayContent, false, mEnterBlackFrameLayer);
             } catch (OutOfResourcesException e) {
                 Slog.w(TAG, "Unable to allocate black surface", e);
             }
@@ -737,7 +729,12 @@
                 mScreenshotRotationAnimator = null;
                 mRotateScreenAnimator = null;
                 mService.mAnimator.mBulkUpdateParams |= WindowSurfacePlacer.SET_UPDATE_ROTATION;
-                kill();
+                if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) {
+                    // It also invokes kill().
+                    mDisplayContent.setRotationAnimation(null);
+                } else {
+                    kill();
+                }
                 mService.updateRotation(false, false);
             }
         }
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index e282012..8c056b2 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -69,6 +69,7 @@
 import android.view.InputChannel;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.WindowManager;
@@ -113,7 +114,7 @@
     private float mLastReportedAnimatorScale;
     private String mPackageName;
     private String mRelayoutTag;
-    private final InsetsState mDummyRequestedVisibility = new InsetsState();
+    private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities();
     private final InsetsSourceControl[] mDummyControls =  new InsetsSourceControl[0];
 
     public Session(WindowManagerService service, IWindowSessionCallback callback) {
@@ -187,29 +188,28 @@
 
     @Override
     public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, InsetsState requestedVisibility,
+            int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId,
-                UserHandle.getUserId(mUid), requestedVisibility, outInputChannel, outInsetsState,
+                UserHandle.getUserId(mUid), requestedVisibilities, outInputChannel, outInsetsState,
                 outActiveControls);
     }
 
-
     @Override
     public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, int userId, InsetsState requestedVisibility,
+            int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
-                requestedVisibility, outInputChannel, outInsetsState, outActiveControls);
+                requestedVisibilities, outInputChannel, outInsetsState, outActiveControls);
     }
 
     @Override
     public int addToDisplayWithoutInputChannel(IWindow window, WindowManager.LayoutParams attrs,
             int viewVisibility, int displayId, InsetsState outInsetsState) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId,
-                UserHandle.getUserId(mUid), mDummyRequestedVisibility, null /* outInputChannel */,
+                UserHandle.getUserId(mUid), mDummyRequestedVisibilities, null /* outInputChannel */,
                 outInsetsState, mDummyControls);
     }
 
@@ -299,6 +299,17 @@
         }
     }
 
+
+    @Override
+    public boolean dropForAccessibility(IWindow window, int x, int y) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            return mDragDropController.dropForAccessibility(window, x, y);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     /**
      * Validates the given drag data.
      */
@@ -624,12 +635,12 @@
     }
 
     @Override
-    public void insetsModified(IWindow window, InsetsState state) {
+    public void updateRequestedVisibilities(IWindow window, InsetsVisibilities visibilities) {
         synchronized (mService.mGlobalLock) {
             final WindowState windowState = mService.windowForClientLocked(this, window,
                     false /* throwOnError */);
             if (windowState != null) {
-                windowState.updateRequestedVisibility(state);
+                windowState.setRequestedVisibilities(visibilities);
                 windowState.getDisplayContent().getInsetsPolicy().onInsetsModified(windowState);
             }
         }
diff --git a/services/core/java/com/android/server/wm/ShellRoot.java b/services/core/java/com/android/server/wm/ShellRoot.java
index be6a5d2..6ed59e9 100644
--- a/services/core/java/com/android/server/wm/ShellRoot.java
+++ b/services/core/java/com/android/server/wm/ShellRoot.java
@@ -197,7 +197,7 @@
                 mAccessibilityWindow = null;
             }
         }
-        if (mDisplayContent.mWmService.mAccessibilityController != null) {
+        if (mDisplayContent.mWmService.mAccessibilityController.hasCallbacks()) {
             mDisplayContent.mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(
                     mDisplayContent.getDisplayId());
         }
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index c671e38..8b1befb 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -32,6 +32,12 @@
      */
     boolean mIsTransitionForward;
 
+    /**
+     * Non-null if the starting window should cover the bounds of associated task. It is assigned
+     * when the parent activity of starting window may be put in a partial area of the task.
+     */
+    Task mAssociatedTask;
+
     protected StartingData(WindowManagerService service, int typeParams) {
         mService = service;
         mTypeParams = typeParams;
diff --git a/services/core/java/com/android/server/wm/StrictModeFlash.java b/services/core/java/com/android/server/wm/StrictModeFlash.java
index 48a7bde..cdf6b08 100644
--- a/services/core/java/com/android/server/wm/StrictModeFlash.java
+++ b/services/core/java/com/android/server/wm/StrictModeFlash.java
@@ -27,6 +27,7 @@
 import android.view.Surface;
 import android.view.Surface.OutOfResourcesException;
 import android.view.SurfaceControl;
+import android.view.WindowManagerPolicyConstants;
 
 class StrictModeFlash {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "StrictModeFlash" : TAG_WM;
@@ -52,7 +53,7 @@
                     .build();
 
             // one more than Watermark? arbitrary.
-            t.setLayer(ctrl, WindowManagerService.TYPE_LAYER_MULTIPLIER * 101);
+            t.setLayer(ctrl, WindowManagerPolicyConstants.STRICT_MODE_LAYER);
             t.setPosition(ctrl, 0, 0);
             t.show(ctrl);
             // Ensure we aren't considered as obscuring for Input purposes.
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index c7bf8ec..d712bbf 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -57,8 +57,11 @@
     @VisibleForTesting
     SurfaceControl mLeash;
     @VisibleForTesting
+    SurfaceFreezer.Snapshot mSnapshot;
+    @VisibleForTesting
     final Animatable mAnimatable;
-    private final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
+    @VisibleForTesting
+    final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
 
     /**
      * Static callback to run on all animations started through this SurfaceAnimator
@@ -151,12 +154,14 @@
      * @param animationFinishedCallback The callback being triggered when the animation finishes.
      * @param animationCancelledCallback The callback is triggered after the SurfaceAnimator sends a
      *                                   cancel call to the underlying AnimationAdapter.
+     * @param snapshotAnim The animation to run for the snapshot. {@code null} if there is no
+     *                     snapshot.
      */
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type,
             @Nullable OnAnimationFinishedCallback animationFinishedCallback,
             @Nullable Runnable animationCancelledCallback,
-            @Nullable SurfaceFreezer freezer) {
+            @Nullable AnimationAdapter snapshotAnim, @Nullable SurfaceFreezer freezer) {
         cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
         mAnimation = anim;
         mAnimationType = type;
@@ -181,12 +186,16 @@
             return;
         }
         mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
+        if (snapshotAnim != null) {
+            mSnapshot = freezer.takeSnapshotForAnimation();
+            mSnapshot.startAnimation(t, snapshotAnim, type);
+        }
     }
 
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type) {
         startAnimation(t, anim, hidden, type, null /* animationFinishedCallback */,
-                null /* animationCancelledCallback */, null /* freezer */);
+                null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */);
     }
 
     /**
@@ -328,6 +337,7 @@
         final OnAnimationFinishedCallback animationFinishedCallback =
                 mSurfaceAnimationFinishedCallback;
         final Runnable animationCancelledCallback = mAnimationCancelledCallback;
+        final SurfaceFreezer.Snapshot snapshot = mSnapshot;
         reset(t, false);
         if (animation != null) {
             if (!mAnimationStartDelayed && forwardCancel) {
@@ -346,9 +356,14 @@
             }
         }
 
-        if (forwardCancel && leash != null) {
-            t.remove(leash);
-            mService.scheduleAnimationLocked();
+        if (forwardCancel) {
+            if (snapshot != null) {
+                snapshot.cancelAnimation(t, false /* restarting */);
+            }
+            if (leash != null) {
+                t.remove(leash);
+                mService.scheduleAnimationLocked();
+            }
         }
 
         if (!restarting) {
@@ -361,6 +376,12 @@
         mAnimation = null;
         mSurfaceAnimationFinishedCallback = null;
         mAnimationType = ANIMATION_TYPE_NONE;
+        final SurfaceFreezer.Snapshot snapshot = mSnapshot;
+        mSnapshot = null;
+        if (snapshot != null) {
+            // Reset the mSnapshot reference before calling the callback to prevent circular reset.
+            snapshot.cancelAnimation(t, !destroyLeash);
+        }
         if (mLeash == null) {
             return;
         }
@@ -377,11 +398,15 @@
         boolean scheduleAnim = false;
         final SurfaceControl surface = animatable.getSurfaceControl();
         final SurfaceControl parent = animatable.getParentSurfaceControl();
+        final SurfaceControl curAnimationLeash = animatable.getAnimationLeash();
 
         // If the surface was destroyed or the leash is invalid, we don't care to reparent it back.
         // Note that we also set this variable to true even if the parent isn't valid anymore, in
         // order to ensure onAnimationLeashLost still gets called in this case.
-        final boolean reparent = surface != null;
+        // If the animation leash is set, and it is different from the removing leash, it means the
+        // surface now has a new animation surface. We don't want to reparent for that.
+        final boolean reparent = surface != null && (curAnimationLeash == null
+                || curAnimationLeash.equals(leash));
         if (reparent) {
             if (DEBUG_ANIM) Slog.i(TAG, "Reparenting to original parent: " + parent);
             // We shouldn't really need these isValid checks but we do
@@ -608,6 +633,14 @@
         void onAnimationLeashLost(Transaction t);
 
         /**
+         * Gets the last created animation leash that has not lost yet.
+         */
+        @Nullable
+        default SurfaceControl getAnimationLeash() {
+            return null;
+        }
+
+        /**
          * @return A new surface to be used for the animation leash, inserted at the correct
          *         position in the hierarchy.
          */
diff --git a/services/core/java/com/android/server/wm/SurfaceFreezer.java b/services/core/java/com/android/server/wm/SurfaceFreezer.java
index e0a791e..c667db8 100644
--- a/services/core/java/com/android/server/wm/SurfaceFreezer.java
+++ b/services/core/java/com/android/server/wm/SurfaceFreezer.java
@@ -17,22 +17,20 @@
 package com.android.server.wm;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.GraphicBuffer;
 import android.graphics.PixelFormat;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.view.Surface;
 import android.view.SurfaceControl;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 
-import java.util.function.Supplier;
-
 /**
  * This class handles "freezing" of an Animatable. The Animatable in question should implement
  * Freezable.
@@ -53,7 +51,8 @@
 
     private final Freezable mAnimatable;
     private final WindowManagerService mWmService;
-    private SurfaceControl mLeash;
+    @VisibleForTesting
+    SurfaceControl mLeash;
     Snapshot mSnapshot = null;
     final Rect mFreezeBounds = new Rect();
 
@@ -70,17 +69,21 @@
      * above the target surface) and then taking a snapshot and placing it over the target surface.
      *
      * @param startBounds The original bounds (on screen) of the surface we are snapshotting.
+     * @param relativePosition The related position of the snapshot surface to its parent.
+     * @param freezeTarget The surface to take snapshot from. If {@code null}, we will take a
+     *                     snapshot from the {@link #mAnimatable} surface.
      */
-    void freeze(SurfaceControl.Transaction t, Rect startBounds) {
+    void freeze(SurfaceControl.Transaction t, Rect startBounds, Point relativePosition,
+            @Nullable SurfaceControl freezeTarget) {
         mFreezeBounds.set(startBounds);
 
         mLeash = SurfaceAnimator.createAnimationLeash(mAnimatable, mAnimatable.getSurfaceControl(),
                 t, ANIMATION_TYPE_SCREEN_ROTATION, startBounds.width(), startBounds.height(),
-                startBounds.left, startBounds.top, false /* hidden */,
+                relativePosition.x, relativePosition.y, false /* hidden */,
                 mWmService.mTransactionFactory);
         mAnimatable.onAnimationLeashCreated(t, mLeash);
 
-        SurfaceControl freezeTarget = mAnimatable.getFreezeSnapshotTarget();
+        freezeTarget = freezeTarget != null ? freezeTarget : mAnimatable.getFreezeSnapshotTarget();
         if (freezeTarget != null) {
             SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = createSnapshotBuffer(
                     freezeTarget, startBounds);
@@ -89,7 +92,7 @@
             if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
                 return;
             }
-            mSnapshot = new Snapshot(mWmService.mSurfaceFactory, t, screenshotBuffer, mLeash);
+            mSnapshot = new Snapshot(t, screenshotBuffer, mLeash);
         }
     }
 
@@ -104,12 +107,25 @@
     }
 
     /**
+     * Used by {@link SurfaceAnimator}. This "transfers" the snapshot leash to be used for
+     * animation. By transferring the leash, this will no longer try to clean-up the leash when
+     * finished.
+     */
+    @Nullable
+    Snapshot takeSnapshotForAnimation() {
+        final Snapshot out = mSnapshot;
+        mSnapshot = null;
+        return out;
+    }
+
+    /**
      * Clean-up the snapshot and remove leash. If the leash was taken, this just cleans-up the
      * snapshot.
      */
     void unfreeze(SurfaceControl.Transaction t) {
         if (mSnapshot != null) {
             mSnapshot.cancelAnimation(t, false /* restarting */);
+            mSnapshot = null;
         }
         if (mLeash == null) {
             return;
@@ -117,12 +133,24 @@
         SurfaceControl leash = mLeash;
         mLeash = null;
         final boolean scheduleAnim = SurfaceAnimator.removeLeash(t, mAnimatable, leash,
-                false /* destroy */);
+                true /* destroy */);
         if (scheduleAnim) {
             mWmService.scheduleAnimationLocked();
         }
     }
 
+    void setLayer(SurfaceControl.Transaction t, int layer) {
+        if (mLeash != null) {
+            t.setLayer(mLeash, layer);
+        }
+    }
+
+    void setRelativeLayer(SurfaceControl.Transaction t, SurfaceControl relativeTo, int layer) {
+        if (mLeash != null) {
+            t.setRelativeLayer(mLeash, relativeTo, layer);
+        }
+    }
+
     boolean hasLeash() {
         return mLeash != null;
     }
@@ -146,13 +174,12 @@
     class Snapshot {
         private SurfaceControl mSurfaceControl;
         private AnimationAdapter mAnimation;
-        private SurfaceAnimator.OnAnimationFinishedCallback mFinishedCallback;
 
         /**
          * @param t Transaction to create the thumbnail in.
          * @param screenshotBuffer A thumbnail or placeholder for thumbnail to initialize with.
          */
-        Snapshot(Supplier<Surface> surfaceFactory, SurfaceControl.Transaction t,
+        Snapshot(SurfaceControl.Transaction t,
                 SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer, SurfaceControl parent) {
             // We can't use a delegating constructor since we need to
             // reference this::onAnimationFinished
@@ -194,19 +221,15 @@
          *             component responsible for running the animation. It runs the animation with
          *             {@link AnimationAdapter#startAnimation} once the hierarchy with
          *             the Leash has been set up.
-         * @param animationFinishedCallback The callback being triggered when the animation
-         *                                  finishes.
          */
-        void startAnimation(SurfaceControl.Transaction t, AnimationAdapter anim, int type,
-                @Nullable SurfaceAnimator.OnAnimationFinishedCallback animationFinishedCallback) {
+        void startAnimation(SurfaceControl.Transaction t, AnimationAdapter anim, int type) {
             cancelAnimation(t, true /* restarting */);
             mAnimation = anim;
-            mFinishedCallback = animationFinishedCallback;
             if (mSurfaceControl == null) {
                 cancelAnimation(t, false /* restarting */);
                 return;
             }
-            mAnimation.startAnimation(mSurfaceControl, t, type, animationFinishedCallback);
+            mAnimation.startAnimation(mSurfaceControl, t, type, (typ, ani) -> { });
         }
 
         /**
@@ -218,18 +241,9 @@
         void cancelAnimation(SurfaceControl.Transaction t, boolean restarting) {
             final SurfaceControl leash = mSurfaceControl;
             final AnimationAdapter animation = mAnimation;
-            final SurfaceAnimator.OnAnimationFinishedCallback animationFinishedCallback =
-                    mFinishedCallback;
             mAnimation = null;
-            mFinishedCallback = null;
             if (animation != null) {
                 animation.onAnimationCancelled(leash);
-                if (!restarting) {
-                    if (animationFinishedCallback != null) {
-                        animationFinishedCallback.onAnimationFinished(
-                                ANIMATION_TYPE_APP_TRANSITION, animation);
-                    }
-                }
             }
             if (!restarting) {
                 destroy(t);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index c068fd1..ba2da06 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -27,13 +27,10 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.PINNED_WINDOWING_MODE_ELEVATION_IN_DIP;
-import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.activityTypeToString;
 import static android.app.WindowConfiguration.windowingModeToString;
@@ -43,7 +40,6 @@
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY;
-import static android.content.pm.ActivityInfo.FLAG_RESUME_WHILE_PAUSING;
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY;
@@ -53,9 +49,6 @@
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_AND_PIPABLE_DEPRECATED;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -67,7 +60,6 @@
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
-import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
@@ -84,22 +76,18 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
-import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_SHOWN;
+import static com.android.server.wm.ActivityRecord.State.INITIALIZING;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STARTED;
 import static com.android.server.wm.ActivityRecord.TRANSFER_SPLASH_SCREEN_COPYING;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TRANSITION;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_USER_LEAVING;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ADD_REMOVE;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_APP;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CLEANUP;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_LOCKTASK;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_PAUSE;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECENTS;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RESULTS;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ROOT_TASK;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_STATES;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TRANSITION;
@@ -112,7 +100,6 @@
 import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS;
-import static com.android.server.wm.ActivityTaskSupervisor.dumpHistoryList;
 import static com.android.server.wm.ActivityTaskSupervisor.printThisActivity;
 import static com.android.server.wm.IdentifierProto.HASH_CODE;
 import static com.android.server.wm.IdentifierProto.TITLE;
@@ -123,21 +110,12 @@
 import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_LAUNCHABLE_PRIV;
 import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_PINNABLE;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
-import static com.android.server.wm.Task.ActivityState.STARTED;
-import static com.android.server.wm.Task.ActivityState.STOPPING;
-import static com.android.server.wm.TaskProto.ACTIVITY_TYPE;
 import static com.android.server.wm.TaskProto.AFFINITY;
 import static com.android.server.wm.TaskProto.BOUNDS;
 import static com.android.server.wm.TaskProto.CREATED_BY_ORGANIZER;
-import static com.android.server.wm.TaskProto.DISPLAY_ID;
 import static com.android.server.wm.TaskProto.FILLS_PARENT;
 import static com.android.server.wm.TaskProto.HAS_CHILD_PIP_ACTIVITY;
 import static com.android.server.wm.TaskProto.LAST_NON_FULLSCREEN_BOUNDS;
-import static com.android.server.wm.TaskProto.MIN_HEIGHT;
-import static com.android.server.wm.TaskProto.MIN_WIDTH;
 import static com.android.server.wm.TaskProto.ORIG_ACTIVITY;
 import static com.android.server.wm.TaskProto.REAL_ACTIVITY;
 import static com.android.server.wm.TaskProto.RESIZE_MODE;
@@ -145,15 +123,13 @@
 import static com.android.server.wm.TaskProto.ROOT_TASK_ID;
 import static com.android.server.wm.TaskProto.SURFACE_HEIGHT;
 import static com.android.server.wm.TaskProto.SURFACE_WIDTH;
-import static com.android.server.wm.TaskProto.WINDOW_CONTAINER;
+import static com.android.server.wm.TaskProto.TASK_FRAGMENT;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
-import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowContainerChildProto.TASK;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ROOT_TASK;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.WindowManagerService.dipToPixel;
-import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_BEFORE_ANIM;
 
 import static java.lang.Integer.MAX_VALUE;
 
@@ -170,14 +146,8 @@
 import android.app.IActivityController;
 import android.app.PictureInPictureParams;
 import android.app.RemoteAction;
-import android.app.ResultInfo;
 import android.app.TaskInfo;
 import android.app.WindowConfiguration;
-import android.app.servertransaction.ActivityResultItem;
-import android.app.servertransaction.ClientTransaction;
-import android.app.servertransaction.NewIntentItem;
-import android.app.servertransaction.PauseActivityItem;
-import android.app.servertransaction.ResumeActivityItem;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -194,6 +164,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
@@ -208,7 +179,6 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
 import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
@@ -244,23 +214,21 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
-class Task extends WindowContainer<WindowContainer> {
+/**
+ * {@link Task} is a TaskFragment that can contain a group of activities to perform a certain job.
+ * Activities of the same task affinities usually group in the same {@link Task}. A {@link Task}
+ * can also be an entity that showing in the Recents Screen for a job that user interacted with.
+ * A {@link Task} can also contain other {@link Task}s.
+ */
+class Task extends TaskFragment {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "Task" : TAG_ATM;
-    static final String TAG_ADD_REMOVE = TAG + POSTFIX_ADD_REMOVE;
     private static final String TAG_RECENTS = TAG + POSTFIX_RECENTS;
-    private static final String TAG_LOCKTASK = TAG + POSTFIX_LOCKTASK;
     static final String TAG_TASKS = TAG + POSTFIX_TASKS;
-    private static final String TAG_APP = TAG + POSTFIX_APP;
     static final String TAG_CLEANUP = TAG + POSTFIX_CLEANUP;
-    private static final String TAG_PAUSE = TAG + POSTFIX_PAUSE;
-    private static final String TAG_RESULTS = TAG + POSTFIX_RESULTS;
-    private static final String TAG_ROOT_TASK = TAG + POSTFIX_ROOT_TASK;
-    private static final String TAG_STATES = TAG + POSTFIX_STATES;
     private static final String TAG_SWITCH = TAG + POSTFIX_SWITCH;
     private static final String TAG_TRANSITION = TAG + POSTFIX_TRANSITION;
     private static final String TAG_USER_LEAVING = TAG + POSTFIX_USER_LEAVING;
@@ -303,10 +271,6 @@
     private static final String ATTR_LAST_SNAPSHOT_CONTENT_INSETS = "last_snapshot_content_insets";
     private static final String ATTR_LAST_SNAPSHOT_BUFFER_SIZE = "last_snapshot_buffer_size";
 
-    // Set to false to disable the preview that is shown while a new activity
-    // is being started.
-    private static final boolean SHOW_APP_STARTING_PREVIEW = true;
-
     // How long to wait for all background Activities to redraw following a call to
     // convertToTranslucent().
     private static final long TRANSLUCENT_CONVERSION_TIMEOUT = 2000;
@@ -315,7 +279,6 @@
     // code.
     static final int PERSIST_TASK_VERSION = 1;
 
-    static final int INVALID_MIN_SIZE = -1;
     private float mShadowRadius = 0;
 
     /**
@@ -335,36 +298,6 @@
     // Do not move the root task as a part of reparenting
     static final int REPARENT_LEAVE_ROOT_TASK_IN_PLACE = 2;
 
-    @IntDef(prefix = {"TASK_VISIBILITY"}, value = {
-            TASK_VISIBILITY_VISIBLE,
-            TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
-            TASK_VISIBILITY_INVISIBLE,
-    })
-    @interface TaskVisibility {}
-
-    /** Task is visible. No other tasks on top that fully or partially occlude it. */
-    static final int TASK_VISIBILITY_VISIBLE = 0;
-
-    /** Task is partially occluded by other translucent task(s) on top of it. */
-    static final int TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT = 1;
-
-    /** Task is completely invisible. */
-    static final int TASK_VISIBILITY_INVISIBLE = 2;
-
-    enum ActivityState {
-        INITIALIZING,
-        STARTED,
-        RESUMED,
-        PAUSING,
-        PAUSED,
-        STOPPING,
-        STOPPED,
-        FINISHING,
-        DESTROYING,
-        DESTROYED,
-        RESTARTING_PROCESS
-    }
-
     // The topmost Activity passed to convertToTranslucent(). When non-null it means we are
     // waiting for all Activities in mUndrawnActivitiesBelowTopTranslucent to be removed as they
     // are drawn. When the last member of mUndrawnActivitiesBelowTopTranslucent is removed the
@@ -446,7 +379,6 @@
 
     CharSequence lastDescription; // Last description captured for this item.
 
-    Task mAdjacentTask; // Task adjacent to this one.
     int mAffiliatedTaskId; // taskId of parent affiliation or self if no parent.
     Task mPrevAffiliate; // previous task in affiliated chain.
     int mPrevAffiliateTaskId = INVALID_TASK_ID; // previous id for persistence.
@@ -458,21 +390,12 @@
     String mCallingPackage;
     String mCallingFeatureId;
 
-    private final Rect mTmpStableBounds = new Rect();
-    private final Rect mTmpNonDecorBounds = new Rect();
-    private final Rect mTmpBounds = new Rect();
-    private final Rect mTmpInsets = new Rect();
-    private final Rect mTmpFullBounds = new Rect();
     private static final Rect sTmpBounds = new Rect();
 
     // Last non-fullscreen bounds the task was launched in or resized to.
     // The information is persisted and used to determine the appropriate root task to launch the
     // task into on restore.
     Rect mLastNonFullscreenBounds = null;
-    // Minimal width and height of this task when it's resizeable. -1 means it should use the
-    // default minimal width/height.
-    int mMinWidth;
-    int mMinHeight;
 
     // The surface transition of the target when recents animation is finished.
     // This is originally introduced to carry out the current surface control position and window
@@ -497,10 +420,6 @@
     /** Used by fillTaskInfo */
     final TaskActivitiesReport mReuseActivitiesReport = new TaskActivitiesReport();
 
-    final ActivityTaskManagerService mAtmService;
-    final ActivityTaskSupervisor mTaskSupervisor;
-    final RootWindowContainer mRootWindowContainer;
-
     /* Unique identifier for this task. */
     final int mTaskId;
     /* User for which this task was created. */
@@ -561,7 +480,6 @@
 
     private Dimmer mDimmer = new Dimmer(this);
     private final Rect mTmpDimBoundsRect = new Rect();
-    private final Point mLastSurfaceSize = new Point();
 
     /** @see #setCanAffectSystemUiFlags */
     private boolean mCanAffectSystemUiFlags = true;
@@ -571,29 +489,6 @@
     /** ActivityRecords that are exiting, but still on screen for animations. */
     final ArrayList<ActivityRecord> mExitingActivities = new ArrayList<>();
 
-    /**
-     * When we are in the process of pausing an activity, before starting the
-     * next one, this variable holds the activity that is currently being paused.
-     *
-     * Only set at leaf tasks.
-     */
-    @Nullable
-    private ActivityRecord mPausingActivity = null;
-
-    /**
-     * This is the last activity that we put into the paused state.  This is
-     * used to determine if we need to do an activity transition while sleeping,
-     * when we normally hold the top activity paused.
-     */
-    ActivityRecord mLastPausedActivity = null;
-
-    /**
-     * Current activity that is resumed, or null if there is none.
-     * Only set at leaf tasks.
-     */
-    @Nullable
-    private ActivityRecord mResumedActivity = null;
-
     private boolean mForceShowForAllUsers;
 
     /** When set, will force the task to report as invisible. */
@@ -641,121 +536,6 @@
     }
 
     private static final ResetTargetTaskHelper sResetTargetTaskHelper = new ResetTargetTaskHelper();
-    private final EnsureActivitiesVisibleHelper mEnsureActivitiesVisibleHelper =
-            new EnsureActivitiesVisibleHelper(this);
-    private final EnsureVisibleActivitiesConfigHelper mEnsureVisibleActivitiesConfigHelper =
-            new EnsureVisibleActivitiesConfigHelper();
-    private class EnsureVisibleActivitiesConfigHelper {
-        private boolean mUpdateConfig;
-        private boolean mPreserveWindow;
-        private boolean mBehindFullscreen;
-
-        void reset(boolean preserveWindow) {
-            mPreserveWindow = preserveWindow;
-            mUpdateConfig = false;
-            mBehindFullscreen = false;
-        }
-
-        void process(ActivityRecord start, boolean preserveWindow) {
-            if (start == null || !start.mVisibleRequested) {
-                return;
-            }
-            reset(preserveWindow);
-
-            final PooledFunction f = PooledLambda.obtainFunction(
-                    EnsureVisibleActivitiesConfigHelper::processActivity, this,
-                    PooledLambda.__(ActivityRecord.class));
-            forAllActivities(f, start, true /*includeBoundary*/, true /*traverseTopToBottom*/);
-            f.recycle();
-
-            if (mUpdateConfig) {
-                // Ensure the resumed state of the focus activity if we updated the configuration of
-                // any activity.
-                mRootWindowContainer.resumeFocusedTasksTopActivities();
-            }
-        }
-
-        boolean processActivity(ActivityRecord r) {
-            mUpdateConfig |= r.ensureActivityConfiguration(0 /*globalChanges*/, mPreserveWindow);
-            mBehindFullscreen |= r.occludesParent();
-            return mBehindFullscreen;
-        }
-    }
-
-    private final CheckBehindFullscreenActivityHelper mCheckBehindFullscreenActivityHelper =
-            new CheckBehindFullscreenActivityHelper();
-    private class CheckBehindFullscreenActivityHelper {
-        private boolean mAboveTop;
-        private boolean mBehindFullscreenActivity;
-        private ActivityRecord mToCheck;
-        private Consumer<ActivityRecord> mHandleBehindFullscreenActivity;
-        private boolean mHandlingOccluded;
-
-        private void reset(ActivityRecord toCheck,
-                Consumer<ActivityRecord> handleBehindFullscreenActivity) {
-            mToCheck = toCheck;
-            mHandleBehindFullscreenActivity = handleBehindFullscreenActivity;
-            mAboveTop = true;
-            mBehindFullscreenActivity = false;
-
-            if (!shouldBeVisible(null)) {
-                // The root task is not visible, so no activity in it should be displaying a
-                // starting window. Mark all activities below top and behind fullscreen.
-                mAboveTop = false;
-                mBehindFullscreenActivity = true;
-            }
-
-            mHandlingOccluded = mToCheck == null && mHandleBehindFullscreenActivity != null;
-        }
-
-        boolean process(ActivityRecord toCheck,
-                Consumer<ActivityRecord> handleBehindFullscreenActivity) {
-            reset(toCheck, handleBehindFullscreenActivity);
-
-            if (!mHandlingOccluded && mBehindFullscreenActivity) {
-                return true;
-            }
-
-            final ActivityRecord topActivity = topRunningActivity();
-            final PooledFunction f = PooledLambda.obtainFunction(
-                    CheckBehindFullscreenActivityHelper::processActivity, this,
-                    PooledLambda.__(ActivityRecord.class), topActivity);
-            forAllActivities(f);
-            f.recycle();
-
-            return mBehindFullscreenActivity;
-        }
-
-        /** Returns {@code true} to stop the outer loop and indicate the result is computed. */
-        private boolean processActivity(ActivityRecord r, ActivityRecord topActivity) {
-            if (mAboveTop) {
-                if (r == topActivity) {
-                    if (r == mToCheck) {
-                        // It is the top activity in a visible root task.
-                        mBehindFullscreenActivity = false;
-                        return true;
-                    }
-                    mAboveTop = false;
-                }
-                mBehindFullscreenActivity |= r.occludesParent();
-                return false;
-            }
-
-            if (mHandlingOccluded) {
-                // Iterating through all occluded activities.
-                if (mBehindFullscreenActivity) {
-                    mHandleBehindFullscreenActivity.accept(r);
-                }
-            } else if (r == mToCheck) {
-                return true;
-            } else if (mBehindFullscreenActivity) {
-                // It is occluded before {@param toCheck} is found.
-                return true;
-            }
-            mBehindFullscreenActivity |= r.occludesParent();
-            return false;
-        }
-    }
 
     private final FindRootHelper mFindRootHelper = new FindRootHelper();
     private class FindRootHelper {
@@ -789,7 +569,9 @@
             mRoot = r;
 
             // Only end search if we are ignore relinquishing identity or we are not relinquishing.
-            return ignoreRelinquishIdentity || (r.info.flags & FLAG_RELINQUISH_TASK_IDENTITY) == 0;
+            return ignoreRelinquishIdentity
+                    || mNeverRelinquishIdentity
+                    || (r.info.flags & FLAG_RELINQUISH_TASK_IDENTITY) == 0;
         }
     }
 
@@ -809,28 +591,6 @@
     // false.
     private boolean mDeferTaskAppear;
 
-    /**
-     * Forces this task to be unorganized. Currently it is used for deferring the control of
-     * organizer when windowing mode is changing from PiP to fullscreen with orientation change.
-     * It is true only during Task#setWindowingMode ~ DisplayRotation#continueRotation.
-     *
-     * TODO(b/179235349): Remove this field by making surface operations from task organizer sync
-     *                    with display rotation.
-     */
-    private boolean mForceNotOrganized;
-
-    /**
-     * This task was created by the task organizer which has the following implementations.
-     * <ul>
-     *     <lis>The task won't be removed when it is empty. Removal has to be an explicit request
-     *     from the task organizer.</li>
-     *     <li>Unlike other non-root tasks, it's direct children are visible to the task
-     *     organizer for ordering purposes.</li>
-     * </ul>
-     */
-    @VisibleForTesting
-    boolean mCreatedByOrganizer;
-
     // Tracking cookie for the creation of this task.
     IBinder mLaunchCookie;
 
@@ -858,11 +618,8 @@
             IVoiceInteractionSession _voiceSession, IVoiceInteractor _voiceInteractor,
             boolean _createdByOrganizer, IBinder _launchCookie, boolean _deferTaskAppear,
             boolean _removeWithTaskOrganizer) {
-        super(atmService.mWindowManager);
+        super(atmService, null /* fragmentToken */, _createdByOrganizer, false /* isEmbedded */);
 
-        mAtmService = atmService;
-        mTaskSupervisor = atmService.mTaskSupervisor;
-        mRootWindowContainer = mAtmService.mRootWindowContainer;
         mTaskId = _taskId;
         mUserId = _userId;
         mResizeMode = resizeMode;
@@ -875,7 +632,6 @@
                 : new PersistedTaskSnapshotData();
         // Tasks have no set orientation value (including SCREEN_ORIENTATION_UNSPECIFIED).
         setOrientation(SCREEN_ORIENTATION_UNSET);
-        mRemoteToken = new RemoteToken(this);
         affinityIntent = _affinityIntent;
         affinity = _affinity;
         rootAffinity = _rootAffinity;
@@ -913,7 +669,6 @@
         mHandler = new ActivityTaskHandler(mTaskSupervisor.mLooper);
         mCurrentUser = mAtmService.mAmInternal.getCurrentUserId();
 
-        mCreatedByOrganizer = _createdByOrganizer;
         mLaunchCookie = _launchCookie;
         mDeferTaskAppear = _deferTaskAppear;
         mRemoveWithTaskOrganizer = _removeWithTaskOrganizer;
@@ -942,13 +697,13 @@
         return this;
     }
 
-    private void cleanUpResourcesForDestroy(ConfigurationContainer oldParent) {
+    private void cleanUpResourcesForDestroy(WindowContainer<?> oldParent) {
         if (hasChild()) {
             return;
         }
 
         // This task is going away, so save the last state if necessary.
-        saveLaunchingStateIfNeeded(((WindowContainer) oldParent).getDisplayContent());
+        saveLaunchingStateIfNeeded(oldParent.getDisplayContent());
 
         // TODO: VI what about activity?
         final boolean isVoiceSession = voiceSession != null;
@@ -958,7 +713,7 @@
             } catch (RemoteException e) {
             }
         }
-        if (autoRemoveFromRecents() || isVoiceSession) {
+        if (autoRemoveFromRecents(oldParent.asTaskFragment()) || isVoiceSession) {
             // Task creator asked to remove this when done, or this task was a voice
             // interaction, so it should not remain on the recent tasks list.
             mTaskSupervisor.mRecentTasks.remove(this);
@@ -1246,7 +1001,6 @@
             mCallingPackage = r.launchedFromPackage;
             mCallingFeatureId = r.launchedFromFeatureId;
             setIntent(intent != null ? intent : r.intent, info != null ? info : r.info);
-            return;
         }
         setLockTaskAuth(r);
     }
@@ -1255,7 +1009,14 @@
     private void setIntent(Intent _intent, ActivityInfo info) {
         if (!isLeafTask()) return;
 
-        mNeverRelinquishIdentity = (info.flags & FLAG_RELINQUISH_TASK_IDENTITY) == 0;
+        if (info.applicationInfo.uid == Process.SYSTEM_UID
+                || info.applicationInfo.isSystemApp()) {
+            // Only allow the apps that pre-installed on the system image to apply
+            // relinquishTaskIdentity
+            mNeverRelinquishIdentity = (info.flags & FLAG_RELINQUISH_TASK_IDENTITY) == 0;
+        } else {
+            mNeverRelinquishIdentity = true;
+        }
         affinity = info.taskAffinity;
         if (intent == null) {
             // If this task already has an intent associated with it, don't set the root
@@ -1379,11 +1140,11 @@
     }
 
     @Override
-    void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent) {
-        final DisplayContent display = newParent != null
-                ? ((WindowContainer) newParent).getDisplayContent() : null;
-        final DisplayContent oldDisplay = oldParent != null
-                ? ((WindowContainer) oldParent).getDisplayContent() : null;
+    void onParentChanged(ConfigurationContainer rawNewParent, ConfigurationContainer rawOldParent) {
+        final WindowContainer<?> newParent = (WindowContainer<?>) rawNewParent;
+        final WindowContainer<?> oldParent = (WindowContainer<?>) rawOldParent;
+        final DisplayContent display = newParent != null ? newParent.getDisplayContent() : null;
+        final DisplayContent oldDisplay = oldParent != null ? oldParent.getDisplayContent() : null;
 
         mPrevDisplayId = (oldDisplay != null) ? oldDisplay.mDisplayId : INVALID_DISPLAY;
 
@@ -1424,7 +1185,7 @@
         }
 
         if (oldParent != null) {
-            final Task oldParentTask = ((WindowContainer) oldParent).asTask();
+            final Task oldParentTask = oldParent.asTask();
             if (oldParentTask != null) {
                 final PooledConsumer c = PooledLambda.obtainConsumer(
                         Task::cleanUpActivityReferences, oldParentTask,
@@ -1442,6 +1203,12 @@
         }
 
         if (newParent != null) {
+            // Surface of Task that will not be organized should be shown by default.
+            // See Task#showSurfaceOnCreation
+            if (!mCreatedByOrganizer && !canBeOrganized()) {
+                getSyncTransaction().show(mSurfaceControl);
+            }
+
             // TODO: Ensure that this is actually necessary here
             // Notify the voice session if required
             if (voiceSession != null) {
@@ -1466,64 +1233,62 @@
         mRootWindowContainer.updateUIDsPresentOnDisplay();
     }
 
-    void cleanUpActivityReferences(ActivityRecord r) {
-        // mPausingActivity is set at leaf task
-        if (mPausingActivity != null && mPausingActivity == r) {
-            mPausingActivity = null;
+    @Override
+    @Nullable
+    ActivityRecord getTopResumedActivity() {
+        if (!isLeafTask()) {
+            for (int i = mChildren.size() - 1; i >= 0; --i) {
+                ActivityRecord resumedActivity = mChildren.get(i).asTask().getTopResumedActivity();
+                if (resumedActivity != null) {
+                    return resumedActivity;
+                }
+            }
         }
 
-        if (mResumedActivity != null && mResumedActivity == r) {
-            setResumedActivity(null, "cleanUpActivityReferences");
+        final ActivityRecord taskResumedActivity = getResumedActivity();
+        ActivityRecord topResumedActivity = null;
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            final WindowContainer child = mChildren.get(i);
+            if (child.asTaskFragment() != null) {
+                topResumedActivity = child.asTaskFragment().getTopResumedActivity();
+            } else if (taskResumedActivity != null
+                    && child.asActivityRecord() == taskResumedActivity) {
+                topResumedActivity = taskResumedActivity;
+            }
+            if (topResumedActivity != null) {
+                return topResumedActivity;
+            }
         }
-
-        final WindowContainer parent = getParent();
-        if (parent != null && parent.asTask() != null) {
-            parent.asTask().cleanUpActivityReferences(r);
-            return;
-        }
-        r.removeTimeouts();
-        mExitingActivities.remove(r);
+        return null;
     }
 
-    /** @return the currently resumed activity. */
-    ActivityRecord getResumedActivity() {
-        if (isLeafTask()) {
-            return mResumedActivity;
+    @Override
+    @Nullable
+    ActivityRecord getTopPausingActivity() {
+        if (!isLeafTask()) {
+            for (int i = mChildren.size() - 1; i >= 0; --i) {
+                ActivityRecord pausingActivity = mChildren.get(i).asTask().getTopPausingActivity();
+                if (pausingActivity != null) {
+                    return pausingActivity;
+                }
+            }
         }
 
-        final Task task = getTask(t -> t.mResumedActivity != null, true /* traverseTopToBottom */);
-        return task != null ? task.mResumedActivity : null;
-    }
-
-    @VisibleForTesting
-    void setPausingActivity(ActivityRecord pausing) {
-        mPausingActivity = pausing;
-    }
-
-    /**
-     * @return the currently pausing activity of this task or the topmost pausing activity of the
-     * child tasks
-     */
-    ActivityRecord getPausingActivity() {
-        if (isLeafTask()) {
-            return mPausingActivity;
+        final ActivityRecord taskPausingActivity = getPausingActivity();
+        ActivityRecord topPausingActivity = null;
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            final WindowContainer child = mChildren.get(i);
+            if (child.asTaskFragment() != null) {
+                topPausingActivity = child.asTaskFragment().getTopPausingActivity();
+            } else if (taskPausingActivity != null
+                    && child.asActivityRecord() == taskPausingActivity) {
+                topPausingActivity = taskPausingActivity;
+            }
+            if (topPausingActivity != null) {
+                return topPausingActivity;
+            }
         }
-
-        final Task task = getTask(t -> t.mPausingActivity != null, true /* traverseTopToBottom */);
-        return task != null ? task.mPausingActivity : null;
-    }
-
-    void setResumedActivity(ActivityRecord r, String reason) {
-        warnForNonLeafTask("setResumedActivity");
-        if (mResumedActivity == r) {
-            return;
-        }
-
-        if (ActivityTaskManagerDebugConfig.DEBUG_ROOT_TASK) Slog.d(TAG_ROOT_TASK,
-                "setResumedActivity task:" + this + " + from: "
-                + mResumedActivity + " to:" + r + " reason:" + reason);
-        mResumedActivity = r;
-        mTaskSupervisor.updateTopResumedActivityIfNeeded();
+        return null;
     }
 
     void updateTaskMovement(boolean toTop, int position) {
@@ -1562,11 +1327,6 @@
                 mTaskId, mUserId);
     }
 
-    void setAdjacentTask(Task adjacent) {
-        mAdjacentTask = adjacent;
-        adjacent.mAdjacentTask = this;
-    }
-
     void setTaskToAffiliateWith(Task taskToAffiliateWith) {
         closeRecentsChain();
         mAffiliatedTaskId = taskToAffiliateWith.mAffiliatedTaskId;
@@ -1612,14 +1372,6 @@
         return mFindRootHelper.findRoot(ignoreRelinquishIdentity, setToBottomIfNone);
     }
 
-    ActivityRecord getTopNonFinishingActivity() {
-        return getTopNonFinishingActivity(true /* includeOverlays */);
-    }
-
-    ActivityRecord getTopNonFinishingActivity(boolean includeOverlays) {
-        return getTopActivity(false /*includeFinishing*/, includeOverlays);
-    }
-
     ActivityRecord topRunningActivityLocked() {
         if (getParent() == null) {
             return null;
@@ -1646,14 +1398,6 @@
                 window.getBaseType() == TYPE_APPLICATION_STARTING) != null);
     }
 
-    ActivityRecord topActivityWithStartingWindow() {
-        if (getParent() == null) {
-            return null;
-        }
-        return getActivity((r) -> r.mStartingWindowState == STARTING_WINDOW_SHOWN
-                && r.okToShowLocked());
-    }
-
     /**
      * Return the number of running activities, and the number of non-finishing/initializing
      * activities in the provided {@param reportOut} respectively.
@@ -1675,22 +1419,7 @@
     }
 
     @Override
-    public int getActivityType() {
-        final int applicationType = super.getActivityType();
-        if (applicationType != ACTIVITY_TYPE_UNDEFINED || !hasChild()) {
-            return applicationType;
-        }
-        return getTopChild().getActivityType();
-    }
-
-    @Override
     void addChild(WindowContainer child, int index) {
-        // If this task had any child before we added this one.
-        boolean hadChild = hasChild();
-        // getActivityType() looks at the top child, so we need to read the type before adding
-        // a new child in case the new child is on top and UNDEFINED.
-        final int activityType = getActivityType();
-
         index = getAdjustedChildPosition(child, index);
         super.addChild(child, index);
 
@@ -1706,10 +1435,17 @@
         // now that this record is in a new task.
         mRootWindowContainer.updateUIDsPresentOnDisplay();
 
-        final ActivityRecord r = child.asActivityRecord();
-        if (r == null) return;
+        // Only pass minimum dimensions for pure TaskFragment. Task's minimum dimensions must be
+        // passed from Task constructor.
+        final TaskFragment childTaskFrag = child.asTaskFragment();
+        if (childTaskFrag != null && childTaskFrag.asTask() == null) {
+            childTaskFrag.setMinDimensions(mMinWidth, mMinHeight);
+        }
+    }
 
-        r.inHistory = true;
+    /** Called when an {@link ActivityRecord} is added as a descendant */
+    void onDescendantActivityAdded(boolean hadChild, int activityType, ActivityRecord r) {
+        warnForNonLeafTask("onDescendantActivityAdded");
 
         // Only set this based on the first activity
         if (!hadChild) {
@@ -1736,10 +1472,6 @@
         updateEffectiveIntent();
     }
 
-    void addChild(ActivityRecord r) {
-        addChild(r, Integer.MAX_VALUE /* add on top */);
-    }
-
     @Override
     void removeChild(WindowContainer child) {
         removeChild(child, "removeChild");
@@ -1759,7 +1491,7 @@
         if (DEBUG_TASK_MOVEMENT) {
             Slog.d(TAG_WM, "removeChild: child=" + r + " reason=" + reason);
         }
-        super.removeChild(r);
+        super.removeChild(r, false /* removeSelfIfPossible */);
 
         if (inPinnedWindowingMode()) {
             // We normally notify listeners of task stack changes on pause, however root pinned task
@@ -1789,7 +1521,10 @@
             // Remove entire task if it doesn't have any activity left and it isn't marked for reuse
             // or created by task organizer.
             if (!isRootTask()) {
-                getRootTask().removeChild(this, reason);
+                final WindowContainer<?> parent = getParent();
+                if (parent != null) {
+                    parent.asTaskFragment().removeChild(this);
+                }
             }
             EventLogTags.writeWmTaskRemoved(mTaskId,
                     "removeChild:" + reason + " last r=" + r + " in t=" + this);
@@ -1821,11 +1556,12 @@
         return count > 0;
     }
 
-    private boolean autoRemoveFromRecents() {
+    private boolean autoRemoveFromRecents(TaskFragment oldParentFragment) {
         // We will automatically remove the task either if it has explicitly asked for
         // this, or it is empty and has never contained an activity that got shown to
-        // the user.
-        return autoRemoveRecents || (!hasChild() && !getHasBeenVisible());
+        // the user, or it was being embedded in another Task.
+        return autoRemoveRecents || (!hasChild() && !getHasBeenVisible()
+                || (oldParentFragment != null && oldParentFragment.isEmbedded()));
     }
 
     private void clearPinnedTaskIfNeed() {
@@ -1849,9 +1585,15 @@
         } else {
             forAllActivities((r) -> {
                 if (r.finishing) return;
-                // TODO: figure-out how to avoid object creation due to capture of reason variable.
-                r.finishIfPossible(Activity.RESULT_CANCELED,
-                        null /* resultData */, null /* resultGrants */, reason, false /* oomAdj */);
+                // Prevent the transition from being executed too early if the top activity is
+                // resumed but the mVisibleRequested of any other activity is true, the transition
+                // should wait until next activity resumed.
+                if (r.isState(RESUMED) || (r.isVisible()
+                        && !mDisplayContent.mAppTransition.containsTransitRequest(TRANSIT_CLOSE))) {
+                    r.finishIfPossible(reason, false /* oomAdj */);
+                } else {
+                    r.destroyIfPossible(reason);
+                }
             });
         }
     }
@@ -1985,32 +1727,6 @@
                 && supportsMultiWindowInDisplayArea(tda);
     }
 
-    boolean supportsMultiWindow() {
-        return supportsMultiWindowInDisplayArea(getDisplayArea());
-    }
-
-    /**
-     * @return whether this task supports multi-window if it is in the given
-     *         {@link TaskDisplayArea}.
-     */
-    boolean supportsMultiWindowInDisplayArea(@Nullable TaskDisplayArea tda) {
-        if (!mAtmService.mSupportsMultiWindow) {
-            return false;
-        }
-        if (tda == null) {
-            Slog.w(TAG_TASKS, "Can't find TaskDisplayArea to determine support for multi"
-                    + " window. Task id=" + mTaskId + " attached=" + isAttached());
-            return false;
-        }
-
-        if (!isResizeable() && !tda.supportsNonResizableMultiWindow()) {
-            // Not support non-resizable in multi window.
-            return false;
-        }
-
-        return tda.supportsActivityMinWidthHeightMultiWindow(mMinWidth, mMinHeight);
-    }
-
     /**
      * Check whether this task can be launched on the specified display.
      *
@@ -2146,60 +1862,6 @@
         }
     }
 
-    void adjustForMinimalTaskDimensions(@NonNull Rect bounds, @NonNull Rect previousBounds,
-            @NonNull Configuration parentConfig) {
-        int minWidth = mMinWidth;
-        int minHeight = mMinHeight;
-        // If the task has no requested minimal size, we'd like to enforce a minimal size
-        // so that the user can not render the task too small to manipulate. We don't need
-        // to do this for the root pinned task as the bounds are controlled by the system.
-        if (!inPinnedWindowingMode()) {
-            final int defaultMinSizeDp = mRootWindowContainer.mDefaultMinSizeOfResizeableTaskDp;
-            final float density = (float) parentConfig.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
-            final int defaultMinSize = (int) (defaultMinSizeDp * density);
-
-            if (minWidth == INVALID_MIN_SIZE) {
-                minWidth = defaultMinSize;
-            }
-            if (minHeight == INVALID_MIN_SIZE) {
-                minHeight = defaultMinSize;
-            }
-        }
-        if (bounds.isEmpty()) {
-            // If inheriting parent bounds, check if parent bounds adhere to minimum size. If they
-            // do, we can just skip.
-            final Rect parentBounds = parentConfig.windowConfiguration.getBounds();
-            if (parentBounds.width() >= minWidth && parentBounds.height() >= minHeight) {
-                return;
-            }
-            bounds.set(parentBounds);
-        }
-        final boolean adjustWidth = minWidth > bounds.width();
-        final boolean adjustHeight = minHeight > bounds.height();
-        if (!(adjustWidth || adjustHeight)) {
-            return;
-        }
-
-        if (adjustWidth) {
-            if (!previousBounds.isEmpty() && bounds.right == previousBounds.right) {
-                bounds.left = bounds.right - minWidth;
-            } else {
-                // Either left bounds match, or neither match, or the previous bounds were
-                // fullscreen and we default to keeping left.
-                bounds.right = bounds.left + minWidth;
-            }
-        }
-        if (adjustHeight) {
-            if (!previousBounds.isEmpty() && bounds.bottom == previousBounds.bottom) {
-                bounds.top = bounds.bottom - minHeight;
-            } else {
-                // Either top bounds match, or neither match, or the previous bounds were
-                // fullscreen and we default to keeping top.
-                bounds.bottom = bounds.top + minHeight;
-            }
-        }
-    }
-
     void setLastNonFullscreenBounds(Rect bounds) {
         if (mLastNonFullscreenBounds == null) {
             mLastNonFullscreenBounds = new Rect(bounds);
@@ -2208,32 +1870,6 @@
         }
     }
 
-    /**
-     * This should be called when an child activity changes state. This should only
-     * be called from
-     * {@link ActivityRecord#setState(ActivityState, String)} .
-     * @param record The {@link ActivityRecord} whose state has changed.
-     * @param state The new state.
-     * @param reason The reason for the change.
-     */
-    void onActivityStateChanged(ActivityRecord record, ActivityState state, String reason) {
-        warnForNonLeafTask("onActivityStateChanged");
-        if (record == mResumedActivity && state != RESUMED) {
-            setResumedActivity(null, reason + " - onActivityStateChanged");
-        }
-
-        if (state == RESUMED) {
-            if (ActivityTaskManagerDebugConfig.DEBUG_ROOT_TASK) {
-                Slog.v(TAG_ROOT_TASK, "set resumed activity to:" + record + " reason:" + reason);
-            }
-            setResumedActivity(record, reason + " - onActivityStateChanged");
-            if (record == mRootWindowContainer.getTopResumedActivity()) {
-                mAtmService.setResumedActivityUncheckLocked(record, reason);
-            }
-            mTaskSupervisor.mRecentTasks.add(record.getTask());
-        }
-    }
-
     private void onConfigurationChangedInner(Configuration newParentConfig) {
         // Check if the new configuration supports persistent bounds (eg. is Freeform) and if so
         // restore the last recorded non-fullscreen bounds.
@@ -2281,19 +1917,17 @@
             }
         }
 
-        if (pipChanging) {
-            // If the top activity is using fixed rotation, it should be changing from PiP to
-            // fullscreen with display orientation change. Do not notify fullscreen task organizer
-            // because the restoration of task surface and the transformation of activity surface
-            // need to be done synchronously.
+        if (pipChanging && wasInPictureInPicture) {
+            // If the top activity is changing from PiP to fullscreen with fixed rotation,
+            // clear the crop and rotation matrix of task because fixed rotation will handle
+            // the transformation on activity level. This also avoids flickering caused by the
+            // latency of fullscreen task organizer configuring the surface.
             final ActivityRecord r = topRunningActivity();
             if (r != null && mDisplayContent.isFixedRotationLaunchingApp(r)) {
-                mForceNotOrganized = true;
+                getSyncTransaction().setWindowCrop(mSurfaceControl, null)
+                        .setCornerRadius(mSurfaceControl, 0f)
+                        .setMatrix(mSurfaceControl, Matrix.IDENTITY_MATRIX, new float[9]);
             }
-        } else {
-            // If the display orientation change is done, let the corresponding task organizer take
-            // back the control of this task.
-            mForceNotOrganized = false;
         }
 
         saveLaunchingStateIfNeeded();
@@ -2352,14 +1986,7 @@
             taskDisplayArea.onRootTaskWindowingModeChanged(this);
         }
 
-        if (mDisplayContent == null) {
-            return;
-        }
-
-        // Use override windowing mode to prevent extra bounds changes if inheriting the mode.
-        final int overrideWindowingMode = getRequestedOverrideWindowingMode();
-        if (overrideWindowingMode != WINDOWING_MODE_PINNED
-                && !getRequestedOverrideBounds().isEmpty()) {
+        if (!isOrganized() && !getRequestedOverrideBounds().isEmpty() && mDisplayContent != null) {
             // If the parent (display) has rotated, rotate our bounds to best-fit where their
             // bounds were on the pre-rotated display.
             final int newRotation = getWindowConfiguration().getRotation();
@@ -2378,14 +2005,154 @@
         }
     }
 
-    /**
-     * Initializes a change transition. See {@link SurfaceFreezer} for more information.
-     */
-    private void initializeChangeTransition(Rect startBounds) {
-        mDisplayContent.prepareAppTransition(TRANSIT_CHANGE);
-        mDisplayContent.mChangingContainers.add(this);
+    void resolveLeafTaskOnlyOverrideConfigs(Configuration newParentConfig, Rect previousBounds) {
+        if (!isLeafTask()) {
+            return;
+        }
 
-        mSurfaceFreezer.freeze(getPendingTransaction(), startBounds);
+        int windowingMode =
+                getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
+        if (windowingMode == WINDOWING_MODE_UNDEFINED) {
+            windowingMode = newParentConfig.windowConfiguration.getWindowingMode();
+        }
+        // Commit the resolved windowing mode so the canSpecifyOrientation won't get the old
+        // mode that may cause the bounds to be miscalculated, e.g. letterboxed.
+        getConfiguration().windowConfiguration.setWindowingMode(windowingMode);
+        Rect outOverrideBounds = getResolvedOverrideConfiguration().windowConfiguration.getBounds();
+
+        if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
+            // Use empty bounds to indicate "fill parent".
+            outOverrideBounds.setEmpty();
+            // The bounds for fullscreen mode shouldn't be adjusted by minimal size. Otherwise if
+            // the parent or display is smaller than the size, the content may be cropped.
+            return;
+        }
+
+        adjustForMinimalTaskDimensions(outOverrideBounds, previousBounds, newParentConfig);
+        if (windowingMode == WINDOWING_MODE_FREEFORM) {
+            computeFreeformBounds(outOverrideBounds, newParentConfig);
+            return;
+        }
+    }
+
+    void adjustForMinimalTaskDimensions(@NonNull Rect bounds, @NonNull Rect previousBounds,
+            @NonNull Configuration parentConfig) {
+        int minWidth = mMinWidth;
+        int minHeight = mMinHeight;
+        // If the task has no requested minimal size, we'd like to enforce a minimal size
+        // so that the user can not render the task fragment too small to manipulate. We don't need
+        // to do this for the root pinned task as the bounds are controlled by the system.
+        if (!inPinnedWindowingMode()) {
+            final int defaultMinSizeDp = mRootWindowContainer.mDefaultMinSizeOfResizeableTaskDp;
+            final float density = (float) parentConfig.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+            final int defaultMinSize = (int) (defaultMinSizeDp * density);
+
+            if (minWidth == INVALID_MIN_SIZE) {
+                minWidth = defaultMinSize;
+            }
+            if (minHeight == INVALID_MIN_SIZE) {
+                minHeight = defaultMinSize;
+            }
+        }
+        if (bounds.isEmpty()) {
+            // If inheriting parent bounds, check if parent bounds adhere to minimum size. If they
+            // do, we can just skip.
+            final Rect parentBounds = parentConfig.windowConfiguration.getBounds();
+            if (parentBounds.width() >= minWidth && parentBounds.height() >= minHeight) {
+                return;
+            }
+            bounds.set(parentBounds);
+        }
+        final boolean adjustWidth = minWidth > bounds.width();
+        final boolean adjustHeight = minHeight > bounds.height();
+        if (!(adjustWidth || adjustHeight)) {
+            return;
+        }
+
+        if (adjustWidth) {
+            if (!previousBounds.isEmpty() && bounds.right == previousBounds.right) {
+                bounds.left = bounds.right - minWidth;
+            } else {
+                // Either left bounds match, or neither match, or the previous bounds were
+                // fullscreen and we default to keeping left.
+                bounds.right = bounds.left + minWidth;
+            }
+        }
+        if (adjustHeight) {
+            if (!previousBounds.isEmpty() && bounds.bottom == previousBounds.bottom) {
+                bounds.top = bounds.bottom - minHeight;
+            } else {
+                // Either top bounds match, or neither match, or the previous bounds were
+                // fullscreen and we default to keeping top.
+                bounds.bottom = bounds.top + minHeight;
+            }
+        }
+    }
+
+    /** Computes bounds for {@link WindowConfiguration#WINDOWING_MODE_FREEFORM}. */
+    private void computeFreeformBounds(@NonNull Rect outBounds,
+            @NonNull Configuration newParentConfig) {
+        // by policy, make sure the window remains within parent somewhere
+        final float density =
+                ((float) newParentConfig.densityDpi) / DisplayMetrics.DENSITY_DEFAULT;
+        final Rect parentBounds =
+                new Rect(newParentConfig.windowConfiguration.getBounds());
+        final DisplayContent display = getDisplayContent();
+        if (display != null) {
+            // If a freeform window moves below system bar, there is no way to move it again
+            // by touch. Because its caption is covered by system bar. So we exclude them
+            // from root task bounds. and then caption will be shown inside stable area.
+            final Rect stableBounds = new Rect();
+            display.getStableRect(stableBounds);
+            parentBounds.intersect(stableBounds);
+        }
+
+        fitWithinBounds(outBounds, parentBounds,
+                (int) (density * WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP),
+                (int) (density * WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP));
+
+        // Prevent to overlap caption with stable insets.
+        final int offsetTop = parentBounds.top - outBounds.top;
+        if (offsetTop > 0) {
+            outBounds.offset(0, offsetTop);
+        }
+    }
+
+    /**
+     * Adjusts bounds to stay within root task bounds.
+     *
+     * Since bounds might be outside of root task bounds, this method tries to move the bounds in
+     * a way that keep them unchanged, but be contained within the root task bounds.
+     *
+     * @param bounds Bounds to be adjusted.
+     * @param rootTaskBounds Bounds within which the other bounds should remain.
+     * @param overlapPxX The amount of px required to be visible in the X dimension.
+     * @param overlapPxY The amount of px required to be visible in the Y dimension.
+     */
+    private static void fitWithinBounds(Rect bounds, Rect rootTaskBounds, int overlapPxX,
+            int overlapPxY) {
+        if (rootTaskBounds == null || rootTaskBounds.isEmpty() || rootTaskBounds.contains(bounds)) {
+            return;
+        }
+
+        // For each side of the parent (eg. left), check if the opposing side of the window (eg.
+        // right) is at least overlap pixels away. If less, offset the window by that difference.
+        int horizontalDiff = 0;
+        // If window is smaller than overlap, use it's smallest dimension instead
+        int overlapLR = Math.min(overlapPxX, bounds.width());
+        if (bounds.right < (rootTaskBounds.left + overlapLR)) {
+            horizontalDiff = overlapLR - (bounds.right - rootTaskBounds.left);
+        } else if (bounds.left > (rootTaskBounds.right - overlapLR)) {
+            horizontalDiff = -(overlapLR - (rootTaskBounds.right - bounds.left));
+        }
+        int verticalDiff = 0;
+        int overlapTB = Math.min(overlapPxY, bounds.width());
+        if (bounds.bottom < (rootTaskBounds.top + overlapTB)) {
+            verticalDiff = overlapTB - (bounds.bottom - rootTaskBounds.top);
+        } else if (bounds.top > (rootTaskBounds.bottom - overlapTB)) {
+            verticalDiff = -(overlapTB - (rootTaskBounds.bottom - bounds.top));
+        }
+        bounds.offset(horizontalDiff, verticalDiff);
     }
 
     private boolean shouldStartChangeTransition(int prevWinMode, int newWinMode) {
@@ -2526,400 +2293,6 @@
         mTaskSupervisor.mLaunchParamsPersister.saveTask(this, display);
     }
 
-    /**
-     * Adjust bounds to stay within root task bounds.
-     *
-     * Since bounds might be outside of root task bounds, this method tries to move the bounds in
-     * a way that keep them unchanged, but be contained within the root task bounds.
-     *
-     * @param bounds Bounds to be adjusted.
-     * @param rootTaskBounds Bounds within which the other bounds should remain.
-     * @param overlapPxX The amount of px required to be visible in the X dimension.
-     * @param overlapPxY The amount of px required to be visible in the Y dimension.
-     */
-    private static void fitWithinBounds(Rect bounds, Rect rootTaskBounds, int overlapPxX,
-            int overlapPxY) {
-        if (rootTaskBounds == null || rootTaskBounds.isEmpty() || rootTaskBounds.contains(bounds)) {
-            return;
-        }
-
-        // For each side of the parent (eg. left), check if the opposing side of the window (eg.
-        // right) is at least overlap pixels away. If less, offset the window by that difference.
-        int horizontalDiff = 0;
-        // If window is smaller than overlap, use it's smallest dimension instead
-        int overlapLR = Math.min(overlapPxX, bounds.width());
-        if (bounds.right < (rootTaskBounds.left + overlapLR)) {
-            horizontalDiff = overlapLR - (bounds.right - rootTaskBounds.left);
-        } else if (bounds.left > (rootTaskBounds.right - overlapLR)) {
-            horizontalDiff = -(overlapLR - (rootTaskBounds.right - bounds.left));
-        }
-        int verticalDiff = 0;
-        int overlapTB = Math.min(overlapPxY, bounds.width());
-        if (bounds.bottom < (rootTaskBounds.top + overlapTB)) {
-            verticalDiff = overlapTB - (bounds.bottom - rootTaskBounds.top);
-        } else if (bounds.top > (rootTaskBounds.bottom - overlapTB)) {
-            verticalDiff = -(overlapTB - (rootTaskBounds.bottom - bounds.top));
-        }
-        bounds.offset(horizontalDiff, verticalDiff);
-    }
-
-    /**
-     * Intersects inOutBounds with intersectBounds-intersectInsets. If inOutBounds is larger than
-     * intersectBounds on a side, then the respective side will not be intersected.
-     *
-     * The assumption is that if inOutBounds is initially larger than intersectBounds, then the
-     * inset on that side is no-longer applicable. This scenario happens when a task's minimal
-     * bounds are larger than the provided parent/display bounds.
-     *
-     * @param inOutBounds the bounds to intersect.
-     * @param intersectBounds the bounds to intersect with.
-     * @param intersectInsets insets to apply to intersectBounds before intersecting.
-     */
-    static void intersectWithInsetsIfFits(
-            Rect inOutBounds, Rect intersectBounds, Rect intersectInsets) {
-        if (inOutBounds.right <= intersectBounds.right) {
-            inOutBounds.right =
-                    Math.min(intersectBounds.right - intersectInsets.right, inOutBounds.right);
-        }
-        if (inOutBounds.bottom <= intersectBounds.bottom) {
-            inOutBounds.bottom =
-                    Math.min(intersectBounds.bottom - intersectInsets.bottom, inOutBounds.bottom);
-        }
-        if (inOutBounds.left >= intersectBounds.left) {
-            inOutBounds.left =
-                    Math.max(intersectBounds.left + intersectInsets.left, inOutBounds.left);
-        }
-        if (inOutBounds.top >= intersectBounds.top) {
-            inOutBounds.top =
-                    Math.max(intersectBounds.top + intersectInsets.top, inOutBounds.top);
-        }
-    }
-
-    /**
-     * Gets bounds with non-decor and stable insets applied respectively.
-     *
-     * If bounds overhangs the display, those edges will not get insets. See
-     * {@link #intersectWithInsetsIfFits}
-     *
-     * @param outNonDecorBounds where to place bounds with non-decor insets applied.
-     * @param outStableBounds where to place bounds with stable insets applied.
-     * @param bounds the bounds to inset.
-     */
-    private void calculateInsetFrames(Rect outNonDecorBounds, Rect outStableBounds, Rect bounds,
-            DisplayInfo displayInfo) {
-        outNonDecorBounds.set(bounds);
-        outStableBounds.set(bounds);
-        final Task rootTask = getRootTask();
-        if (rootTask == null || rootTask.mDisplayContent == null) {
-            return;
-        }
-        mTmpBounds.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight);
-
-        final DisplayPolicy policy = rootTask.mDisplayContent.getDisplayPolicy();
-        policy.getNonDecorInsetsLw(displayInfo.rotation, displayInfo.logicalWidth,
-                displayInfo.logicalHeight, displayInfo.displayCutout, mTmpInsets);
-        intersectWithInsetsIfFits(outNonDecorBounds, mTmpBounds, mTmpInsets);
-
-        policy.convertNonDecorInsetsToStableInsets(mTmpInsets, displayInfo.rotation);
-        intersectWithInsetsIfFits(outStableBounds, mTmpBounds, mTmpInsets);
-    }
-
-    /**
-     * Forces the app bounds related configuration can be computed by
-     * {@link #computeConfigResourceOverrides(Configuration, Configuration, DisplayInfo,
-     * ActivityRecord.CompatDisplayInsets)}.
-     */
-    private static void invalidateAppBoundsConfig(@NonNull Configuration inOutConfig) {
-        final Rect appBounds = inOutConfig.windowConfiguration.getAppBounds();
-        if (appBounds != null) {
-            appBounds.setEmpty();
-        }
-        inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
-        inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
-    }
-
-    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
-            @NonNull Configuration parentConfig, @Nullable DisplayInfo overrideDisplayInfo) {
-        if (overrideDisplayInfo != null) {
-            // Make sure the screen related configs can be computed by the provided display info.
-            inOutConfig.screenLayout = Configuration.SCREENLAYOUT_UNDEFINED;
-            invalidateAppBoundsConfig(inOutConfig);
-        }
-        computeConfigResourceOverrides(inOutConfig, parentConfig, overrideDisplayInfo,
-                null /* compatInsets */);
-    }
-
-    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
-            @NonNull Configuration parentConfig) {
-        computeConfigResourceOverrides(inOutConfig, parentConfig, null /* overrideDisplayInfo */,
-                null /* compatInsets */);
-    }
-
-    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
-            @NonNull Configuration parentConfig,
-            @Nullable ActivityRecord.CompatDisplayInsets compatInsets) {
-        if (compatInsets != null) {
-            // Make sure the app bounds can be computed by the compat insets.
-            invalidateAppBoundsConfig(inOutConfig);
-        }
-        computeConfigResourceOverrides(inOutConfig, parentConfig, null /* overrideDisplayInfo */,
-                compatInsets);
-    }
-
-    /**
-     * Calculates configuration values used by the client to get resources. This should be run
-     * using app-facing bounds (bounds unmodified by animations or transient interactions).
-     *
-     * This assumes bounds are non-empty/null. For the null-bounds case, the caller is likely
-     * configuring an "inherit-bounds" window which means that all configuration settings would
-     * just be inherited from the parent configuration.
-     **/
-    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
-            @NonNull Configuration parentConfig, @Nullable DisplayInfo overrideDisplayInfo,
-            @Nullable ActivityRecord.CompatDisplayInsets compatInsets) {
-        int windowingMode = inOutConfig.windowConfiguration.getWindowingMode();
-        if (windowingMode == WINDOWING_MODE_UNDEFINED) {
-            windowingMode = parentConfig.windowConfiguration.getWindowingMode();
-        }
-
-        float density = inOutConfig.densityDpi;
-        if (density == Configuration.DENSITY_DPI_UNDEFINED) {
-            density = parentConfig.densityDpi;
-        }
-        density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
-
-        // The bounds may have been overridden at this level. If the parent cannot cover these
-        // bounds, the configuration is still computed according to the override bounds.
-        final boolean insideParentBounds;
-
-        final Rect parentBounds = parentConfig.windowConfiguration.getBounds();
-        final Rect resolvedBounds = inOutConfig.windowConfiguration.getBounds();
-        if (resolvedBounds == null || resolvedBounds.isEmpty()) {
-            mTmpFullBounds.set(parentBounds);
-            insideParentBounds = true;
-        } else {
-            mTmpFullBounds.set(resolvedBounds);
-            insideParentBounds = parentBounds.contains(resolvedBounds);
-        }
-
-        // Non-null compatibility insets means the activity prefers to keep its original size, so
-        // out bounds doesn't need to be restricted by the parent or current display
-        final boolean customContainerPolicy = compatInsets != null;
-
-        Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
-        if (outAppBounds == null || outAppBounds.isEmpty()) {
-            // App-bounds hasn't been overridden, so calculate a value for it.
-            inOutConfig.windowConfiguration.setAppBounds(mTmpFullBounds);
-            outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
-
-            if (!customContainerPolicy && windowingMode != WINDOWING_MODE_FREEFORM) {
-                final Rect containingAppBounds;
-                if (insideParentBounds) {
-                    containingAppBounds = parentConfig.windowConfiguration.getAppBounds();
-                } else {
-                    // Restrict appBounds to display non-decor rather than parent because the
-                    // override bounds are beyond the parent. Otherwise, it won't match the
-                    // overridden bounds.
-                    final TaskDisplayArea displayArea = getDisplayArea();
-                    containingAppBounds = displayArea != null
-                            ? displayArea.getWindowConfiguration().getAppBounds() : null;
-                }
-                if (containingAppBounds != null && !containingAppBounds.isEmpty()) {
-                    outAppBounds.intersect(containingAppBounds);
-                }
-            }
-        }
-
-        if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED
-                || inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
-            if (!customContainerPolicy && WindowConfiguration.isFloating(windowingMode)) {
-                mTmpNonDecorBounds.set(mTmpFullBounds);
-                mTmpStableBounds.set(mTmpFullBounds);
-            } else if (!customContainerPolicy
-                    && (overrideDisplayInfo != null || getDisplayContent() != null)) {
-                final DisplayInfo di = overrideDisplayInfo != null
-                        ? overrideDisplayInfo
-                        : getDisplayContent().getDisplayInfo();
-
-                // For calculating screenWidthDp, screenWidthDp, we use the stable inset screen
-                // area, i.e. the screen area without the system bars.
-                // The non decor inset are areas that could never be removed in Honeycomb. See
-                // {@link WindowManagerPolicy#getNonDecorInsetsLw}.
-                calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, mTmpFullBounds, di);
-            } else {
-                // Apply the given non-decor and stable insets to calculate the corresponding bounds
-                // for screen size of configuration.
-                int rotation = inOutConfig.windowConfiguration.getRotation();
-                if (rotation == ROTATION_UNDEFINED) {
-                    rotation = parentConfig.windowConfiguration.getRotation();
-                }
-                if (rotation != ROTATION_UNDEFINED && customContainerPolicy) {
-                    mTmpNonDecorBounds.set(mTmpFullBounds);
-                    mTmpStableBounds.set(mTmpFullBounds);
-                    compatInsets.getBoundsByRotation(mTmpBounds, rotation);
-                    intersectWithInsetsIfFits(mTmpNonDecorBounds, mTmpBounds,
-                            compatInsets.mNonDecorInsets[rotation]);
-                    intersectWithInsetsIfFits(mTmpStableBounds, mTmpBounds,
-                            compatInsets.mStableInsets[rotation]);
-                    outAppBounds.set(mTmpNonDecorBounds);
-                } else {
-                    // Set to app bounds because it excludes decor insets.
-                    mTmpNonDecorBounds.set(outAppBounds);
-                    mTmpStableBounds.set(outAppBounds);
-                }
-            }
-
-            if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
-                final int overrideScreenWidthDp = (int) (mTmpStableBounds.width() / density);
-                inOutConfig.screenWidthDp = (insideParentBounds && !customContainerPolicy)
-                        ? Math.min(overrideScreenWidthDp, parentConfig.screenWidthDp)
-                        : overrideScreenWidthDp;
-            }
-            if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
-                final int overrideScreenHeightDp = (int) (mTmpStableBounds.height() / density);
-                inOutConfig.screenHeightDp = (insideParentBounds && !customContainerPolicy)
-                        ? Math.min(overrideScreenHeightDp, parentConfig.screenHeightDp)
-                        : overrideScreenHeightDp;
-            }
-
-            if (inOutConfig.smallestScreenWidthDp
-                    == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
-                if (WindowConfiguration.isFloating(windowingMode)) {
-                    // For floating tasks, calculate the smallest width from the bounds of the task
-                    inOutConfig.smallestScreenWidthDp = (int) (
-                            Math.min(mTmpFullBounds.width(), mTmpFullBounds.height()) / density);
-                }
-                // otherwise, it will just inherit
-            }
-        }
-
-        if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
-            inOutConfig.orientation = (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
-                    ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
-        }
-        if (inOutConfig.screenLayout == Configuration.SCREENLAYOUT_UNDEFINED) {
-            // For calculating screen layout, we need to use the non-decor inset screen area for the
-            // calculation for compatibility reasons, i.e. screen area without system bars that
-            // could never go away in Honeycomb.
-            int compatScreenWidthDp = (int) (mTmpNonDecorBounds.width() / density);
-            int compatScreenHeightDp = (int) (mTmpNonDecorBounds.height() / density);
-            // Use overrides if provided. If both overrides are provided, mTmpNonDecorBounds is
-            // undefined so it can't be used.
-            if (inOutConfig.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
-                compatScreenWidthDp = inOutConfig.screenWidthDp;
-            }
-            if (inOutConfig.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
-                compatScreenHeightDp = inOutConfig.screenHeightDp;
-            }
-            // Reducing the screen layout starting from its parent config.
-            inOutConfig.screenLayout = computeScreenLayoutOverride(parentConfig.screenLayout,
-                    compatScreenWidthDp, compatScreenHeightDp);
-        }
-    }
-
-    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
-    static int computeScreenLayoutOverride(int sourceScreenLayout, int screenWidthDp,
-            int screenHeightDp) {
-        sourceScreenLayout = sourceScreenLayout
-                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
-        final int longSize = Math.max(screenWidthDp, screenHeightDp);
-        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
-        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
-    }
-
-    @Override
-    void resolveOverrideConfiguration(Configuration newParentConfig) {
-        mTmpBounds.set(getResolvedOverrideConfiguration().windowConfiguration.getBounds());
-        super.resolveOverrideConfiguration(newParentConfig);
-
-        int windowingMode =
-                getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
-        final int parentWindowingMode = newParentConfig.windowConfiguration.getWindowingMode();
-
-        // Resolve override windowing mode to fullscreen for home task (even on freeform
-        // display), or split-screen if in split-screen mode.
-        if (getActivityType() == ACTIVITY_TYPE_HOME && windowingMode == WINDOWING_MODE_UNDEFINED) {
-            windowingMode = WindowConfiguration.isSplitScreenWindowingMode(parentWindowingMode)
-                    ? parentWindowingMode : WINDOWING_MODE_FULLSCREEN;
-            getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode);
-        }
-
-        // Do not allow tasks not support multi window to be in a multi-window mode, unless it is in
-        // pinned windowing mode.
-        if (!supportsMultiWindow()) {
-            final int candidateWindowingMode =
-                    windowingMode != WINDOWING_MODE_UNDEFINED ? windowingMode : parentWindowingMode;
-            if (WindowConfiguration.inMultiWindowMode(candidateWindowingMode)
-                    && candidateWindowingMode != WINDOWING_MODE_PINNED) {
-                getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(
-                        WINDOWING_MODE_FULLSCREEN);
-            }
-        }
-
-        if (isLeafTask()) {
-            resolveLeafOnlyOverrideConfigs(newParentConfig, mTmpBounds /* previousBounds */);
-        }
-        computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
-    }
-
-    private void resolveLeafOnlyOverrideConfigs(Configuration newParentConfig,
-            Rect previousBounds) {
-
-        int windowingMode =
-                getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
-        if (windowingMode == WINDOWING_MODE_UNDEFINED) {
-            windowingMode = newParentConfig.windowConfiguration.getWindowingMode();
-        }
-        // Commit the resolved windowing mode so the canSpecifyOrientation won't get the old
-        // mode that may cause the bounds to be miscalculated, e.g. letterboxed.
-        getConfiguration().windowConfiguration.setWindowingMode(windowingMode);
-        Rect outOverrideBounds =
-                getResolvedOverrideConfiguration().windowConfiguration.getBounds();
-
-        if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
-            // Use empty bounds to indicate "fill parent".
-            outOverrideBounds.setEmpty();
-            // The bounds for fullscreen mode shouldn't be adjusted by minimal size. Otherwise if
-            // the parent or display is smaller than the size, the content may be cropped.
-            return;
-        }
-
-        adjustForMinimalTaskDimensions(outOverrideBounds, previousBounds, newParentConfig);
-        if (windowingMode == WINDOWING_MODE_FREEFORM) {
-            computeFreeformBounds(outOverrideBounds, newParentConfig);
-            return;
-        }
-    }
-
-    /** Computes bounds for {@link WindowConfiguration#WINDOWING_MODE_FREEFORM}. */
-    private void computeFreeformBounds(@NonNull Rect outBounds,
-            @NonNull Configuration newParentConfig) {
-        // by policy, make sure the window remains within parent somewhere
-        final float density =
-                ((float) newParentConfig.densityDpi) / DisplayMetrics.DENSITY_DEFAULT;
-        final Rect parentBounds =
-                new Rect(newParentConfig.windowConfiguration.getBounds());
-        final DisplayContent display = getDisplayContent();
-        if (display != null) {
-            // If a freeform window moves below system bar, there is no way to move it again
-            // by touch. Because its caption is covered by system bar. So we exclude them
-            // from root task bounds. and then caption will be shown inside stable area.
-            final Rect stableBounds = new Rect();
-            display.getStableRect(stableBounds);
-            parentBounds.intersect(stableBounds);
-        }
-
-        fitWithinBounds(outBounds, parentBounds,
-                (int) (density * WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP),
-                (int) (density * WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP));
-
-        // Prevent to overlap caption with stable insets.
-        final int offsetTop = parentBounds.top - outBounds.top;
-        if (offsetTop > 0) {
-            outBounds.offset(0, offsetTop);
-        }
-    }
-
     Rect updateOverrideConfigurationFromLaunchBounds() {
         // If the task is controlled by another organized task, do not set override
         // configurations and let its parent (organized task) to control it;
@@ -2968,24 +2341,11 @@
         }
     }
 
-    int getDisplayId() {
-        final DisplayContent dc = getDisplayContent();
-        return dc != null ? dc.mDisplayId : INVALID_DISPLAY;
-    }
-
     /** @return Id of root task. */
     int getRootTaskId() {
         return getRootTask().mTaskId;
     }
 
-    Task getRootTask() {
-        final WindowContainer parent = getParent();
-        if (parent == null) return this;
-
-        final Task parentTask = parent.asTask();
-        return parentTask == null ? this : parentTask.getRootTask();
-    }
-
     /** @return the first organized task. */
     @Nullable
     Task getOrganizedTask() {
@@ -3098,12 +2458,12 @@
         // and focused application if needed.
         focusableTask.moveToFront(myReason);
         // Top display focused root task is changed, update top resumed activity if needed.
-        if (rootTask.getResumedActivity() != null) {
+        if (rootTask.getTopResumedActivity() != null) {
             mTaskSupervisor.updateTopResumedActivityIfNeeded();
             // Set focused app directly because if the next focused activity is already resumed
             // (e.g. the next top activity is on a different display), there won't have activity
             // state change to update it.
-            mAtmService.setResumedActivityUncheckLocked(rootTask.getResumedActivity(), reason);
+            mAtmService.setResumedActivityUncheckLocked(rootTask.getTopResumedActivity(), reason);
         }
         return rootTask;
     }
@@ -3152,17 +2512,16 @@
 
         // Figure-out min/max possible position depending on if child can show for current user.
         int minPosition = (canShowChild) ? computeMinUserPosition(0, size) : 0;
-        int maxPosition = (canShowChild) ? size - 1 : computeMaxUserPosition(size - 1);
-        if (!hasChild(wc)) {
-            // Increase the maxPosition because children size will grow once wc is added.
-            ++maxPosition;
+        int maxPosition = minPosition;
+        if (size > 0) {
+            maxPosition = (canShowChild) ? size - 1 : computeMaxUserPosition(size - 1);
         }
 
         // Factor in always-on-top children in max possible position.
         if (!wc.isAlwaysOnTop()) {
             // We want to place all non-always-on-top containers below always-on-top ones.
             while (maxPosition > minPosition) {
-                if (!mChildren.get(maxPosition - 1).isAlwaysOnTop()) break;
+                if (!mChildren.get(maxPosition).isAlwaysOnTop()) break;
                 --maxPosition;
             }
         }
@@ -3173,6 +2532,12 @@
         } else if (suggestedPosition == POSITION_TOP && maxPosition >= (size - 1)) {
             return POSITION_TOP;
         }
+
+        // Increase the maxPosition because children size will grow once wc is added.
+        if (!hasChild(wc)) {
+            ++maxPosition;
+        }
+
         // Reset position based on minimum/maximum possible positions.
         return Math.min(Math.max(suggestedPosition, minPosition), maxPosition);
     }
@@ -3193,25 +2558,12 @@
         }
     }
 
-    @VisibleForTesting
-    boolean hasWindowsAlive() {
-        return getActivity(ActivityRecord::hasWindowsAlive) != null;
-    }
-
-    @VisibleForTesting
-    boolean shouldDeferRemoval() {
-        if (mChildren.isEmpty()) {
-            // No reason to defer removal of a Task that doesn't have any child.
-            return false;
-        }
-        return hasWindowsAlive() && getRootTask().isAnimating(TRANSITION | CHILDREN);
-    }
-
     @Override
     void removeImmediately() {
         removeImmediately("removeTask");
     }
 
+    @Override
     void removeImmediately(String reason) {
         if (DEBUG_ROOT_TASK) Slog.i(TAG, "removeTask:" + reason + " removing taskId=" + mTaskId);
         if (mRemoving) {
@@ -3554,18 +2906,6 @@
         mForceShowForAllUsers = forceShowForAllUsers;
     }
 
-    @Override
-    public boolean isAttached() {
-        final TaskDisplayArea taskDisplayArea = getDisplayArea();
-        return taskDisplayArea != null && !taskDisplayArea.isRemoved();
-    }
-
-    @Override
-    @Nullable
-    TaskDisplayArea getDisplayArea() {
-        return (TaskDisplayArea) super.getDisplayArea();
-    }
-
     /**
      * When we are in a floating root task (Freeform, Pinned, ...) we calculate
      * insets differently. However if we are animating to the fullscreen root task
@@ -3576,70 +2916,55 @@
         return getWindowConfiguration().tasksAreFloating() && !mPreserveNonFloatingState;
     }
 
-    /**
-     * Returns true if the root task is translucent and can have other contents visible behind it if
-     * needed. A root task is considered translucent if it don't contain a visible or
-     * starting (about to be visible) activity that is fullscreen (opaque).
-     * @param starting The currently starting activity or null if there is none.
-     */
-    @VisibleForTesting
-    boolean isTranslucent(ActivityRecord starting) {
-        if (!isAttached() || isForceHidden()) {
-            return true;
-        }
-        final PooledPredicate p = PooledLambda.obtainPredicate(Task::isOpaqueActivity,
-                PooledLambda.__(ActivityRecord.class), starting);
-        final ActivityRecord opaque = getActivity(p);
-        p.recycle();
-        return opaque == null;
-    }
-
-    private static boolean isOpaqueActivity(ActivityRecord r, ActivityRecord starting) {
-        if (r.finishing) {
-            // We don't factor in finishing activities when determining translucency since
-            // they will be gone soon.
-            return false;
-        }
-
-        if (!r.visibleIgnoringKeyguard && r != starting) {
-            // Also ignore invisible activities that are not the currently starting
-            // activity (about to be visible).
-            return false;
-        }
-
-        if (r.occludesParent()) {
-            // Root task isn't translucent if it has at least one fullscreen activity
-            // that is visible.
-            return true;
-        }
-        return false;
-    }
-
     /** Returns the top-most activity that occludes the given one, or {@code null} if none. */
     @Nullable
     ActivityRecord getOccludingActivityAbove(ActivityRecord activity) {
-        final ActivityRecord top = getActivity(ActivityRecord::occludesParent,
-                true /* traverseTopToBottom */, activity);
+        final ActivityRecord top = getActivity(r -> {
+            if (r == activity) {
+                // Reached the given activity, return the activity to stop searching.
+                return true;
+            }
+
+            if (!r.occludesParent()) {
+                return false;
+            }
+
+            TaskFragment parent = r.getTaskFragment();
+            if (parent == activity.getTaskFragment()) {
+                // Found it. This activity on top of the given activity on the same TaskFragment.
+                return true;
+            }
+            if (isSelfOrNonEmbeddedTask(parent.asTask())) {
+                // Found it. This activity is the direct child of a leaf Task without being
+                // embedded.
+                return true;
+            }
+            // The candidate activity is being embedded. Checking if the bounds of the containing
+            // TaskFragment equals to the outer TaskFragment.
+            TaskFragment grandParent = parent.getParent().asTaskFragment();
+            while (grandParent != null) {
+                if (!parent.getBounds().equals(grandParent.getBounds())) {
+                    // Not occluding the grandparent.
+                    break;
+                }
+                if (isSelfOrNonEmbeddedTask(grandParent.asTask())) {
+                    // Found it. The activity occludes its parent TaskFragment and the parent
+                    // TaskFragment also occludes its parent all the way up.
+                    return true;
+                }
+                parent = grandParent;
+                grandParent = parent.getParent().asTaskFragment();
+            }
+            return false;
+        });
         return top != activity ? top : null;
     }
 
-    /** Iterates through all occluded activities. */
-    void forAllOccludedActivities(Consumer<ActivityRecord> handleOccludedActivity) {
-        if (!shouldBeVisible(null /* starting */)) {
-            // The root task is invisible so all activities are occluded.
-            forAllActivities(handleOccludedActivity);
-            return;
+    private boolean isSelfOrNonEmbeddedTask(Task task) {
+        if (task == this) {
+            return true;
         }
-        final ActivityRecord topOccluding = getOccludingActivityAbove(null);
-        if (topOccluding == null) {
-            // No activities are occluded.
-            return;
-        }
-        // Invoke the callback on the activities behind the top occluding activity.
-        forAllActivities(r -> {
-            handleOccludedActivity.accept(r);
-            return false;
-        }, topOccluding, false /* includeBoundary */, true /* traverseTopToBottom */);
+        return task != null && !task.isEmbedded();
     }
 
     @Override
@@ -3647,21 +2972,6 @@
         return super.makeAnimationLeash().setMetadata(METADATA_TASK_ID, mTaskId);
     }
 
-    @Override
-    void resetSurfacePositionForAnimationLeash(SurfaceControl.Transaction t) {
-        super.resetSurfacePositionForAnimationLeash(t);
-    }
-
-    @Override
-    Rect getAnimationBounds(int appRootTaskClipMode) {
-        // TODO(b/131661052): we should remove appRootTaskClipMode with hierarchical animations.
-        if (appRootTaskClipMode == ROOT_TASK_CLIP_BEFORE_ANIM && getRootTask() != null) {
-            // Using the root task bounds here effectively applies the clipping before animation.
-            return getRootTask().getBounds();
-        }
-        return super.getAnimationBounds(appRootTaskClipMode);
-    }
-
     boolean shouldAnimate() {
         /**
          * Animations are handled by the TaskOrganizer implementation.
@@ -3691,36 +3001,11 @@
         return isAnimating(CHILDREN, ANIMATION_TYPE_RECENTS);
     }
 
-    @Override
-    RemoteAnimationTarget createRemoteAnimationTarget(
-            RemoteAnimationController.RemoteAnimationRecord record) {
-        final ActivityRecord activity = getTopMostActivity();
-        return activity != null ? activity.createRemoteAnimationTarget(record) : null;
-    }
-
-    @Override
-    boolean canCreateRemoteAnimationTarget() {
-        return true;
-    }
-
     WindowState getTopVisibleAppMainWindow() {
         final ActivityRecord activity = getTopVisibleActivity();
         return activity != null ? activity.findMainWindow() : null;
     }
 
-    ActivityRecord topRunningActivity() {
-        return topRunningActivity(false /* focusableOnly */);
-    }
-
-    ActivityRecord topRunningActivity(boolean focusableOnly) {
-        // Split into 2 to avoid object creation due to variable capture.
-        if (focusableOnly) {
-            return getActivity((r) -> r.canBeTopRunning() && r.isFocusable());
-        } else {
-            return getActivity(ActivityRecord::canBeTopRunning);
-        }
-    }
-
     ActivityRecord topRunningNonDelayedActivityLocked(ActivityRecord notTop) {
         final PooledPredicate p = PooledLambda.obtainPredicate(Task::isTopRunningNonDelayed
                 , PooledLambda.__(ActivityRecord.class), notTop);
@@ -3775,16 +3060,6 @@
         });
     }
 
-    boolean isTopActivityFocusable() {
-        final ActivityRecord r = topRunningActivity();
-        return r != null ? r.isFocusable()
-                : (isFocusable() && getWindowConfiguration().canReceiveKeys());
-    }
-
-    boolean isFocusableAndVisible() {
-        return isTopActivityFocusable() && shouldBeVisible(null /* starting */);
-    }
-
     void positionChildAtTop(ActivityRecord child) {
         positionChildAt(child, POSITION_TOP);
     }
@@ -3896,6 +3171,41 @@
         return false;
     }
 
+    /** Iterates through all leaf task fragments and the leaf tasks. */
+    void forAllLeafTasksAndLeafTaskFragments(final Consumer<TaskFragment> callback,
+            boolean traverseTopToBottom) {
+        forAllLeafTasks(task -> {
+            if (task.isLeafTaskFragment()) {
+                callback.accept(task);
+                return;
+            }
+
+            // A leaf task that may contains both activities and task fragments.
+            boolean consumed = false;
+            if (traverseTopToBottom) {
+                for (int i = task.mChildren.size() - 1; i >= 0; --i) {
+                    final WindowContainer child = task.mChildren.get(i);
+                    if (child.asTaskFragment() != null) {
+                        child.forAllLeafTaskFragments(callback, traverseTopToBottom);
+                    } else if (child.asActivityRecord() != null && !consumed) {
+                        callback.accept(task);
+                        consumed = true;
+                    }
+                }
+            } else {
+                for (int i = 0; i < task.mChildren.size(); i++) {
+                    final WindowContainer child = task.mChildren.get(i);
+                    if (child.asTaskFragment() != null) {
+                        child.forAllLeafTaskFragments(callback, traverseTopToBottom);
+                    } else if (child.asActivityRecord() != null && !consumed) {
+                        callback.accept(task);
+                        consumed = true;
+                    }
+                }
+            }
+        }, traverseTopToBottom);
+    }
+
     @Override
     boolean forAllRootTasks(Function<Task, Boolean> callback, boolean traverseTopToBottom) {
         return isRootTask() ? callback.apply(this) : false;
@@ -4015,19 +3325,9 @@
     @Override
     void dump(PrintWriter pw, String prefix, boolean dumpAll) {
         super.dump(pw, prefix, dumpAll);
-        pw.println(prefix + "bounds=" + getBounds().toShortString());
-        final String doublePrefix = prefix + "  ";
-        for (int i = mChildren.size() - 1; i >= 0; i--) {
-            final WindowContainer<?> child = mChildren.get(i);
-            pw.println(prefix + "* " + child);
-            // Only dump non-activity because full activity info is already printed by
-            // RootWindowContainer#dumpActivities.
-            if (child.asActivityRecord() == null) {
-                child.dump(pw, doublePrefix, dumpAll);
-            }
-        }
 
         if (!mExitingActivities.isEmpty()) {
+            final String doublePrefix = prefix + "  ";
             pw.println();
             pw.println(prefix + "Exiting application tokens:");
             for (int i = mExitingActivities.size() - 1; i >= 0; i--) {
@@ -4066,6 +3366,9 @@
         info.userId = isLeafTask() ? mUserId : mCurrentUser;
         info.taskId = mTaskId;
         info.displayId = getDisplayId();
+        if (tda != null) {
+            info.displayAreaFeatureId = tda.mFeatureId;
+        }
         info.isRunning = getTopNonFinishingActivity() != null;
         final Intent baseIntent = getBaseIntent();
         // Make a copy of base intent because this is like a snapshot info.
@@ -4125,6 +3428,7 @@
                 : INVALID_TASK_ID;
         info.isFocused = isFocused();
         info.isVisible = hasVisibleChildren();
+        info.isSleeping = shouldSleepActivities();
         ActivityRecord topRecord = getTopNonFinishingActivity();
         info.mTopActivityLocusId = topRecord != null ? topRecord.getLocusId() : null;
     }
@@ -4135,9 +3439,9 @@
 
     private @Nullable PictureInPictureParams getPictureInPictureParams(Task top) {
         if (top == null) return null;
-        final ActivityRecord topVisibleActivity = top.getTopVisibleActivity();
-        return (topVisibleActivity == null || topVisibleActivity.pictureInPictureArgs.empty())
-                ? null : new PictureInPictureParams(topVisibleActivity.pictureInPictureArgs);
+        final ActivityRecord topMostActivity = top.getTopMostActivity();
+        return (topMostActivity == null || topMostActivity.pictureInPictureArgs.empty())
+                ? null : new PictureInPictureParams(topMostActivity.pictureInPictureArgs);
     }
 
     Rect getDisplayCutoutInsets() {
@@ -4177,6 +3481,7 @@
         final WindowState mainWindow = activity.findMainWindow();
         if (mainWindow != null) {
             info.mainWindowLayoutParams = mainWindow.getAttrs();
+            info.requestedVisibilities.set(mainWindow.getRequestedVisibilities());
         }
         // If the developer has persist a different configuration, we need to override it to the
         // starting window because persisted configuration does not effect to Task.
@@ -4204,184 +3509,6 @@
         return this;
     }
 
-    /**
-     * Returns true if the task should be visible.
-     *
-     * @param starting The currently starting activity or null if there is none.
-     */
-    boolean shouldBeVisible(ActivityRecord starting) {
-        return getVisibility(starting) != TASK_VISIBILITY_INVISIBLE;
-    }
-
-    /**
-     * Returns true if the task should be visible.
-     *
-     * @param starting The currently starting activity or null if there is none.
-     */
-    @TaskVisibility
-    int getVisibility(ActivityRecord starting) {
-        if (!isAttached() || isForceHidden()) {
-            return TASK_VISIBILITY_INVISIBLE;
-        }
-
-        if (isTopActivityLaunchedBehind()) {
-            return TASK_VISIBILITY_VISIBLE;
-        }
-
-        boolean gotRootSplitScreenTask = false;
-        boolean gotOpaqueSplitScreenPrimary = false;
-        boolean gotOpaqueSplitScreenSecondary = false;
-        boolean gotTranslucentFullscreen = false;
-        boolean gotTranslucentSplitScreenPrimary = false;
-        boolean gotTranslucentSplitScreenSecondary = false;
-        boolean shouldBeVisible = true;
-
-        // This root task is only considered visible if all its parent root tasks are considered
-        // visible, so check the visibility of all ancestor root task first.
-        final WindowContainer parent = getParent();
-        if (parent.asTask() != null) {
-            final int parentVisibility = parent.asTask().getVisibility(starting);
-            if (parentVisibility == TASK_VISIBILITY_INVISIBLE) {
-                // Can't be visible if parent isn't visible
-                return TASK_VISIBILITY_INVISIBLE;
-            } else if (parentVisibility == TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT) {
-                // Parent is behind a translucent container so the highest visibility this container
-                // can get is that.
-                gotTranslucentFullscreen = true;
-            }
-        }
-
-        final List<Task> adjacentTasks = new ArrayList<>();
-        final int windowingMode = getWindowingMode();
-        final boolean isAssistantType = isActivityTypeAssistant();
-        for (int i = parent.getChildCount() - 1; i >= 0; --i) {
-            final WindowContainer wc = parent.getChildAt(i);
-            final Task other = wc.asTask();
-            if (other == null) continue;
-
-            final boolean hasRunningActivities = other.topRunningActivity() != null;
-            if (other == this) {
-                // Should be visible if there is no other stack occluding it, unless it doesn't
-                // have any running activities, not starting one and not home stack.
-                shouldBeVisible = hasRunningActivities || isInTask(starting) != null
-                        || isActivityTypeHome();
-                break;
-            }
-
-            if (!hasRunningActivities) {
-                continue;
-            }
-
-            final int otherWindowingMode = other.getWindowingMode();
-
-            if (otherWindowingMode == WINDOWING_MODE_FULLSCREEN) {
-                if (other.isTranslucent(starting)) {
-                    // Can be visible behind a translucent fullscreen stack.
-                    gotTranslucentFullscreen = true;
-                    continue;
-                }
-                return TASK_VISIBILITY_INVISIBLE;
-            } else if (otherWindowingMode == WINDOWING_MODE_MULTI_WINDOW
-                    && other.matchParentBounds()) {
-                if (other.isTranslucent(starting)) {
-                    // Can be visible behind a translucent task.
-                    gotTranslucentFullscreen = true;
-                    continue;
-                }
-                // Multi-window task that matches parent bounds would occlude other children.
-                return TASK_VISIBILITY_INVISIBLE;
-            } else if (otherWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
-                    && !gotOpaqueSplitScreenPrimary) {
-                gotRootSplitScreenTask = true;
-                gotTranslucentSplitScreenPrimary = other.isTranslucent(starting);
-                gotOpaqueSplitScreenPrimary = !gotTranslucentSplitScreenPrimary;
-                if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
-                        && gotOpaqueSplitScreenPrimary) {
-                    // Can not be visible behind another opaque stack in split-screen-primary mode.
-                    return TASK_VISIBILITY_INVISIBLE;
-                }
-            } else if (otherWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                    && !gotOpaqueSplitScreenSecondary) {
-                gotRootSplitScreenTask = true;
-                gotTranslucentSplitScreenSecondary = other.isTranslucent(starting);
-                gotOpaqueSplitScreenSecondary = !gotTranslucentSplitScreenSecondary;
-                if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                        && gotOpaqueSplitScreenSecondary) {
-                    // Can not be visible behind another opaque stack in split-screen-secondary mode.
-                    return TASK_VISIBILITY_INVISIBLE;
-                }
-            }
-            if (gotOpaqueSplitScreenPrimary && gotOpaqueSplitScreenSecondary) {
-                // Can not be visible if we are in split-screen windowing mode and both halves of
-                // the screen are opaque.
-                return TASK_VISIBILITY_INVISIBLE;
-            }
-            if (isAssistantType && gotRootSplitScreenTask) {
-                // Assistant stack can't be visible behind split-screen. In addition to this not
-                // making sense, it also works around an issue here we boost the z-order of the
-                // assistant window surfaces in window manager whenever it is visible.
-                return TASK_VISIBILITY_INVISIBLE;
-            }
-            if (other.mAdjacentTask != null) {
-                if (adjacentTasks.contains(other.mAdjacentTask)) {
-                    if (other.isTranslucent(starting)
-                            || other.mAdjacentTask.isTranslucent(starting)) {
-                        // Can be visible behind a translucent adjacent tasks.
-                        gotTranslucentFullscreen = true;
-                        continue;
-                    }
-                    // Can not be visible behind adjacent tasks.
-                    return TASK_VISIBILITY_INVISIBLE;
-                } else {
-                    adjacentTasks.add(other);
-                }
-            }
-        }
-
-        if (!shouldBeVisible) {
-            return TASK_VISIBILITY_INVISIBLE;
-        }
-
-        // Handle cases when there can be a translucent split-screen stack on top.
-        switch (windowingMode) {
-            case WINDOWING_MODE_FULLSCREEN:
-                if (gotTranslucentSplitScreenPrimary || gotTranslucentSplitScreenSecondary) {
-                    // At least one of the split-screen stacks that covers this one is translucent.
-                    // When in split mode, home task will be reparented to the secondary split while
-                    // leaving tasks not supporting split below. Due to
-                    // TaskDisplayArea#assignRootTaskOrdering always adjusts home surface layer to
-                    // the bottom, this makes sure tasks not in split roots won't occlude home task
-                    // unexpectedly.
-                    return TASK_VISIBILITY_INVISIBLE;
-                }
-                break;
-            case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY:
-                if (gotTranslucentSplitScreenPrimary) {
-                    // Covered by translucent primary split-screen on top.
-                    return TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
-                }
-                break;
-            case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY:
-                if (gotTranslucentSplitScreenSecondary) {
-                    // Covered by translucent secondary split-screen on top.
-                    return TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
-                }
-                break;
-        }
-
-        // Lastly - check if there is a translucent fullscreen stack on top.
-        return gotTranslucentFullscreen ? TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT
-                : TASK_VISIBILITY_VISIBLE;
-    }
-
-    private boolean isTopActivityLaunchedBehind() {
-        final ActivityRecord top = topRunningActivity();
-        if (top != null && top.mLaunchTaskBehind) {
-            return true;
-        }
-        return false;
-    }
-
     ActivityRecord isInTask(ActivityRecord r) {
         if (r == null) {
             return null;
@@ -4492,9 +3619,6 @@
         pw.print(" isResizeable="); pw.println(isResizeable());
         pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime);
         pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)");
-        if (mForceNotOrganized) {
-            pw.print(prefix); pw.println("mForceNotOrganized=true");
-        }
     }
 
     @Override
@@ -4511,6 +3635,8 @@
             }
             sb.append(" visible=");
             sb.append(shouldBeVisible(null /* starting */));
+            sb.append(" visibleRequested=");
+            sb.append(isVisibleRequested());
             sb.append(" mode=");
             sb.append(windowingModeToString(getWindowingMode()));
             sb.append(" translucent=");
@@ -4564,7 +3690,7 @@
             // Increment the total number of non-finishing activities
             numActivities++;
 
-            if (top == null || (top.isState(ActivityState.INITIALIZING))) {
+            if (top == null || (top.isState(INITIALIZING))) {
                 top = r;
                 // Reset the number of running activities until we hit the first non-initializing
                 // activity
@@ -4968,10 +4094,6 @@
     }
 
     private boolean canBeOrganized() {
-        if (mForceNotOrganized || !mAtmService.mTaskOrganizerController
-                .isSupportedWindowingMode(getWindowingMode())) {
-            return false;
-        }
         // All root tasks can be organized
         if (isRootTask()) {
             return true;
@@ -5124,9 +4246,8 @@
             return setTaskOrganizer(null);
         }
 
-        final int windowingMode = getWindowingMode();
         final TaskOrganizerController controller = mWmService.mAtmService.mTaskOrganizerController;
-        final ITaskOrganizer organizer = controller.getTaskOrganizer(windowingMode);
+        final ITaskOrganizer organizer = controller.getTaskOrganizer();
         if (!forceUpdate && mTaskOrganizer == organizer) {
             return false;
         }
@@ -5144,11 +4265,11 @@
     /**
      * @return true if the task is currently focused.
      */
-    boolean isFocused() {
-        if (mDisplayContent == null || mDisplayContent.mCurrentFocus == null) {
+    private boolean isFocused() {
+        if (mDisplayContent == null || mDisplayContent.mFocusedApp == null) {
             return false;
         }
-        return mDisplayContent.mCurrentFocus.getTask() == this;
+        return mDisplayContent.mFocusedApp.getTask() == this;
     }
 
     /**
@@ -5202,13 +4323,12 @@
     }
 
     /**
-     * Called on the task of a window which gained or lost focus.
+     * Called on the task when it gained or lost focus.
      * @param hasFocus
      */
-    void onWindowFocusChanged(boolean hasFocus) {
+    void onAppFocusChanged(boolean hasFocus) {
         updateShadowsRadius(hasFocus, getSyncTransaction());
-        // TODO(b/180525887): Un-comment once there is resolution on the bug.
-        // dispatchTaskInfoChangedIfNeeded(false /* force */);
+        dispatchTaskInfoChangedIfNeeded(false /* force */);
     }
 
     void onPictureInPictureParamsChanged() {
@@ -5307,9 +4427,7 @@
         return super.isAlwaysOnTop();
     }
 
-    /**
-     * Returns whether this task is currently forced to be hidden for any reason.
-     */
+    @Override
     protected boolean isForceHidden() {
         return mForceHiddenFlags != 0;
     }
@@ -5454,7 +4572,8 @@
 
             // From fullscreen to PiP.
             if (topActivity != null && currentMode == WINDOWING_MODE_FULLSCREEN
-                    && windowingMode == WINDOWING_MODE_PINNED) {
+                    && windowingMode == WINDOWING_MODE_PINNED
+                    && !mTransitionController.isShellTransitionsEnabled()) {
                 mDisplayContent.mPinnedTaskController
                         .deferOrientationChangeForEnteringPipFromFullScreenIfNeeded();
             }
@@ -5462,8 +4581,10 @@
             mAtmService.continueWindowLayout();
         }
 
-        mRootWindowContainer.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS);
-        mRootWindowContainer.resumeFocusedTasksTopActivities();
+        if (!mTaskSupervisor.isRootVisibilityUpdateDeferred()) {
+            mRootWindowContainer.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS);
+            mRootWindowContainer.resumeFocusedTasksTopActivities();
+        }
     }
 
     void resumeNextFocusAfterReparent() {
@@ -5595,19 +4716,6 @@
         r.completeResumeLocked();
     }
 
-    void awakeFromSleepingLocked() {
-        if (!isLeafTask()) {
-            forAllLeafTasks((task) -> task.awakeFromSleepingLocked(),
-                    true /* traverseTopToBottom */);
-            return;
-        }
-
-        if (mPausingActivity != null) {
-            Slog.d(TAG, "awakeFromSleepingLocked: previously pausing activity didn't pause");
-            mPausingActivity.activityPaused(true);
-        }
-    }
-
     void checkReadyForSleep() {
         if (shouldSleepActivities() && goToSleepIfPossible(false /* shuttingDown */)) {
             mTaskSupervisor.checkReadyForSleepLocked(true /* allowDelay */);
@@ -5626,306 +4734,13 @@
      * the process of going to sleep (checkReadyForSleep will be called when that process finishes).
      */
     boolean goToSleepIfPossible(boolean shuttingDown) {
-        if (!isLeafTask()) {
-            final int[] sleepInProgress = {0};
-            forAllLeafTasks((t) -> {
-                if (!t.goToSleepIfPossible(shuttingDown)) {
-                    sleepInProgress[0]++;
-                }
-            }, true);
-            return sleepInProgress[0] == 0;
-        }
-
-        boolean shouldSleep = true;
-        if (mResumedActivity != null) {
-            // Still have something resumed; can't sleep until it is paused.
-            ProtoLog.v(WM_DEBUG_STATES, "Sleep needs to pause %s", mResumedActivity);
-            if (DEBUG_USER_LEAVING) Slog.v(TAG_USER_LEAVING,
-                    "Sleep => pause with userLeaving=false");
-
-            startPausingLocked(false /* userLeaving */, true /* uiSleeping */, null /* resuming */,
-                    "sleep");
-            shouldSleep = false ;
-        } else if (mPausingActivity != null) {
-            // Still waiting for something to pause; can't sleep yet.
-            ProtoLog.v(WM_DEBUG_STATES, "Sleep still waiting to pause %s", mPausingActivity);
-            shouldSleep = false;
-        }
-
-        if (!shuttingDown) {
-            if (containsActivityFromRootTask(mTaskSupervisor.mStoppingActivities)) {
-                // Still need to tell some activities to stop; can't sleep yet.
-                ProtoLog.v(WM_DEBUG_STATES, "Sleep still need to stop %d activities",
-                        mTaskSupervisor.mStoppingActivities.size());
-
-                mTaskSupervisor.scheduleIdle();
-                shouldSleep = false;
+        final int[] sleepInProgress = {0};
+        forAllLeafTasksAndLeafTaskFragments(taskFragment -> {
+            if (!taskFragment.sleepIfPossible(shuttingDown)) {
+                sleepInProgress[0]++;
             }
-        }
-
-        if (shouldSleep) {
-            ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
-                    !PRESERVE_WINDOWS);
-        }
-
-        return shouldSleep;
-    }
-
-    private boolean containsActivityFromRootTask(List<ActivityRecord> rs) {
-        for (ActivityRecord r : rs) {
-            if (r.getRootTask() == this) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    final boolean startPausingLocked(boolean uiSleeping, ActivityRecord resuming, String reason) {
-        return startPausingLocked(mTaskSupervisor.mUserLeaving, uiSleeping, resuming, reason);
-    }
-
-    /**
-     * Start pausing the currently resumed activity.  It is an error to call this if there
-     * is already an activity being paused or there is no resumed activity.
-     *
-     * @param userLeaving True if this should result in an onUserLeaving to the current activity.
-     * @param uiSleeping True if this is happening with the user interface going to sleep (the
-     * screen turning off).
-     * @param resuming The activity we are currently trying to resume or null if this is not being
-     *                 called as part of resuming the top activity, so we shouldn't try to instigate
-     *                 a resume here if not null.
-     * @param reason The reason of pausing the activity.
-     * @return Returns true if an activity now is in the PAUSING state, and we are waiting for
-     * it to tell us when it is done.
-     */
-    final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping,
-            ActivityRecord resuming, String reason) {
-        if (!isLeafTask()) {
-            final int[] pausing = {0};
-            forAllLeafTasks((t) -> {
-                if (t.startPausingLocked(userLeaving, uiSleeping, resuming, reason)) {
-                    pausing[0]++;
-                }
-            }, true /* traverseTopToBottom */);
-            return pausing[0] > 0;
-        }
-
-        if (mPausingActivity != null) {
-            Slog.wtf(TAG, "Going to pause when pause is already pending for " + mPausingActivity
-                    + " state=" + mPausingActivity.getState());
-            if (!shouldSleepActivities()) {
-                // Avoid recursion among check for sleep and complete pause during sleeping.
-                // Because activity will be paused immediately after resume, just let pause
-                // be completed by the order of activity paused from clients.
-                completePauseLocked(false, resuming);
-            }
-        }
-        ActivityRecord prev = mResumedActivity;
-
-        if (prev == null) {
-            if (resuming == null) {
-                Slog.wtf(TAG, "Trying to pause when nothing is resumed");
-                mRootWindowContainer.resumeFocusedTasksTopActivities();
-            }
-            return false;
-        }
-
-        if (prev == resuming) {
-            Slog.wtf(TAG, "Trying to pause activity that is in process of being resumed");
-            return false;
-        }
-
-        ProtoLog.v(WM_DEBUG_STATES, "Moving to PAUSING: %s", prev);
-        mPausingActivity = prev;
-        mLastPausedActivity = prev;
-        if (prev.isNoHistory() && !mTaskSupervisor.mNoHistoryActivities.contains(prev)) {
-            mTaskSupervisor.mNoHistoryActivities.add(prev);
-        }
-        prev.setState(PAUSING, "startPausingLocked");
-        prev.getTask().touchActiveTime();
-
-        mAtmService.updateCpuStats();
-
-        boolean pauseImmediately = false;
-        boolean shouldAutoPip = false;
-        if (resuming != null && (resuming.info.flags & FLAG_RESUME_WHILE_PAUSING) != 0) {
-            // If the flag RESUME_WHILE_PAUSING is set, then continue to schedule the previous
-            // activity to be paused, while at the same time resuming the new resume activity
-            // only if the previous activity can't go into Pip since we want to give Pip
-            // activities a chance to enter Pip before resuming the next activity.
-            final boolean lastResumedCanPip = prev != null && prev.checkEnterPictureInPictureState(
-                    "shouldResumeWhilePausing", userLeaving);
-            if (lastResumedCanPip && prev.pictureInPictureArgs.isAutoEnterEnabled()) {
-                shouldAutoPip = true;
-            } else if (!lastResumedCanPip) {
-                pauseImmediately = true;
-            } else {
-                // The previous activity may still enter PIP even though it did not allow auto-PIP.
-            }
-        }
-
-        boolean didAutoPip = false;
-        if (prev.attachedToProcess()) {
-            if (shouldAutoPip) {
-                ProtoLog.d(WM_DEBUG_STATES, "Auto-PIP allowed, entering PIP mode "
-                        + "directly: %s", prev);
-
-                didAutoPip = mAtmService.enterPictureInPictureMode(prev, prev.pictureInPictureArgs);
-            } else {
-                schedulePauseActivity(prev, userLeaving, pauseImmediately, reason);
-            }
-        } else {
-            mPausingActivity = null;
-            mLastPausedActivity = null;
-            mTaskSupervisor.mNoHistoryActivities.remove(prev);
-        }
-
-        // If we are not going to sleep, we want to ensure the device is
-        // awake until the next activity is started.
-        if (!uiSleeping && !mAtmService.isSleepingOrShuttingDownLocked()) {
-            mTaskSupervisor.acquireLaunchWakelock();
-        }
-
-        // If already entered PIP mode, no need to keep pausing.
-        if (mPausingActivity != null) {
-            // Have the window manager pause its key dispatching until the new
-            // activity has started.  If we're pausing the activity just because
-            // the screen is being turned off and the UI is sleeping, don't interrupt
-            // key dispatch; the same activity will pick it up again on wakeup.
-            if (!uiSleeping) {
-                prev.pauseKeyDispatchingLocked();
-            } else {
-                ProtoLog.v(WM_DEBUG_STATES, "Key dispatch not paused for screen off");
-            }
-
-            if (pauseImmediately) {
-                // If the caller said they don't want to wait for the pause, then complete
-                // the pause now.
-                completePauseLocked(false, resuming);
-                return false;
-
-            } else {
-                prev.schedulePauseTimeout();
-                return true;
-            }
-
-        } else {
-            // This activity either failed to schedule the pause or it entered PIP mode,
-            // so just treat it as being paused now.
-            ProtoLog.v(WM_DEBUG_STATES, "Activity not running or entered PiP, resuming next.");
-            if (resuming == null) {
-                mRootWindowContainer.resumeFocusedTasksTopActivities();
-            }
-            return false;
-        }
-    }
-
-    void schedulePauseActivity(ActivityRecord prev, boolean userLeaving,
-            boolean pauseImmediately, String reason) {
-        ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev);
-        try {
-            EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
-                    prev.shortComponentName, "userLeaving=" + userLeaving, reason);
-
-            mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
-                    prev.appToken, PauseActivityItem.obtain(prev.finishing, userLeaving,
-                            prev.configChangeFlags, pauseImmediately));
-        } catch (Exception e) {
-            // Ignore exception, if process died other code will cleanup.
-            Slog.w(TAG, "Exception thrown during pause", e);
-            mPausingActivity = null;
-            mLastPausedActivity = null;
-            mTaskSupervisor.mNoHistoryActivities.remove(prev);
-        }
-    }
-
-    @VisibleForTesting
-    void completePauseLocked(boolean resumeNext, ActivityRecord resuming) {
-        // Complete the pausing process of a pausing activity, so it doesn't make sense to
-        // operate on non-leaf tasks.
-        warnForNonLeafTask("completePauseLocked");
-
-        ActivityRecord prev = mPausingActivity;
-        ProtoLog.v(WM_DEBUG_STATES, "Complete pause: %s", prev);
-
-        if (prev != null) {
-            prev.setWillCloseOrEnterPip(false);
-            final boolean wasStopping = prev.isState(STOPPING);
-            prev.setState(PAUSED, "completePausedLocked");
-            if (prev.finishing) {
-                // We will update the activity visibility later, no need to do in
-                // completeFinishing(). Updating visibility here might also making the next
-                // activities to be resumed, and could result in wrong app transition due to
-                // lack of previous activity information.
-                ProtoLog.v(WM_DEBUG_STATES, "Executing finish of activity: %s", prev);
-                prev = prev.completeFinishing(false /* updateVisibility */,
-                        "completePausedLocked");
-            } else if (prev.hasProcess()) {
-                ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
-                        + "wasStopping=%b visibleRequested=%b",  prev,  wasStopping,
-                        prev.mVisibleRequested);
-                if (prev.deferRelaunchUntilPaused) {
-                    // Complete the deferred relaunch that was waiting for pause to complete.
-                    ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev);
-                    prev.relaunchActivityLocked(prev.preserveWindowOnDeferredRelaunch);
-                } else if (wasStopping) {
-                    // We are also stopping, the stop request must have gone soon after the pause.
-                    // We can't clobber it, because the stop confirmation will not be handled.
-                    // We don't need to schedule another stop, we only need to let it happen.
-                    prev.setState(STOPPING, "completePausedLocked");
-                } else if (!prev.mVisibleRequested || shouldSleepOrShutDownActivities()) {
-                    // Clear out any deferred client hide we might currently have.
-                    prev.setDeferHidingClient(false);
-                    // If we were visible then resumeTopActivities will release resources before
-                    // stopping.
-                    prev.addToStopping(true /* scheduleIdle */, false /* idleDelayed */,
-                            "completePauseLocked");
-                }
-            } else {
-                ProtoLog.v(WM_DEBUG_STATES, "App died during pause, not stopping: %s", prev);
-                prev = null;
-            }
-            // It is possible the activity was freezing the screen before it was paused.
-            // In that case go ahead and remove the freeze this activity has on the screen
-            // since it is no longer visible.
-            if (prev != null) {
-                prev.stopFreezingScreenLocked(true /*force*/);
-            }
-            mPausingActivity = null;
-        }
-
-        if (resumeNext) {
-            final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
-            if (topRootTask != null && !topRootTask.shouldSleepOrShutDownActivities()) {
-                mRootWindowContainer.resumeFocusedTasksTopActivities(topRootTask, prev, null);
-            } else {
-                checkReadyForSleep();
-                final ActivityRecord top =
-                        topRootTask != null ? topRootTask.topRunningActivity() : null;
-                if (top == null || (prev != null && top != prev)) {
-                    // If there are no more activities available to run, do resume anyway to start
-                    // something. Also if the top activity on the root task is not the just paused
-                    // activity, we need to go ahead and resume it to ensure we complete an
-                    // in-flight app switch.
-                    mRootWindowContainer.resumeFocusedTasksTopActivities();
-                }
-            }
-        }
-
-        if (prev != null) {
-            prev.resumeKeyDispatchingLocked();
-        }
-
-        mRootWindowContainer.ensureActivitiesVisible(resuming, 0, !PRESERVE_WINDOWS);
-
-        // Notify when the task stack has changed, but only if visibilities changed (not just
-        // focus). Also if there is an active root pinned task - we always want to notify it about
-        // task stack changes, because its positioning may depend on it.
-        if (mTaskSupervisor.mAppVisibilitiesChangedSinceLastPause
-                || (getDisplayArea() != null && getDisplayArea().hasPinnedTask())) {
-            mAtmService.getTaskChangeNotificationController().notifyTaskStackChanged();
-            mTaskSupervisor.mAppVisibilitiesChangedSinceLastPause = false;
-        }
+        }, true /* traverseTopToBottom */);
+        return sleepInProgress[0] == 0;
     }
 
     boolean isTopRootTaskInDisplayArea() {
@@ -5948,10 +4763,10 @@
      *                 The activity is either starting or resuming.
      *                 Caller should ensure starting activity is visible.
      * @param preserveWindows Flag indicating whether windows should be preserved when updating
-     *                        configuration in {@link mEnsureActivitiesVisibleHelper}.
+     *                        configuration in {@link EnsureActivitiesVisibleHelper}.
      * @param configChanges Parts of the configuration that changed for this activity for evaluating
      *                      if the screen should be frozen as part of
-     *                      {@link mEnsureActivitiesVisibleHelper}.
+     *                      {@link EnsureActivitiesVisibleHelper}.
      *
      */
     void ensureActivitiesVisible(@Nullable ActivityRecord starting, int configChanges,
@@ -5967,25 +4782,22 @@
      *                 The activity is either starting or resuming.
      *                 Caller should ensure starting activity is visible.
      * @param notifyClients Flag indicating whether the visibility updates should be sent to the
-     *                      clients in {@link mEnsureActivitiesVisibleHelper}.
+     *                      clients in {@link EnsureActivitiesVisibleHelper}.
      * @param preserveWindows Flag indicating whether windows should be preserved when updating
-     *                        configuration in {@link mEnsureActivitiesVisibleHelper}.
+     *                        configuration in {@link EnsureActivitiesVisibleHelper}.
      * @param configChanges Parts of the configuration that changed for this activity for evaluating
      *                      if the screen should be frozen as part of
-     *                      {@link mEnsureActivitiesVisibleHelper}.
+     *                      {@link EnsureActivitiesVisibleHelper}.
      */
     // TODO: Should be re-worked based on the fact that each task as a root task in most cases.
     void ensureActivitiesVisible(@Nullable ActivityRecord starting, int configChanges,
             boolean preserveWindows, boolean notifyClients) {
         mTaskSupervisor.beginActivityVisibilityUpdate();
         try {
-            forAllLeafTasks(task -> task.mEnsureActivitiesVisibleHelper.process(
-                    starting, configChanges, preserveWindows, notifyClients),
-                    true /* traverseTopToBottom */);
-
-            // Notify WM shell that task visibilities may have changed
-            forAllTasks(task -> task.dispatchTaskInfoChangedIfNeeded(/* force */ false),
-                    true /* traverseTopToBottom */);
+            forAllLeafTasks(task -> {
+                task.updateActivityVisibilities(starting, configChanges, preserveWindows,
+                        notifyClients);
+            }, true /* traverseTopToBottom */);
 
             if (mTranslucentActivityWaiting != null &&
                     mUndrawnActivitiesBelowTopTranslucent.isEmpty()) {
@@ -6056,25 +4868,6 @@
         }
     }
 
-    /** @see ActivityRecord#cancelInitializing() */
-    void cancelInitializingActivities() {
-        // We don't want to clear starting window for activities that aren't behind fullscreen
-        // activities as we need to display their starting window until they are done initializing.
-        checkBehindFullscreenActivity(null /* toCheck */, ActivityRecord::cancelInitializing);
-    }
-
-    /**
-     * If an activity {@param toCheck} is given, this method returns {@code true} if the activity
-     * is occluded by any fullscreen activity. If there is no {@param toCheck} and the handling
-     * function {@param handleBehindFullscreenActivity} is given, this method will pass all occluded
-     * activities to the function.
-     */
-    boolean checkBehindFullscreenActivity(ActivityRecord toCheck,
-            Consumer<ActivityRecord> handleBehindFullscreenActivity) {
-        return mCheckBehindFullscreenActivityHelper.process(
-                toCheck, handleBehindFullscreenActivity);
-    }
-
     /**
      * Ensure that the top activity in the root task is resumed.
      *
@@ -6115,7 +4908,8 @@
                     if (!child.isTopActivityFocusable()) {
                         continue;
                     }
-                    if (child.getVisibility(null /* starting */) != TASK_VISIBILITY_VISIBLE) {
+                    if (child.getVisibility(null /* starting */)
+                            != TASK_FRAGMENT_VISIBILITY_VISIBLE) {
                         break;
                     }
 
@@ -6162,383 +4956,26 @@
             return false;
         }
 
-        // Find the next top-most activity to resume in this root task that is not finishing and is
-        // focusable. If it is not focusable, we will fall into the case below to resume the
-        // top activity in the next focusable task.
-        ActivityRecord next = topRunningActivity(true /* focusableOnly */);
-
-        final boolean hasRunningActivity = next != null;
-
-        // TODO: Maybe this entire condition can get removed?
-        if (hasRunningActivity && !isAttached()) {
-            return false;
-        }
-
-        mRootWindowContainer.cancelInitializingActivities();
-
-        if (!hasRunningActivity) {
-            // There are no activities left in the root task, let's look somewhere else.
+        final ActivityRecord topActivity = topRunningActivity(true /* focusableOnly */);
+        if (topActivity == null) {
+            // There are no activities left in this task, let's look somewhere else.
             return resumeNextFocusableActivityWhenRootTaskIsEmpty(prev, options);
         }
 
-        next.delayedResume = false;
-        final TaskDisplayArea taskDisplayArea = getDisplayArea();
-
-        // If the top activity is the resumed one, nothing to do.
-        if (mResumedActivity == next && next.isState(RESUMED)
-                && taskDisplayArea.allResumedActivitiesComplete()) {
-            // Make sure we have executed any pending transitions, since there
-            // should be nothing left to do at this point.
-            executeAppTransition(options);
-            // For devices that are not in fullscreen mode (e.g. freeform windows), it's possible
-            // we still want to check if the visibility of other windows have changed (e.g. bringing
-            // a fullscreen window forward to cover another freeform activity.)
-            if (taskDisplayArea.inMultiWindowMode()) {
-                taskDisplayArea.ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
-                        false /* preserveWindows */, true /* notifyClients */);
+        final boolean[] resumed = new boolean[1];
+        final TaskFragment topFragment = topActivity.getTaskFragment();
+        resumed[0] = topFragment.resumeTopActivity(prev, options, deferPause);
+        forAllLeafTaskFragments(f -> {
+            if (topFragment == f) {
+                return;
             }
-            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivityLocked: Top activity "
-                    + "resumed %s", next);
-            return false;
-        }
-
-        if (!next.canResumeByCompat()) {
-            return false;
-        }
-
-        // If we are currently pausing an activity, then don't do anything until that is done.
-        final boolean allPausedComplete = mRootWindowContainer.allPausedActivitiesComplete();
-        if (!allPausedComplete) {
-            ProtoLog.v(WM_DEBUG_STATES,
-                    "resumeTopActivityLocked: Skip resume: some activity pausing.");
-
-            return false;
-        }
-
-        // If we are sleeping, and there is no resumed activity, and the top activity is paused,
-        // well that is the state we want.
-        if (mLastPausedActivity == next && shouldSleepOrShutDownActivities()) {
-            // Make sure we have executed any pending transitions, since there
-            // should be nothing left to do at this point.
-            executeAppTransition(options);
-            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivityLocked: Going to sleep and"
-                    + " all paused");
-            return false;
-        }
-
-        // Make sure that the user who owns this activity is started.  If not,
-        // we will just leave it as is because someone should be bringing
-        // another user's activities to the top of the stack.
-        if (!mAtmService.mAmInternal.hasStartedUserState(next.mUserId)) {
-            Slog.w(TAG, "Skipping resume of top activity " + next
-                    + ": user " + next.mUserId + " is stopped");
-            return false;
-        }
-
-        // The activity may be waiting for stop, but that is no longer
-        // appropriate for it.
-        mTaskSupervisor.mStoppingActivities.remove(next);
-
-        if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Resuming " + next);
-
-        mTaskSupervisor.setLaunchSource(next.info.applicationInfo.uid);
-
-        ActivityRecord lastResumed = null;
-        final Task lastFocusedRootTask = taskDisplayArea.getLastFocusedRootTask();
-        if (lastFocusedRootTask != null && lastFocusedRootTask != getRootTask()) {
-            // So, why aren't we using prev here??? See the param comment on the method. prev
-            // doesn't represent the last resumed activity. However, the last focus stack does if
-            // it isn't null.
-            lastResumed = lastFocusedRootTask.getResumedActivity();
-        }
-
-        boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next);
-        if (mResumedActivity != null) {
-            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivityLocked: Pausing %s", mResumedActivity);
-            pausing |= startPausingLocked(false /* uiSleeping */, next,
-                    "resumeTopActivityInnerLocked");
-        }
-        if (pausing) {
-            ProtoLog.v(WM_DEBUG_STATES, "resumeTopActivityLocked: Skip resume: need to"
-                    + " start pausing");
-            // At this point we want to put the upcoming activity's process
-            // at the top of the LRU list, since we know we will be needing it
-            // very soon and it would be a waste to let it get killed if it
-            // happens to be sitting towards the end.
-            if (next.attachedToProcess()) {
-                next.app.updateProcessInfo(false /* updateServiceConnectionActivities */,
-                        true /* activityChange */, false /* updateOomAdj */,
-                        false /* addPendingTopUid */);
-            } else if (!next.isProcessRunning()) {
-                // Since the start-process is asynchronous, if we already know the process of next
-                // activity isn't running, we can start the process earlier to save the time to wait
-                // for the current activity to be paused.
-                final boolean isTop = this == taskDisplayArea.getFocusedRootTask();
-                mAtmService.startProcessAsync(next, false /* knownToBeDead */, isTop,
-                        isTop ? "pre-top-activity" : "pre-activity");
+            if (!f.isFocusableAndVisible()) {
+                // No need to resume activity in TaskFragment that is not visible.
+                return;
             }
-            if (lastResumed != null) {
-                lastResumed.setWillCloseOrEnterPip(true);
-            }
-            return true;
-        } else if (mResumedActivity == next && next.isState(RESUMED)
-                && taskDisplayArea.allResumedActivitiesComplete()) {
-            // It is possible for the activity to be resumed when we paused back stacks above if the
-            // next activity doesn't have to wait for pause to complete.
-            // So, nothing else to-do except:
-            // Make sure we have executed any pending transitions, since there
-            // should be nothing left to do at this point.
-            executeAppTransition(options);
-            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivityLocked: Top activity resumed "
-                    + "(dontWaitForPause) %s", next);
-            return true;
-        }
-
-        // If the most recent activity was noHistory but was only stopped rather
-        // than stopped+finished because the device went to sleep, we need to make
-        // sure to finish it as we're making a new activity topmost.
-        if (shouldSleepActivities()) {
-            mTaskSupervisor.finishNoHistoryActivitiesIfNeeded(next);
-        }
-
-        if (prev != null && prev != next && next.nowVisible) {
-
-            // The next activity is already visible, so hide the previous
-            // activity's windows right now so we can show the new one ASAP.
-            // We only do this if the previous is finishing, which should mean
-            // it is on top of the one being resumed so hiding it quickly
-            // is good.  Otherwise, we want to do the normal route of allowing
-            // the resumed activity to be shown so we can decide if the
-            // previous should actually be hidden depending on whether the
-            // new one is found to be full-screen or not.
-            if (prev.finishing) {
-                prev.setVisibility(false);
-                if (DEBUG_SWITCH) Slog.v(TAG_SWITCH,
-                        "Not waiting for visible to hide: " + prev
-                                + ", nowVisible=" + next.nowVisible);
-            } else {
-                if (DEBUG_SWITCH) Slog.v(TAG_SWITCH,
-                        "Previous already visible but still waiting to hide: " + prev
-                                + ", nowVisible=" + next.nowVisible);
-            }
-
-        }
-
-        // Launching this app's activity, make sure the app is no longer
-        // considered stopped.
-        try {
-            mTaskSupervisor.getActivityMetricsLogger()
-                    .notifyBeforePackageUnstopped(next.packageName);
-            mAtmService.getPackageManager().setPackageStoppedState(
-                    next.packageName, false, next.mUserId); /* TODO: Verify if correct userid */
-        } catch (RemoteException e1) {
-        } catch (IllegalArgumentException e) {
-            Slog.w(TAG, "Failed trying to unstop package "
-                    + next.packageName + ": " + e);
-        }
-
-        // We are starting up the next activity, so tell the window manager
-        // that the previous one will be hidden soon.  This way it can know
-        // to ignore it when computing the desired screen orientation.
-        boolean anim = true;
-        final DisplayContent dc = taskDisplayArea.mDisplayContent;
-        if (prev != null) {
-            if (prev.finishing) {
-                if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION,
-                        "Prepare close transition: prev=" + prev);
-                if (mTaskSupervisor.mNoAnimActivities.contains(prev)) {
-                    anim = false;
-                    dc.prepareAppTransition(TRANSIT_NONE);
-                } else {
-                    dc.prepareAppTransition(TRANSIT_CLOSE);
-                }
-                prev.setVisibility(false);
-            } else {
-                if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION,
-                        "Prepare open transition: prev=" + prev);
-                if (mTaskSupervisor.mNoAnimActivities.contains(next)) {
-                    anim = false;
-                    dc.prepareAppTransition(TRANSIT_NONE);
-                } else {
-                    dc.prepareAppTransition(TRANSIT_OPEN,
-                            next.mLaunchTaskBehind ? TRANSIT_FLAG_OPEN_BEHIND : 0);
-                }
-            }
-        } else {
-            if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION, "Prepare open transition: no previous");
-            if (mTaskSupervisor.mNoAnimActivities.contains(next)) {
-                anim = false;
-                dc.prepareAppTransition(TRANSIT_NONE);
-            } else {
-                dc.prepareAppTransition(TRANSIT_OPEN);
-            }
-        }
-
-        if (anim) {
-            next.applyOptionsAnimation();
-        } else {
-            next.abortAndClearOptionsAnimation();
-        }
-
-        mTaskSupervisor.mNoAnimActivities.clear();
-
-        if (next.attachedToProcess()) {
-            if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Resume running: " + next
-                    + " stopped=" + next.stopped
-                    + " visibleRequested=" + next.mVisibleRequested);
-
-            // If the previous activity is translucent, force a visibility update of
-            // the next activity, so that it's added to WM's opening app list, and
-            // transition animation can be set up properly.
-            // For example, pressing Home button with a translucent activity in focus.
-            // Launcher is already visible in this case. If we don't add it to opening
-            // apps, maybeUpdateTransitToWallpaper() will fail to identify this as a
-            // TRANSIT_WALLPAPER_OPEN animation, and run some funny animation.
-            final boolean lastActivityTranslucent = lastFocusedRootTask != null
-                    && (lastFocusedRootTask.inMultiWindowMode()
-                    || (lastFocusedRootTask.mLastPausedActivity != null
-                    && !lastFocusedRootTask.mLastPausedActivity.occludesParent()));
-
-            // This activity is now becoming visible.
-            if (!next.mVisibleRequested || next.stopped || lastActivityTranslucent) {
-                next.setVisibility(true);
-            }
-
-            // schedule launch ticks to collect information about slow apps.
-            next.startLaunchTickingLocked();
-
-            ActivityRecord lastResumedActivity =
-                    lastFocusedRootTask == null ? null : lastFocusedRootTask.getResumedActivity();
-            final ActivityState lastState = next.getState();
-
-            mAtmService.updateCpuStats();
-
-            ProtoLog.v(WM_DEBUG_STATES, "Moving to RESUMED: %s (in existing)", next);
-
-            next.setState(RESUMED, "resumeTopActivityInnerLocked");
-
-            // Have the window manager re-evaluate the orientation of
-            // the screen based on the new activity order.
-            boolean notUpdated = true;
-
-            // Activity should also be visible if set mLaunchTaskBehind to true (see
-            // ActivityRecord#shouldBeVisibleIgnoringKeyguard()).
-            if (shouldBeVisible(next)) {
-                // We have special rotation behavior when here is some active activity that
-                // requests specific orientation or Keyguard is locked. Make sure all activity
-                // visibilities are set correctly as well as the transition is updated if needed
-                // to get the correct rotation behavior. Otherwise the following call to update
-                // the orientation may cause incorrect configurations delivered to client as a
-                // result of invisible window resize.
-                // TODO: Remove this once visibilities are set correctly immediately when
-                // starting an activity.
-                notUpdated = !mRootWindowContainer.ensureVisibilityAndConfig(next, getDisplayId(),
-                        true /* markFrozenIfConfigChanged */, false /* deferResume */);
-            }
-
-            if (notUpdated) {
-                // The configuration update wasn't able to keep the existing
-                // instance of the activity, and instead started a new one.
-                // We should be all done, but let's just make sure our activity
-                // is still at the top and schedule another run if something
-                // weird happened.
-                ActivityRecord nextNext = topRunningActivity();
-                ProtoLog.i(WM_DEBUG_STATES, "Activity config changed during resume: "
-                        + "%s, new next: %s", next, nextNext);
-                if (nextNext != next) {
-                    // Do over!
-                    mTaskSupervisor.scheduleResumeTopActivities();
-                }
-                if (!next.mVisibleRequested || next.stopped) {
-                    next.setVisibility(true);
-                }
-                next.completeResumeLocked();
-                return true;
-            }
-
-            try {
-                final ClientTransaction transaction =
-                        ClientTransaction.obtain(next.app.getThread(), next.appToken);
-                // Deliver all pending results.
-                ArrayList<ResultInfo> a = next.results;
-                if (a != null) {
-                    final int N = a.size();
-                    if (!next.finishing && N > 0) {
-                        if (DEBUG_RESULTS) Slog.v(TAG_RESULTS,
-                                "Delivering results to " + next + ": " + a);
-                        transaction.addCallback(ActivityResultItem.obtain(a));
-                    }
-                }
-
-                if (next.newIntents != null) {
-                    transaction.addCallback(
-                            NewIntentItem.obtain(next.newIntents, true /* resume */));
-                }
-
-                // Well the app will no longer be stopped.
-                // Clear app token stopped state in window manager if needed.
-                next.notifyAppResumed(next.stopped);
-
-                EventLogTags.writeWmResumeActivity(next.mUserId, System.identityHashCode(next),
-                        next.getTask().mTaskId, next.shortComponentName);
-
-                mAtmService.getAppWarningsLocked().onResumeActivity(next);
-                next.app.setPendingUiCleanAndForceProcessStateUpTo(mAtmService.mTopProcessState);
-                next.abortAndClearOptionsAnimation();
-                transaction.setLifecycleStateRequest(
-                        ResumeActivityItem.obtain(next.app.getReportedProcState(),
-                                dc.isNextTransitionForward()));
-                mAtmService.getLifecycleManager().scheduleTransaction(transaction);
-
-                ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivityLocked: Resumed %s", next);
-            } catch (Exception e) {
-                // Whoops, need to restart this activity!
-                ProtoLog.v(WM_DEBUG_STATES, "Resume failed; resetting state to %s: "
-                        + "%s", lastState, next);
-                next.setState(lastState, "resumeTopActivityInnerLocked");
-
-                // lastResumedActivity being non-null implies there is a lastStack present.
-                if (lastResumedActivity != null) {
-                    lastResumedActivity.setState(RESUMED, "resumeTopActivityInnerLocked");
-                }
-
-                Slog.i(TAG, "Restarting because process died: " + next);
-                if (!next.hasBeenLaunched) {
-                    next.hasBeenLaunched = true;
-                } else if (SHOW_APP_STARTING_PREVIEW && lastFocusedRootTask != null
-                        && lastFocusedRootTask.isTopRootTaskInDisplayArea()) {
-                    next.showStartingWindow(false /* taskSwitch */);
-                }
-                mTaskSupervisor.startSpecificActivity(next, true, false);
-                return true;
-            }
-
-            // From this point on, if something goes wrong there is no way
-            // to recover the activity.
-            try {
-                next.completeResumeLocked();
-            } catch (Exception e) {
-                // If any exception gets thrown, toss away this
-                // activity and try the next one.
-                Slog.w(TAG, "Exception thrown during resume of " + next, e);
-                next.finishIfPossible("resume-exception", true /* oomAdj */);
-                return true;
-            }
-        } else {
-            // Whoops, need to restart this activity!
-            if (!next.hasBeenLaunched) {
-                next.hasBeenLaunched = true;
-            } else {
-                if (SHOW_APP_STARTING_PREVIEW) {
-                    next.showStartingWindow(false /* taskSwich */);
-                }
-                if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Restarting: " + next);
-            }
-            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivityLocked: Restarting %s", next);
-            mTaskSupervisor.startSpecificActivity(next, true, true);
-        }
-
-        return true;
+            resumed[0] |= f.resumeTopActivity(prev, options, deferPause);
+        }, true);
+        return resumed[0];
     }
 
     /**
@@ -6572,7 +5009,7 @@
     }
 
     void startActivityLocked(ActivityRecord r, @Nullable ActivityRecord focusedTopActivity,
-            boolean newTask, boolean keepCurTransition, ActivityOptions options,
+            boolean newTask, boolean isTaskSwitch, ActivityOptions options,
             @Nullable ActivityRecord sourceRecord) {
         Task rTask = r.getTask();
         final boolean allowMoveToFront = options == null || !options.getAvoidMoveToFront();
@@ -6618,7 +5055,6 @@
         // Slot the activity into the history root task and proceed
         ProtoLog.i(WM_DEBUG_ADD_REMOVE, "Adding activity %s to task %s "
                         + "callers: %s", r, task, new RuntimeException("here").fillInStackTrace());
-        task.positionChildAtTop(r);
 
         // The transition animation and starting window are not needed if {@code allowMoveToFront}
         // is false, because the activity won't be visible.
@@ -6679,21 +5115,18 @@
                 // "has the same starting icon" as the next one.  This allows the
                 // window manager to keep the previous window it had previously
                 // created, if it still had one.
-                Task prevTask = r.getTask();
-                ActivityRecord prev = prevTask.topActivityWithStartingWindow();
-                if (prev != null) {
-                    // We don't want to reuse the previous starting preview if:
-                    // (1) The current activity is in a different task.
-                    if (prev.getTask() != prevTask) {
-                        prev = null;
-                    }
-                    // (2) The current activity is already displayed.
-                    else if (prev.nowVisible) {
-                        prev = null;
-                    }
+                Task baseTask = r.getTask();
+                if (baseTask.isEmbedded()) {
+                    // If the task is embedded in a task fragment, there may have an existing
+                    // starting window in the parent task. This allows the embedded activities
+                    // to share the starting window and make sure that the window can have top
+                    // z-order by transferring to the top activity.
+                    baseTask = baseTask.getParent().asTaskFragment().getTask();
                 }
 
-                r.showStartingWindow(prev, newTask, isTaskSwitch(r, focusedTopActivity),
+                final ActivityRecord prev = baseTask.getActivity(
+                        a -> a.mStartingData != null && a.showToCurrentUser());
+                r.showStartingWindow(prev, newTask, isTaskSwitch,
                         true /* startActivity */, sourceRecord);
             }
         } else {
@@ -6727,10 +5160,6 @@
         return true;
     }
 
-    private boolean isTaskSwitch(ActivityRecord r, ActivityRecord topFocusedActivity) {
-        return topFocusedActivity != null && r.getTask() != topFocusedActivity.getTask();
-    }
-
     /**
      * Reset the task by reparenting the activities that have same affinity to the task or
      * reparenting the activities that have different affinityies out of the task, while these
@@ -6788,7 +5217,6 @@
         Slog.w(TAG, "  Force finishing activity "
                 + r.intent.getComponent().flattenToShortString());
         Task finishedTask = r.getTask();
-        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED);
         mDisplayContent.requestTransitionAndLegacyPrepare(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED);
         r.finishIfPossible(reason, false /* oomAdj */);
 
@@ -6846,18 +5274,6 @@
         return true;
     }
 
-    /** Finish all activities in the root task without waiting. */
-    void finishAllActivitiesImmediately() {
-        if (!hasChild()) {
-            removeIfPossible("finishAllActivitiesImmediately");
-            return;
-        }
-        forAllActivities((r) -> {
-            Slog.d(TAG, "finishAllActivitiesImmediatelyLocked: finishing " + r);
-            r.destroyIfPossible("finishAllActivitiesImmediately");
-        });
-    }
-
     /** @return true if the root task behind this one is a standard activity type. */
     private boolean inFrontOfStandardRootTask() {
         final TaskDisplayArea taskDisplayArea = getDisplayArea();
@@ -7080,7 +5496,7 @@
 
             // Don't refocus if invisible to current user
             final ActivityRecord top = tr.getTopNonFinishingActivity();
-            if (top == null || !top.okToShowLocked()) {
+            if (top == null || !top.showToCurrentUser()) {
                 positionChildAtTop(tr);
                 if (top != null) {
                     mTaskSupervisor.mRecentTasks.add(top.getTask());
@@ -7168,7 +5584,6 @@
 
         // Skip the transition for pinned task.
         if (!inPinnedWindowingMode()) {
-            mDisplayContent.prepareAppTransition(TRANSIT_TO_BACK);
             mDisplayContent.requestTransitionAndLegacyPrepare(TRANSIT_TO_BACK, tr);
         }
         moveToBack("moveTaskToBackLocked", tr);
@@ -7194,13 +5609,6 @@
         return true;
     }
 
-    /**
-     * Ensures all visible activities at or below the input activity have the right configuration.
-     */
-    void ensureVisibleActivitiesConfiguration(ActivityRecord start, boolean preserveWindow) {
-        mEnsureVisibleActivitiesConfigHelper.process(start, preserveWindow);
-    }
-
     // TODO: Can only be called from special methods in ActivityTaskSupervisor.
     // Need to consolidate those calls points into this resize method so anyone can call directly.
     void resize(Rect displayedBounds, boolean preserveWindows, boolean deferResume) {
@@ -7254,114 +5662,32 @@
         }
     }
 
-    /**
-     * Reset local parameters because an app's activity died.
-     * @param app The app of the activity that died.
-     * @return {@code true} if the process of the pausing activity is died.
-     */
-    boolean handleAppDied(WindowProcessController app) {
-        warnForNonLeafTask("handleAppDied");
-        boolean isPausingDied = false;
-        if (mPausingActivity != null && mPausingActivity.app == app) {
-            ProtoLog.v(WM_DEBUG_STATES, "App died while pausing: %s",
-                    mPausingActivity);
-            mPausingActivity = null;
-            isPausingDied = true;
-        }
-        if (mLastPausedActivity != null && mLastPausedActivity.app == app) {
-            if (mLastPausedActivity.isNoHistory()) {
-                mTaskSupervisor.mNoHistoryActivities.remove(mLastPausedActivity);
-            }
-            mLastPausedActivity = null;
-        }
-        return isPausingDied;
-    }
-
     boolean dump(FileDescriptor fd, PrintWriter pw, boolean dumpAll, boolean dumpClient,
             String dumpPackage, final boolean needSep) {
-        Runnable headerPrinter = () -> {
-            if (needSep) {
-                pw.println();
-            }
-            pw.println("  RootTask #" + getRootTaskId()
-                    + ": type=" + activityTypeToString(getActivityType())
-                    + " mode=" + windowingModeToString(getWindowingMode()));
-            pw.println("  isSleeping=" + shouldSleepActivities());
-            pw.println("  mBounds=" + getRequestedOverrideBounds());
-            pw.println("  mCreatedByOrganizer=" + mCreatedByOrganizer);
-        };
-
-        boolean printed = false;
-
-        if (dumpPackage == null) {
-            // If we are not filtering by package, we want to print absolutely everything,
-            // so always print the header even if there are no tasks/activities inside.
-            headerPrinter.run();
-            headerPrinter = null;
-            printed = true;
-        }
-
-        printed |= printThisActivity(pw, getPausingActivity(), dumpPackage, false,
-                "    mPausingActivity: ", null);
-        printed |= printThisActivity(pw, getResumedActivity(), dumpPackage, false,
-                "    mResumedActivity: ", null);
-        if (dumpAll) {
-            printed |= printThisActivity(pw, mLastPausedActivity, dumpPackage, false,
-                    "    mLastPausedActivity: ", null);
-        }
-
-        printed |= dumpActivities(fd, pw, dumpAll, dumpClient, dumpPackage, false, headerPrinter);
-
-        return printed;
+        return dump("  ", fd, pw, dumpAll, dumpClient, dumpPackage, needSep, null /* header */);
     }
 
-    private boolean dumpActivities(FileDescriptor fd, PrintWriter pw, boolean dumpAll,
-            boolean dumpClient, String dumpPackage, boolean needSep, Runnable header) {
-        if (!hasChild()) {
-            return false;
+    @Override
+    void dumpInner(String prefix, PrintWriter pw, boolean dumpAll, String dumpPackage) {
+        super.dumpInner(prefix, pw, dumpAll, dumpPackage);
+        if (mCreatedByOrganizer) {
+            pw.println(prefix + "  mCreatedByOrganizer=true");
         }
-        final AtomicBoolean printedHeader = new AtomicBoolean(false);
-        final AtomicBoolean printed = new AtomicBoolean(false);
-        forAllLeafTasks((task) -> {
-            final String prefix = "    ";
-            Runnable headerPrinter = () -> {
-                printed.set(true);
-                if (!printedHeader.get()) {
-                    if (needSep) {
-                        pw.println("");
-                    }
-                    if (header != null) {
-                        header.run();
-                    }
-                    printedHeader.set(true);
-                }
-                pw.print(prefix); pw.print("* "); pw.println(task);
-                pw.print(prefix); pw.print("  mBounds=");
-                pw.println(task.getRequestedOverrideBounds());
-                pw.print(prefix); pw.print("  mMinWidth="); pw.print(task.mMinWidth);
-                pw.print(" mMinHeight="); pw.println(task.mMinHeight);
-                if (mLastNonFullscreenBounds != null) {
-                    pw.print(prefix);
-                    pw.print("  mLastNonFullscreenBounds=");
-                    pw.println(task.mLastNonFullscreenBounds);
-                }
-                task.dump(pw, prefix + "  ");
-            };
-            if (dumpPackage == null) {
-                // If we are not filtering by package, we want to print absolutely everything,
-                // so always print the header even if there are no activities inside.
-                headerPrinter.run();
-                headerPrinter = null;
+        if (mLastNonFullscreenBounds != null) {
+            pw.print(prefix); pw.print("  mLastNonFullscreenBounds=");
+            pw.println(mLastNonFullscreenBounds);
+        }
+        if (isLeafTask()) {
+            pw.println(prefix + "  isSleeping=" + shouldSleepActivities());
+            printThisActivity(pw, getTopPausingActivity(), dumpPackage, false,
+                    prefix + "  topPausingActivity=", null);
+            printThisActivity(pw, getTopResumedActivity(), dumpPackage, false,
+                    prefix + "  topResumedActivity=", null);
+            if (mMinWidth != INVALID_MIN_SIZE || mMinHeight != INVALID_MIN_SIZE) {
+                pw.print(prefix); pw.print("  mMinWidth="); pw.print(mMinWidth);
+                pw.print(" mMinHeight="); pw.println(mMinHeight);
             }
-            final ArrayList<ActivityRecord> activities = new ArrayList<>();
-            // Add activities by traversing the hierarchy from bottom to top, since activities
-            // are dumped in reverse order in {@link ActivityTaskSupervisor#dumpHistoryList()}.
-            task.forAllActivities((Consumer<ActivityRecord>) activities::add,
-                    false /* traverseTopToBottom */);
-            dumpHistoryList(fd, pw, activities, prefix, "Hist", true, !dumpAll, dumpClient,
-                    dumpPackage, false, headerPrinter, task);
-        }, true /* traverseTopToBottom */);
-        return printed.get();
+        }
     }
 
     ArrayList<ActivityRecord> getDumpActivitiesLocked(String name) {
@@ -7747,15 +6073,6 @@
         getDisplayContent().getPinnedTaskController().setActions(actions);
     }
 
-    /** Returns true if a removal action is still being deferred. */
-    boolean handleCompleteDeferredRemoval() {
-        if (isAnimating(TRANSITION | CHILDREN)) {
-            return true;
-        }
-
-        return super.handleCompleteDeferredRemoval();
-    }
-
     public DisplayInfo getDisplayInfo() {
         return mDisplayContent.getDisplayInfo();
     }
@@ -7764,6 +6081,7 @@
         return mAnimatingActivityRegistry;
     }
 
+    @Override
     void executeAppTransition(ActivityOptions options) {
         mDisplayContent.executeAppTransition();
         ActivityOptions.abort(options);
@@ -7786,10 +6104,6 @@
         return display != null ? display.isSleeping() : mAtmService.isSleepingLocked();
     }
 
-    boolean shouldSleepOrShutDownActivities() {
-        return shouldSleepActivities() || mAtmService.mShuttingDown;
-    }
-
     private Rect getRawBounds() {
         return super.getBounds();
     }
@@ -7808,14 +6122,12 @@
         }
 
         final long token = proto.start(fieldId);
-        super.dumpDebug(proto, WINDOW_CONTAINER, logLevel);
 
         proto.write(TaskProto.ID, mTaskId);
-        proto.write(DISPLAY_ID, getDisplayId());
         proto.write(ROOT_TASK_ID, getRootTaskId());
 
-        if (mResumedActivity != null) {
-            mResumedActivity.writeIdentifierToProto(proto, RESUMED_ACTIVITY);
+        if (getTopResumedActivity() != null) {
+            getTopResumedActivity().writeIdentifierToProto(proto, RESUMED_ACTIVITY);
         }
         if (realActivity != null) {
             proto.write(REAL_ACTIVITY, realActivity.flattenToShortString());
@@ -7823,11 +6135,7 @@
         if (origActivity != null) {
             proto.write(ORIG_ACTIVITY, origActivity.flattenToShortString());
         }
-        proto.write(ACTIVITY_TYPE, getActivityType());
         proto.write(RESIZE_MODE, mResizeMode);
-        proto.write(MIN_WIDTH, mMinWidth);
-        proto.write(MIN_HEIGHT, mMinHeight);
-
         proto.write(FILLS_PARENT, matchParentBounds());
         getRawBounds().dumpDebug(proto, BOUNDS);
 
@@ -7844,6 +6152,8 @@
         proto.write(AFFINITY, affinity);
         proto.write(HAS_CHILD_PIP_ACTIVITY, mChildPipActivity != null);
 
+        super.dumpDebug(proto, TASK_FRAGMENT, logLevel);
+
         proto.end(token);
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index ee4c629..275ed0e 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -32,13 +32,13 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerService.TAG_ROOT_TASK;
 import static com.android.server.wm.DisplayContent.alwaysCreateRootTask;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
-import static com.android.server.wm.Task.TASK_VISIBILITY_VISIBLE;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ROOT_TASK;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
@@ -99,13 +99,6 @@
     private int mColorLayerCounter = 0;
 
     /**
-     * A control placed at the appropriate level for transitions to occur.
-     */
-    private SurfaceControl mAppAnimationLayer;
-    private SurfaceControl mBoostedAppAnimationLayer;
-    private SurfaceControl mHomeAppAnimationLayer;
-
-    /**
      * Given that the split-screen divider does not have an AppWindowToken, it
      * will have to live inside of a "NonAppWindowContainer". However, in visual Z order
      * it will need to be interleaved with some of our children, appearing on top of
@@ -132,8 +125,6 @@
     private final ArrayList<WindowContainer> mTmpNormalChildren = new ArrayList<>();
     private final ArrayList<WindowContainer> mTmpHomeChildren = new ArrayList<>();
     private final IntArray mTmpNeedsZBoostIndexes = new IntArray();
-    private int mTmpLayerForSplitScreenDividerAnchor;
-    private int mTmpLayerForAnimationLayer;
 
     private ArrayList<Task> mTmpTasks = new ArrayList<>();
 
@@ -488,7 +479,7 @@
         // Update the top resumed activity because the preferred top focusable task may be changed.
         mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded();
 
-        final ActivityRecord r = child.getResumedActivity();
+        final ActivityRecord r = child.getTopResumedActivity();
         if (r != null && r == mRootWindowContainer.getTopResumedActivity()) {
             mAtmService.setResumedActivityUncheckLocked(r, "positionChildAt");
         }
@@ -800,10 +791,12 @@
             }
             return SCREEN_ORIENTATION_UNSPECIFIED;
         } else {
-            // Apps and their containers are not allowed to specify an orientation of full screen
-            // tasks created by organizer. The organizer handles the orientation instead.
-            final Task task = getTopRootTaskInWindowingMode(WINDOWING_MODE_FULLSCREEN);
-            if (task != null && task.isVisible() && task.mCreatedByOrganizer) {
+            // Apps and their containers are not allowed to specify an orientation of non floating
+            // visible tasks created by organizer. The organizer handles the orientation instead.
+            final Task nonFloatingTopTask =
+                    getRootTask(t -> !t.getWindowConfiguration().tasksAreFloating());
+            if (nonFloatingTopTask != null && nonFloatingTopTask.mCreatedByOrganizer
+                    && nonFloatingTopTask.isVisible()) {
                 return SCREEN_ORIENTATION_UNSPECIFIED;
             }
         }
@@ -870,36 +863,13 @@
 
         int layer = 0;
         // Place root home tasks to the bottom.
-        layer = adjustRootTaskLayer(t, mTmpHomeChildren, layer, false /* normalRootTasks */);
-        // The home animation layer is between the root home tasks and the normal root tasks.
-        final int layerForHomeAnimationLayer = layer++;
-        mTmpLayerForSplitScreenDividerAnchor = layer++;
-        mTmpLayerForAnimationLayer = layer++;
-        layer = adjustRootTaskLayer(t, mTmpNormalChildren, layer, true /* normalRootTasks */);
+        layer = adjustRootTaskLayer(t, mTmpHomeChildren, layer);
+        adjustRootTaskLayer(t, mTmpNormalChildren, layer);
 
-        // The boosted animation layer is between the normal root tasks and the always on top
-        // root tasks.
-        final int layerForBoostedAnimationLayer = layer++;
-        adjustRootTaskLayer(t, mTmpAlwaysOnTopChildren, layer, false /* normalRootTasks */);
-
-        t.setLayer(mHomeAppAnimationLayer, layerForHomeAnimationLayer);
-        t.setLayer(mAppAnimationLayer, mTmpLayerForAnimationLayer);
-        t.setLayer(mSplitScreenDividerAnchor, mTmpLayerForSplitScreenDividerAnchor);
-        t.setLayer(mBoostedAppAnimationLayer, layerForBoostedAnimationLayer);
-    }
-
-    private int adjustNormalRootTaskLayer(WindowContainer child, int layer) {
-        if (child.asTask() != null && child.inSplitScreenWindowingMode()) {
-            // The split screen divider anchor is located above the split screen window.
-            mTmpLayerForSplitScreenDividerAnchor = layer++;
-        }
-        if ((child.asTask() != null && child.asTask().isAnimatingByRecents())
-                || child.isAppTransitioning()) {
-            // The animation layer is located above the highest animating root task and no
-            // higher.
-            mTmpLayerForAnimationLayer = layer++;
-        }
-        return layer;
+        // Always on top tasks layer should higher than split divider layer so set it as start.
+        t.setLayer(mSplitScreenDividerAnchor, SPLIT_DIVIDER_LAYER);
+        layer = SPLIT_DIVIDER_LAYER + 1;
+        adjustRootTaskLayer(t, mTmpAlwaysOnTopChildren, layer);
     }
 
     /**
@@ -908,11 +878,10 @@
      * normal rootTasks.
      *
      * @param startLayer   The beginning layer of this group of rootTasks.
-     * @param normalRootTasks Set {@code true} if this group is neither home nor always on top.
      * @return The adjusted layer value.
      */
     private int adjustRootTaskLayer(SurfaceControl.Transaction t,
-            ArrayList<WindowContainer> children, int startLayer, boolean normalRootTasks) {
+            ArrayList<WindowContainer> children, int startLayer) {
         mTmpNeedsZBoostIndexes.clear();
         final int childCount = children.size();
         for (int i = 0; i < childCount; i++) {
@@ -925,9 +894,6 @@
 
             if (!childNeedsZBoost) {
                 child.assignLayer(t, startLayer++);
-                if (normalRootTasks) {
-                    startLayer = adjustNormalRootTaskLayer(child, startLayer);
-                }
             } else {
                 mTmpNeedsZBoostIndexes.add(i);
             }
@@ -937,9 +903,6 @@
         for (int i = 0; i < zBoostSize; i++) {
             final WindowContainer child = children.get(mTmpNeedsZBoostIndexes.get(i));
             child.assignLayer(t, startLayer++);
-            if (normalRootTasks) {
-                startLayer = adjustNormalRootTaskLayer(child, startLayer);
-            }
         }
         return startLayer;
     }
@@ -953,19 +916,6 @@
     }
 
     @Override
-    SurfaceControl getAppAnimationLayer(@AnimationLayer int animationLayer) {
-        switch (animationLayer) {
-            case ANIMATION_LAYER_BOOSTED:
-                return mBoostedAppAnimationLayer;
-            case ANIMATION_LAYER_HOME:
-                return mHomeAppAnimationLayer;
-            case ANIMATION_LAYER_STANDARD:
-            default:
-                return mAppAnimationLayer;
-        }
-    }
-
-    @Override
     RemoteAnimationTarget createRemoteAnimationTarget(
             RemoteAnimationController.RemoteAnimationRecord record) {
         final ActivityRecord activity = getTopMostActivity();
@@ -985,42 +935,21 @@
                         .setName("colorBackgroundLayer")
                         .setCallsite("TaskDisplayArea.onParentChanged")
                         .build();
-                mAppAnimationLayer = makeChildSurface(null)
-                        .setName("animationLayer")
-                        .setCallsite("TaskDisplayArea.onParentChanged")
-                        .build();
-                mBoostedAppAnimationLayer = makeChildSurface(null)
-                        .setName("boostedAnimationLayer")
-                        .setCallsite("TaskDisplayArea.onParentChanged")
-                        .build();
-                mHomeAppAnimationLayer = makeChildSurface(null)
-                        .setName("homeAnimationLayer")
-                        .setCallsite("TaskDisplayArea.onParentChanged")
-                        .build();
                 mSplitScreenDividerAnchor = makeChildSurface(null)
                         .setName("splitScreenDividerAnchor")
                         .setCallsite("TaskDisplayArea.onParentChanged")
                         .build();
 
                 getSyncTransaction()
-                        .show(mAppAnimationLayer)
-                        .show(mBoostedAppAnimationLayer)
-                        .show(mHomeAppAnimationLayer)
                         .show(mSplitScreenDividerAnchor);
             });
         } else {
             super.onParentChanged(newParent, oldParent);
             mWmService.mTransactionFactory.get()
                     .remove(mColorBackgroundLayer)
-                    .remove(mAppAnimationLayer)
-                    .remove(mBoostedAppAnimationLayer)
-                    .remove(mHomeAppAnimationLayer)
                     .remove(mSplitScreenDividerAnchor)
                     .apply();
             mColorBackgroundLayer = null;
-            mAppAnimationLayer = null;
-            mBoostedAppAnimationLayer = null;
-            mHomeAppAnimationLayer = null;
             mSplitScreenDividerAnchor = null;
         }
     }
@@ -1061,15 +990,12 @@
     @Override
     void migrateToNewSurfaceControl(SurfaceControl.Transaction t) {
         super.migrateToNewSurfaceControl(t);
-        if (mAppAnimationLayer == null) {
+        if (mColorBackgroundLayer == null) {
             return;
         }
 
         // As TaskDisplayArea is getting a new surface, reparent and reorder the child surfaces.
         t.reparent(mColorBackgroundLayer, mSurfaceControl);
-        t.reparent(mAppAnimationLayer, mSurfaceControl);
-        t.reparent(mBoostedAppAnimationLayer, mSurfaceControl);
-        t.reparent(mHomeAppAnimationLayer, mSurfaceControl);
         t.reparent(mSplitScreenDividerAnchor, mSurfaceControl);
         reassignLayer(t);
         scheduleAnimation();
@@ -1158,29 +1084,27 @@
                 return rootTask;
             }
         } else if (candidateTask != null) {
-            final Task rootTask = candidateTask;
             final int position = onTop ? POSITION_TOP : POSITION_BOTTOM;
             final Task launchRootTask = getLaunchRootTask(windowingMode, activityType, options,
                     sourceTask, launchFlags);
-
             if (launchRootTask != null) {
-                if (rootTask.getParent() == null) {
-                    launchRootTask.addChild(rootTask, position);
-                } else if (rootTask.getParent() != launchRootTask) {
-                    rootTask.reparent(launchRootTask, position);
+                if (candidateTask.getParent() == null) {
+                    launchRootTask.addChild(candidateTask, position);
+                } else if (candidateTask.getParent() != launchRootTask) {
+                    candidateTask.reparent(launchRootTask, position);
                 }
-            } else if (rootTask.getDisplayArea() != this || !rootTask.isRootTask()) {
-                if (rootTask.getParent() == null) {
-                    addChild(rootTask, position);
+            } else if (candidateTask.getDisplayArea() != this || !candidateTask.isRootTask()) {
+                if (candidateTask.getParent() == null) {
+                    addChild(candidateTask, position);
                 } else {
-                    rootTask.reparent(this, onTop);
+                    candidateTask.reparent(this, onTop);
                 }
             }
             // Update windowing mode if necessary, e.g. moving a pinned task to fullscreen.
             if (candidateTask.getWindowingMode() != windowingMode) {
                 candidateTask.setWindowingMode(windowingMode);
             }
-            return rootTask;
+            return candidateTask.getRootTask();
         }
         return new Task.Builder(mAtmService)
                 .setWindowingMode(windowingMode)
@@ -1292,7 +1216,7 @@
                                 + adjacentFlagRootTask);
             }
 
-            if (adjacentFlagRootTask.mAdjacentTask == null) {
+            if (adjacentFlagRootTask.getAdjacentTaskFragment() == null) {
                 throw new UnsupportedOperationException(
                         "Can't set non-adjacent root as launch adjacent flag root tr="
                                 + adjacentFlagRootTask);
@@ -1330,8 +1254,8 @@
             // If the adjacent launch is coming from the same root, launch to adjacent root instead.
             if (sourceTask != null
                     && sourceTask.getRootTask().mTaskId == mLaunchAdjacentFlagRootTask.mTaskId
-                    && mLaunchAdjacentFlagRootTask.mAdjacentTask != null) {
-                return mLaunchAdjacentFlagRootTask.mAdjacentTask;
+                    && mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment() != null) {
+                return mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment().asTask();
             } else {
                 return mLaunchAdjacentFlagRootTask;
             }
@@ -1340,16 +1264,22 @@
         for (int i = mLaunchRootTasks.size() - 1; i >= 0; --i) {
             if (mLaunchRootTasks.get(i).contains(windowingMode, activityType)) {
                 final Task launchRootTask = mLaunchRootTasks.get(i).task;
-                // Return the focusable root task for improving the UX with staged split screen.
-                final Task adjacentRootTask = launchRootTask != null
-                        ? launchRootTask.mAdjacentTask : null;
-                if (adjacentRootTask != null && adjacentRootTask.isFocusedRootTaskOnDisplay()) {
+                final TaskFragment adjacentTaskFragment = launchRootTask != null
+                        ? launchRootTask.getAdjacentTaskFragment() : null;
+                final Task adjacentRootTask =
+                        adjacentTaskFragment != null ? adjacentTaskFragment.asTask() : null;
+                if (sourceTask != null && sourceTask.getRootTask() == adjacentRootTask) {
                     return adjacentRootTask;
                 } else {
                     return launchRootTask;
                 }
             }
         }
+        // For better split UX, If task launch by the source task which root task is created by
+        // organizer, it should also launch in that root too.
+        if (sourceTask != null && sourceTask.getRootTask().mCreatedByOrganizer) {
+            return sourceTask.getRootTask();
+        }
         return null;
     }
 
@@ -1434,11 +1364,11 @@
         }
         // TODO(b/111541062): Move this into Task#getResumedActivity()
         // Check if the focused root task has the resumed activity
-        ActivityRecord resumedActivity = focusedRootTask.getResumedActivity();
+        ActivityRecord resumedActivity = focusedRootTask.getTopResumedActivity();
         if (resumedActivity == null || resumedActivity.app == null) {
             // If there is no registered resumed activity in the root task or it is not running -
             // try to use previously resumed one.
-            resumedActivity = focusedRootTask.getPausingActivity();
+            resumedActivity = focusedRootTask.getTopPausingActivity();
             if (resumedActivity == null || resumedActivity.app == null) {
                 // If previously resumed activity doesn't work either - find the topmost running
                 // activity that can be focused.
@@ -1465,7 +1395,7 @@
         // Clear last paused activity if focused root task changed while sleeping, so that the
         // top activity of current focused task can be resumed.
         if (mDisplayContent.isSleeping()) {
-            currentFocusedTask.mLastPausedActivity = null;
+            currentFocusedTask.clearLastPausedActivity();
         }
 
         mLastFocusedRootTask = prevFocusedTask;
@@ -1486,7 +1416,7 @@
                 continue;
             }
 
-            final ActivityRecord r = mChildren.get(i).asTask().getResumedActivity();
+            final ActivityRecord r = mChildren.get(i).asTask().getTopResumedActivity();
             if (r != null && !r.isState(RESUMED)) {
                 return false;
             }
@@ -1512,18 +1442,30 @@
      */
     boolean pauseBackTasks(ActivityRecord resuming) {
         final int[] someActivityPaused = {0};
-        forAllLeafTasks((task) -> {
-            final ActivityRecord resumedActivity = task.getResumedActivity();
-            if (resumedActivity != null
-                    && (task.getVisibility(resuming) != TASK_VISIBILITY_VISIBLE
-                    || !task.isTopActivityFocusable())) {
-                ProtoLog.d(WM_DEBUG_STATES, "pauseBackTasks: task=%s "
-                        + "mResumedActivity=%s", task, resumedActivity);
-                if (task.startPausingLocked(false /* uiSleeping*/,
-                        resuming, "pauseBackTasks")) {
-                    someActivityPaused[0]++;
+        forAllLeafTasks(leafTask -> {
+            // Check if the direct child resumed activity in the leaf task needed to be paused if
+            // the leaf task is not a leaf task fragment.
+            if (!leafTask.isLeafTaskFragment()) {
+                final ActivityRecord top = topRunningActivity();
+                final ActivityRecord resumedActivity = leafTask.getResumedActivity();
+                if (resumedActivity != null && top.getTaskFragment() != leafTask) {
+                    // Pausing the resumed activity because it is occluded by other task fragment.
+                    if (leafTask.startPausing(false /* uiSleeping*/, resuming, "pauseBackTasks")) {
+                        someActivityPaused[0]++;
+                    }
                 }
             }
+
+            leafTask.forAllLeafTaskFragments((taskFrag) -> {
+                final ActivityRecord resumedActivity = taskFrag.getResumedActivity();
+                if (resumedActivity != null
+                        && (taskFrag.getVisibility(resuming) != TASK_FRAGMENT_VISIBILITY_VISIBLE
+                        || !taskFrag.isTopActivityFocusable())) {
+                    if (taskFrag.startPausing(false /* uiSleeping*/, resuming, "pauseBackTasks")) {
+                        someActivityPaused[0]++;
+                    }
+                }
+            }, true /* traverseTopToBottom */);
         }, true /* traverseTopToBottom */);
         return someActivityPaused[0] > 0;
     }
@@ -2150,7 +2092,7 @@
             if (destroyContentOnRemoval
                     || !task.isActivityTypeStandardOrUndefined()
                     || task.mCreatedByOrganizer) {
-                task.finishAllActivitiesImmediately();
+                task.remove(false /* withTransition */, "removeTaskDisplayArea");
             } else {
                 // Reparent task to corresponding launch root or display area.
                 final WindowContainer launchRoot =
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
new file mode 100644
index 0000000..99f6f34
--- /dev/null
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -0,0 +1,2422 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.pm.ActivityInfo.FLAG_RESUME_WHILE_PAUSING;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.os.UserHandle.USER_NULL;
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
+import static android.view.WindowManager.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TRANSITION;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RESULTS;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TRANSITION;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.ActivityTaskSupervisor.printThisActivity;
+import static com.android.server.wm.IdentifierProto.HASH_CODE;
+import static com.android.server.wm.IdentifierProto.TITLE;
+import static com.android.server.wm.IdentifierProto.USER_ID;
+import static com.android.server.wm.TaskFragmentProto.ACTIVITY_TYPE;
+import static com.android.server.wm.TaskFragmentProto.DISPLAY_ID;
+import static com.android.server.wm.TaskFragmentProto.MIN_HEIGHT;
+import static com.android.server.wm.TaskFragmentProto.MIN_WIDTH;
+import static com.android.server.wm.TaskFragmentProto.WINDOW_CONTAINER;
+import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
+import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
+import static com.android.server.wm.WindowContainerChildProto.TASK_FRAGMENT;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.app.ResultInfo;
+import android.app.WindowConfiguration;
+import android.app.servertransaction.ActivityResultItem;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.NewIntentItem;
+import android.app.servertransaction.PauseActivityItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+import android.view.DisplayInfo;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOrganizerToken;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.function.pooled.PooledFunction;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.internal.util.function.pooled.PooledPredicate;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * A basic container that can be used to contain activities or other {@link TaskFragment}, which
+ * also able to manage the activity lifecycle and updates the visibilities of the activities in it.
+ */
+class TaskFragment extends WindowContainer<WindowContainer> {
+    @IntDef(prefix = {"TASK_FRAGMENT_VISIBILITY"}, value = {
+            TASK_FRAGMENT_VISIBILITY_VISIBLE,
+            TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+            TASK_FRAGMENT_VISIBILITY_INVISIBLE,
+    })
+    @interface TaskFragmentVisibility {}
+
+    /**
+     * TaskFragment is visible. No other TaskFragment(s) on top that fully or partially occlude it.
+     */
+    static final int TASK_FRAGMENT_VISIBILITY_VISIBLE = 0;
+
+    /** TaskFragment is partially occluded by other translucent TaskFragment(s) on top of it. */
+    static final int TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT = 1;
+
+    /** TaskFragment is completely invisible. */
+    static final int TASK_FRAGMENT_VISIBILITY_INVISIBLE = 2;
+
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskFragment" : TAG_ATM;
+    private static final String TAG_SWITCH = TAG + POSTFIX_SWITCH;
+    private static final String TAG_RESULTS = TAG + POSTFIX_RESULTS;
+    private static final String TAG_TRANSITION = TAG + POSTFIX_TRANSITION;
+
+    /** Set to false to disable the preview that is shown while a new activity is being started. */
+    static final boolean SHOW_APP_STARTING_PREVIEW = true;
+
+    /**
+     * Indicate that the minimal width/height should use the default value.
+     *
+     * @see #mMinWidth
+     * @see #mMinHeight
+     */
+    static final int INVALID_MIN_SIZE = -1;
+
+    final ActivityTaskManagerService mAtmService;
+    final ActivityTaskSupervisor mTaskSupervisor;
+    final RootWindowContainer mRootWindowContainer;
+    private final TaskFragmentOrganizerController mTaskFragmentOrganizerController;
+
+    /**
+     * Minimal width of this task fragment when it's resizeable. {@link #INVALID_MIN_SIZE} means it
+     * should use the default minimal width.
+     */
+    int mMinWidth;
+
+    /**
+     * Minimal height of this task fragment when it's resizeable. {@link #INVALID_MIN_SIZE} means it
+     * should use the default minimal height.
+     */
+    int mMinHeight;
+
+    /** This task fragment will be removed when the cleanup of its children are done. */
+    private boolean mIsRemovalRequested;
+
+    /** The TaskFragment that is adjacent to this one. */
+    @Nullable
+    private TaskFragment mAdjacentTaskFragment;
+
+    /**
+     * Prevents duplicate calls to onTaskAppeared.
+     */
+    boolean mTaskFragmentAppearedSent;
+
+    /**
+     * The last running activity of the TaskFragment was finished due to clear task while launching
+     * an activity in the Task.
+     */
+    boolean mClearedTaskForReuse;
+
+    /**
+     * When we are in the process of pausing an activity, before starting the
+     * next one, this variable holds the activity that is currently being paused.
+     *
+     * Only set at leaf task fragments.
+     */
+    @Nullable
+    private ActivityRecord mPausingActivity = null;
+
+    /**
+     * This is the last activity that we put into the paused state.  This is
+     * used to determine if we need to do an activity transition while sleeping,
+     * when we normally hold the top activity paused.
+     */
+    ActivityRecord mLastPausedActivity = null;
+
+    /**
+     * Current activity that is resumed, or null if there is none.
+     * Only set at leaf task fragments.
+     */
+    @Nullable
+    private ActivityRecord mResumedActivity = null;
+
+    /**
+     * This TaskFragment was created by an organizer which has the following implementations.
+     * <ul>
+     *     <li>The TaskFragment won't be removed when it is empty. Removal has to be an explicit
+     *     request from the organizer.</li>
+     *     <li>If this fragment is a Task object then unlike other non-root tasks, it's direct
+     *     children are visible to the organizer for ordering purposes.</li>
+     *     <li>A TaskFragment can be created by {@link android.window.TaskFragmentOrganizer}, and
+     *     a Task can be created by {@link android.window.TaskOrganizer}.</li>
+     * </ul>
+     */
+    @VisibleForTesting
+    boolean mCreatedByOrganizer;
+
+    /** Whether this TaskFragment is embedded in a task. */
+    private final boolean mIsEmbedded;
+
+    /** Organizer that organizing this TaskFragment. */
+    @Nullable
+    private ITaskFragmentOrganizer mTaskFragmentOrganizer;
+
+    /** Client assigned unique token for this TaskFragment if this is created by an organizer. */
+    @Nullable
+    private IBinder mFragmentToken;
+
+    /**
+     * Whether to delay the last activity of TaskFragment being immediately removed while finishing.
+     * This should only be set on a embedded TaskFragment, where the organizer can have the
+     * opportunity to perform other actions or animations.
+     */
+    private boolean mDelayLastActivityRemoval;
+
+    /**
+     * The PID of the organizer that created this TaskFragment. It should be the same as the PID
+     * of {@link android.window.TaskFragmentCreationParams#getOwnerToken()}.
+     * {@link ActivityRecord#INVALID_PID} if this is not an organizer-created TaskFragment.
+     */
+    private int mTaskFragmentOrganizerPid = ActivityRecord.INVALID_PID;
+
+    final Point mLastSurfaceSize = new Point();
+
+    private final Rect mTmpInsets = new Rect();
+    private final Rect mTmpBounds = new Rect();
+    private final Rect mTmpFullBounds = new Rect();
+    private final Rect mTmpStableBounds = new Rect();
+    private final Rect mTmpNonDecorBounds = new Rect();
+
+    private final EnsureActivitiesVisibleHelper mEnsureActivitiesVisibleHelper =
+            new EnsureActivitiesVisibleHelper(this);
+    private final EnsureVisibleActivitiesConfigHelper mEnsureVisibleActivitiesConfigHelper =
+            new EnsureVisibleActivitiesConfigHelper();
+    private class EnsureVisibleActivitiesConfigHelper {
+        private boolean mUpdateConfig;
+        private boolean mPreserveWindow;
+        private boolean mBehindFullscreen;
+
+        void reset(boolean preserveWindow) {
+            mPreserveWindow = preserveWindow;
+            mUpdateConfig = false;
+            mBehindFullscreen = false;
+        }
+
+        void process(ActivityRecord start, boolean preserveWindow) {
+            if (start == null || !start.mVisibleRequested) {
+                return;
+            }
+            reset(preserveWindow);
+
+            final PooledFunction f = PooledLambda.obtainFunction(
+                    EnsureVisibleActivitiesConfigHelper::processActivity, this,
+                    PooledLambda.__(ActivityRecord.class));
+            forAllActivities(f, start, true /*includeBoundary*/, true /*traverseTopToBottom*/);
+            f.recycle();
+
+            if (mUpdateConfig) {
+                // Ensure the resumed state of the focus activity if we updated the configuration of
+                // any activity.
+                mRootWindowContainer.resumeFocusedTasksTopActivities();
+            }
+        }
+
+        boolean processActivity(ActivityRecord r) {
+            mUpdateConfig |= r.ensureActivityConfiguration(0 /*globalChanges*/, mPreserveWindow);
+            mBehindFullscreen |= r.occludesParent();
+            return mBehindFullscreen;
+        }
+    }
+
+    /** Creates an embedded task fragment. */
+    TaskFragment(ActivityTaskManagerService atmService, IBinder fragmentToken,
+            boolean createdByOrganizer) {
+        this(atmService, fragmentToken, createdByOrganizer, true /* isEmbedded */);
+    }
+
+    TaskFragment(ActivityTaskManagerService atmService, IBinder fragmentToken,
+            boolean createdByOrganizer, boolean isEmbedded) {
+        super(atmService.mWindowManager);
+
+        mAtmService = atmService;
+        mTaskSupervisor = mAtmService.mTaskSupervisor;
+        mRootWindowContainer = mAtmService.mRootWindowContainer;
+        mCreatedByOrganizer = createdByOrganizer;
+        mIsEmbedded = isEmbedded;
+        mTaskFragmentOrganizerController =
+                mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController;
+        mFragmentToken = fragmentToken;
+        mRemoteToken = new RemoteToken(this);
+    }
+
+    @NonNull
+    static TaskFragment fromTaskFragmentToken(@Nullable IBinder token,
+            @NonNull ActivityTaskManagerService service) {
+        if (token == null) return null;
+        return service.mWindowOrganizerController.getTaskFragment(token);
+    }
+
+    void setAdjacentTaskFragment(@Nullable TaskFragment taskFragment) {
+        if (mAdjacentTaskFragment == taskFragment) {
+            return;
+        }
+        resetAdjacentTaskFragment();
+        if (taskFragment != null) {
+            mAdjacentTaskFragment = taskFragment;
+            taskFragment.setAdjacentTaskFragment(this);
+        }
+    }
+
+    private void resetAdjacentTaskFragment() {
+        // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment.
+        if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) {
+            mAdjacentTaskFragment.mAdjacentTaskFragment = null;
+            mAdjacentTaskFragment.mDelayLastActivityRemoval = false;
+        }
+        mAdjacentTaskFragment = null;
+        mDelayLastActivityRemoval = false;
+    }
+
+    void setTaskFragmentOrganizer(TaskFragmentOrganizerToken organizer, int pid) {
+        mTaskFragmentOrganizer = ITaskFragmentOrganizer.Stub.asInterface(organizer.asBinder());
+        mTaskFragmentOrganizerPid = pid;
+    }
+
+    /** Whether this TaskFragment is organized by the given {@code organizer}. */
+    boolean hasTaskFragmentOrganizer(ITaskFragmentOrganizer organizer) {
+        return organizer != null && mTaskFragmentOrganizer != null
+                && organizer.asBinder().equals(mTaskFragmentOrganizer.asBinder());
+    }
+
+    TaskFragment getAdjacentTaskFragment() {
+        return mAdjacentTaskFragment;
+    }
+
+    /** Returns the currently topmost resumed activity. */
+    @Nullable
+    ActivityRecord getTopResumedActivity() {
+        final ActivityRecord taskFragResumedActivity = getResumedActivity();
+        for (int i = getChildCount() - 1; i >= 0; --i) {
+            WindowContainer<?> child = getChildAt(i);
+            ActivityRecord topResumedActivity = null;
+            if (taskFragResumedActivity != null && child == taskFragResumedActivity) {
+                topResumedActivity = child.asActivityRecord();
+            } else if (child.asTaskFragment() != null) {
+                topResumedActivity = child.asTaskFragment().getTopResumedActivity();
+            }
+            if (topResumedActivity != null) {
+                return topResumedActivity;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the currently resumed activity in this TaskFragment's
+     * {@link #mChildren direct children}
+     */
+    ActivityRecord getResumedActivity() {
+        return mResumedActivity;
+    }
+
+    void setResumedActivity(ActivityRecord r, String reason) {
+        warnForNonLeafTaskFragment("setResumedActivity");
+        if (mResumedActivity == r) {
+            return;
+        }
+
+        if (ActivityTaskManagerDebugConfig.DEBUG_ROOT_TASK) {
+            Slog.d(TAG, "setResumedActivity taskFrag:" + this + " + from: "
+                    + mResumedActivity + " to:" + r + " reason:" + reason);
+        }
+        mResumedActivity = r;
+        mTaskSupervisor.updateTopResumedActivityIfNeeded();
+    }
+
+    @VisibleForTesting
+    void setPausingActivity(ActivityRecord pausing) {
+        mPausingActivity = pausing;
+    }
+
+    /** Returns the currently topmost pausing activity. */
+    @Nullable
+    ActivityRecord getTopPausingActivity() {
+        final ActivityRecord taskFragPausingActivity = getPausingActivity();
+        for (int i = getChildCount() - 1; i >= 0; --i) {
+            WindowContainer<?> child = getChildAt(i);
+            ActivityRecord topPausingActivity = null;
+            if (taskFragPausingActivity != null && child == taskFragPausingActivity) {
+                topPausingActivity = child.asActivityRecord();
+            } else if (child.asTaskFragment() != null) {
+                topPausingActivity = child.asTaskFragment().getTopPausingActivity();
+            }
+            if (topPausingActivity != null) {
+                return topPausingActivity;
+            }
+        }
+        return null;
+    }
+
+    ActivityRecord getPausingActivity() {
+        return mPausingActivity;
+    }
+
+    int getDisplayId() {
+        final DisplayContent dc = getDisplayContent();
+        return dc != null ? dc.mDisplayId : INVALID_DISPLAY;
+    }
+
+    @Nullable
+    Task getTask() {
+        if (asTask() != null) {
+            return asTask();
+        }
+
+        TaskFragment parent = getParent() != null ? getParent().asTaskFragment() : null;
+        return parent != null ? parent.getTask() : null;
+    }
+
+    @Override
+    @Nullable
+    TaskDisplayArea getDisplayArea() {
+        return (TaskDisplayArea) super.getDisplayArea();
+    }
+
+    @Override
+    public boolean isAttached() {
+        final TaskDisplayArea taskDisplayArea = getDisplayArea();
+        return taskDisplayArea != null && !taskDisplayArea.isRemoved();
+    }
+
+    /**
+     * Returns the root {@link TaskFragment}, which is usually also a {@link Task}.
+     */
+    @NonNull
+    TaskFragment getRootTaskFragment() {
+        final WindowContainer parent = getParent();
+        if (parent == null) return this;
+
+        final TaskFragment parentTaskFragment = parent.asTaskFragment();
+        return parentTaskFragment == null ? this : parentTaskFragment.getRootTaskFragment();
+    }
+
+    @Nullable
+    Task getRootTask() {
+        return getRootTaskFragment().asTask();
+    }
+
+    @Override
+    TaskFragment asTaskFragment() {
+        return this;
+    }
+
+    @Override
+    boolean isEmbedded() {
+        if (mIsEmbedded) {
+            return true;
+        }
+        final WindowContainer<?> parent = getParent();
+        if (parent != null) {
+            final TaskFragment taskFragment = parent.asTaskFragment();
+            return taskFragment != null && taskFragment.isEmbedded();
+        }
+        return false;
+    }
+
+    /**
+     * Returns the TaskFragment that is being organized, which could be this or the ascendant
+     * TaskFragment.
+     */
+    @Nullable
+    TaskFragment getOrganizedTaskFragment() {
+        if (mTaskFragmentOrganizer != null) {
+            return this;
+        }
+
+        TaskFragment parentTaskFragment = getParent() != null ? getParent().asTaskFragment() : null;
+        return parentTaskFragment != null ? parentTaskFragment.getOrganizedTaskFragment() : null;
+    }
+
+    /**
+     * Simply check and give warning logs if this is not operated on leaf {@link TaskFragment}.
+     */
+    private void warnForNonLeafTaskFragment(String func) {
+        if (!isLeafTaskFragment()) {
+            Slog.w(TAG, func + " on non-leaf task fragment " + this);
+        }
+    }
+
+    boolean hasDirectChildActivities() {
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            if (mChildren.get(i).asActivityRecord() != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void cleanUpActivityReferences(@NonNull ActivityRecord r) {
+        if (mPausingActivity != null && mPausingActivity == r) {
+            mPausingActivity = null;
+        }
+
+        if (mResumedActivity != null && mResumedActivity == r) {
+            setResumedActivity(null, "cleanUpActivityReferences");
+        }
+        r.removeTimeouts();
+    }
+
+    /**
+     * Returns whether this TaskFragment is currently forced to be hidden for any reason.
+     */
+    protected boolean isForceHidden() {
+        return false;
+    }
+
+    boolean isLeafTaskFragment() {
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            if (mChildren.get(i).asTaskFragment() != null) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * This should be called when an child activity changes state. This should only
+     * be called from
+     * {@link ActivityRecord#setState(ActivityRecord.State, String)} .
+     * @param record The {@link ActivityRecord} whose state has changed.
+     * @param state The new state.
+     * @param reason The reason for the change.
+     */
+    void onActivityStateChanged(ActivityRecord record, ActivityRecord.State state,
+            String reason) {
+        warnForNonLeafTaskFragment("onActivityStateChanged");
+        if (record == mResumedActivity && state != RESUMED) {
+            setResumedActivity(null, reason + " - onActivityStateChanged");
+        }
+
+        if (state == RESUMED) {
+            if (ActivityTaskManagerDebugConfig.DEBUG_ROOT_TASK) {
+                Slog.v(TAG, "set resumed activity to:" + record + " reason:" + reason);
+            }
+            setResumedActivity(record, reason + " - onActivityStateChanged");
+            if (record == mRootWindowContainer.getTopResumedActivity()) {
+                mAtmService.setResumedActivityUncheckLocked(record, reason);
+            }
+            mTaskSupervisor.mRecentTasks.add(record.getTask());
+        }
+    }
+
+    /**
+     * Resets local parameters because an app's activity died.
+     * @param app The app of the activity that died.
+     * @return {@code true} if the process of the pausing activity is died.
+     */
+    boolean handleAppDied(WindowProcessController app) {
+        warnForNonLeafTaskFragment("handleAppDied");
+        boolean isPausingDied = false;
+        if (mPausingActivity != null && mPausingActivity.app == app) {
+            ProtoLog.v(WM_DEBUG_STATES, "App died while pausing: %s",
+                    mPausingActivity);
+            mPausingActivity = null;
+            isPausingDied = true;
+        }
+        if (mLastPausedActivity != null && mLastPausedActivity.app == app) {
+            mLastPausedActivity = null;
+        }
+        return isPausingDied;
+    }
+
+    void awakeFromSleeping() {
+        if (mPausingActivity != null) {
+            Slog.d(TAG, "awakeFromSleeping: previously pausing activity didn't pause");
+            mPausingActivity.activityPaused(true);
+        }
+    }
+
+    /**
+     * Tries to put the activities in the task fragment to sleep.
+     *
+     * If the task fragment is not in a state where its activities can be put to sleep, this
+     * function will start any necessary actions to move the task fragment into such a state.
+     * It is expected that this function get called again when those actions complete.
+     *
+     * @param shuttingDown {@code true} when the called because the device is shutting down.
+     * @return true if the root task finished going to sleep, false if the root task only started
+     * the process of going to sleep (checkReadyForSleep will be called when that process finishes).
+     */
+    boolean sleepIfPossible(boolean shuttingDown) {
+        boolean shouldSleep = true;
+        if (mResumedActivity != null) {
+            // Still have something resumed; can't sleep until it is paused.
+            ProtoLog.v(WM_DEBUG_STATES, "Sleep needs to pause %s", mResumedActivity);
+            startPausing(false /* userLeaving */, true /* uiSleeping */, null /* resuming */,
+                    "sleep");
+            shouldSleep = false;
+        } else if (mPausingActivity != null) {
+            // Still waiting for something to pause; can't sleep yet.
+            ProtoLog.v(WM_DEBUG_STATES, "Sleep still waiting to pause %s", mPausingActivity);
+            shouldSleep = false;
+        }
+
+        if (!shuttingDown) {
+            if (containsStoppingActivity()) {
+                // Still need to tell some activities to stop; can't sleep yet.
+                ProtoLog.v(WM_DEBUG_STATES, "Sleep still need to stop %d activities",
+                        mTaskSupervisor.mStoppingActivities.size());
+
+                mTaskSupervisor.scheduleIdle();
+                shouldSleep = false;
+            }
+        }
+
+        if (shouldSleep) {
+            updateActivityVisibilities(null /* starting */, 0 /* configChanges */,
+                    !PRESERVE_WINDOWS, true /* notifyClients */);
+        }
+
+        return shouldSleep;
+    }
+
+    private boolean containsStoppingActivity() {
+        for (int i = mTaskSupervisor.mStoppingActivities.size() - 1; i >= 0; --i) {
+            ActivityRecord r = mTaskSupervisor.mStoppingActivities.get(i);
+            if (r.getTaskFragment() == this) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the TaskFragment is translucent and can have other contents visible behind
+     * it if needed. A TaskFragment is considered translucent if it don't contain a visible or
+     * starting (about to be visible) activity that is fullscreen (opaque).
+     * @param starting The currently starting activity or null if there is none.
+     */
+    @VisibleForTesting
+    boolean isTranslucent(ActivityRecord starting) {
+        if (!isAttached() || isForceHidden()) {
+            return true;
+        }
+        final PooledPredicate p = PooledLambda.obtainPredicate(TaskFragment::isOpaqueActivity,
+                PooledLambda.__(ActivityRecord.class), starting);
+        final ActivityRecord opaque = getActivity(p);
+        p.recycle();
+        return opaque == null;
+    }
+
+    private static boolean isOpaqueActivity(ActivityRecord r, ActivityRecord starting) {
+        if (r.finishing) {
+            // We don't factor in finishing activities when determining translucency since
+            // they will be gone soon.
+            return false;
+        }
+
+        if (!r.visibleIgnoringKeyguard && r != starting) {
+            // Also ignore invisible activities that are not the currently starting
+            // activity (about to be visible).
+            return false;
+        }
+
+        if (r.occludesParent()) {
+            // Root task isn't translucent if it has at least one fullscreen activity
+            // that is visible.
+            return true;
+        }
+        return false;
+    }
+
+    ActivityRecord getTopNonFinishingActivity() {
+        return getTopNonFinishingActivity(true /* includeOverlays */);
+    }
+
+    ActivityRecord getTopNonFinishingActivity(boolean includeOverlays) {
+        return getTopNonFinishingActivity(includeOverlays, true /* includingEmbeddedTask */);
+    }
+
+    /**
+     * Returns the top-most non-finishing activity, even if the activity is NOT ok to show to
+     * the current user.
+     * @param includeOverlays whether the task overlay activity should be included.
+     * @param includingEmbeddedTask whether the activity in a task that being embedded from this
+     *                              one should be included.
+     * @see #topRunningActivity(boolean, boolean)
+     */
+    ActivityRecord getTopNonFinishingActivity(boolean includeOverlays,
+            boolean includingEmbeddedTask) {
+        // Split into 4 to avoid object creation due to variable capture.
+        if (includeOverlays) {
+            if (includingEmbeddedTask) {
+                return getActivity((r) -> !r.finishing);
+            }
+            return getActivity((r) -> !r.finishing && r.getTask() == this.getTask());
+        }
+
+        if (includingEmbeddedTask) {
+            return getActivity((r) -> !r.finishing && !r.isTaskOverlay());
+        }
+        return getActivity(
+                (r) -> !r.finishing && !r.isTaskOverlay() && r.getTask() == this.getTask());
+    }
+
+    ActivityRecord topRunningActivity() {
+        return topRunningActivity(false /* focusableOnly */);
+    }
+
+    ActivityRecord topRunningActivity(boolean focusableOnly) {
+        return topRunningActivity(focusableOnly, true /* includingEmbeddedTask */);
+    }
+
+    /**
+     * Returns the top-most running activity, which the activity is non-finishing and ok to show
+     * to the current user.
+     *
+     * @see ActivityRecord#canBeTopRunning()
+     */
+    ActivityRecord topRunningActivity(boolean focusableOnly, boolean includingEmbeddedTask) {
+        // Split into 4 to avoid object creation due to variable capture.
+        if (focusableOnly) {
+            if (includingEmbeddedTask) {
+                return getActivity((r) -> r.canBeTopRunning() && r.isFocusable());
+            }
+            return getActivity(
+                    (r) -> r.canBeTopRunning() && r.isFocusable() && r.getTask() == this.getTask());
+        }
+
+        if (includingEmbeddedTask) {
+            return getActivity(ActivityRecord::canBeTopRunning);
+        }
+        return getActivity((r) -> r.canBeTopRunning() && r.getTask() == this.getTask());
+    }
+
+    boolean isTopActivityFocusable() {
+        final ActivityRecord r = topRunningActivity();
+        return r != null ? r.isFocusable()
+                : (isFocusable() && getWindowConfiguration().canReceiveKeys());
+    }
+
+    /**
+     * Returns the visibility state of this TaskFragment.
+     *
+     * @param starting The currently starting activity or null if there is none.
+     */
+    @TaskFragmentVisibility
+    int getVisibility(ActivityRecord starting) {
+        if (!isAttached() || isForceHidden()) {
+            return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+        }
+
+        if (isTopActivityLaunchedBehind()) {
+            return TASK_FRAGMENT_VISIBILITY_VISIBLE;
+        }
+
+        boolean gotRootSplitScreenFragment = false;
+        boolean gotOpaqueSplitScreenPrimary = false;
+        boolean gotOpaqueSplitScreenSecondary = false;
+        boolean gotTranslucentFullscreen = false;
+        boolean gotTranslucentSplitScreenPrimary = false;
+        boolean gotTranslucentSplitScreenSecondary = false;
+        boolean shouldBeVisible = true;
+
+        // This TaskFragment is only considered visible if all its parent TaskFragments are
+        // considered visible, so check the visibility of all ancestor TaskFragment first.
+        final WindowContainer parent = getParent();
+        if (parent.asTaskFragment() != null) {
+            final int parentVisibility = parent.asTaskFragment().getVisibility(starting);
+            if (parentVisibility == TASK_FRAGMENT_VISIBILITY_INVISIBLE) {
+                // Can't be visible if parent isn't visible
+                return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+            } else if (parentVisibility == TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT) {
+                // Parent is behind a translucent container so the highest visibility this container
+                // can get is that.
+                gotTranslucentFullscreen = true;
+            }
+        }
+
+        final List<TaskFragment> adjacentTaskFragments = new ArrayList<>();
+        final int windowingMode = getWindowingMode();
+        final boolean isAssistantType = isActivityTypeAssistant();
+        for (int i = parent.getChildCount() - 1; i >= 0; --i) {
+            final WindowContainer other = parent.getChildAt(i);
+            if (other == null) continue;
+
+            final boolean hasRunningActivities = hasRunningActivity(other);
+            if (other == this) {
+                // Should be visible if there is no other fragment occluding it, unless it doesn't
+                // have any running activities, not starting one and not home stack.
+                shouldBeVisible = hasRunningActivities
+                        || (starting != null && starting.isDescendantOf(this))
+                        || isActivityTypeHome();
+                break;
+            }
+
+            if (!hasRunningActivities) {
+                continue;
+            }
+
+            final int otherWindowingMode = other.getWindowingMode();
+            if (otherWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+                if (isTranslucent(other, starting)) {
+                    // Can be visible behind a translucent fullscreen TaskFragment.
+                    gotTranslucentFullscreen = true;
+                    continue;
+                }
+                return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+            } else if (otherWindowingMode == WINDOWING_MODE_MULTI_WINDOW
+                    && other.matchParentBounds()) {
+                if (isTranslucent(other, starting)) {
+                    // Can be visible behind a translucent TaskFragment.
+                    gotTranslucentFullscreen = true;
+                    continue;
+                }
+                // Multi-window TaskFragment that matches parent bounds would occlude other children
+                return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+            } else if (otherWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+                    && !gotOpaqueSplitScreenPrimary) {
+                gotRootSplitScreenFragment = true;
+                gotTranslucentSplitScreenPrimary = isTranslucent(other, starting);
+                gotOpaqueSplitScreenPrimary = !gotTranslucentSplitScreenPrimary;
+                if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+                        && gotOpaqueSplitScreenPrimary) {
+                    // Can't be visible behind another opaque TaskFragment in split-screen-primary.
+                    return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+                }
+            } else if (otherWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
+                    && !gotOpaqueSplitScreenSecondary) {
+                gotRootSplitScreenFragment = true;
+                gotTranslucentSplitScreenSecondary = isTranslucent(other, starting);
+                gotOpaqueSplitScreenSecondary = !gotTranslucentSplitScreenSecondary;
+                if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
+                        && gotOpaqueSplitScreenSecondary) {
+                    // Can't be visible behind another opaque TaskFragment in split-screen-secondary
+                    return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+                }
+            }
+            if (gotOpaqueSplitScreenPrimary && gotOpaqueSplitScreenSecondary) {
+                // Can not be visible if we are in split-screen windowing mode and both halves of
+                // the screen are opaque.
+                return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+            }
+            if (isAssistantType && gotRootSplitScreenFragment) {
+                // Assistant TaskFragment can't be visible behind split-screen. In addition to
+                // this not making sense, it also works around an issue here we boost the z-order
+                // of the assistant window surfaces in window manager whenever it is visible.
+                return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+            }
+
+            final TaskFragment otherTaskFrag = other.asTaskFragment();
+            if (otherTaskFrag != null && otherTaskFrag.mAdjacentTaskFragment != null) {
+                if (adjacentTaskFragments.contains(otherTaskFrag.mAdjacentTaskFragment)) {
+                    if (otherTaskFrag.isTranslucent(starting)
+                            || otherTaskFrag.mAdjacentTaskFragment.isTranslucent(starting)) {
+                        // Can be visible behind a translucent adjacent TaskFragments.
+                        gotTranslucentFullscreen = true;
+                        continue;
+                    }
+                    // Can not be visible behind adjacent TaskFragments.
+                    return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+                } else {
+                    adjacentTaskFragments.add(otherTaskFrag);
+                }
+            }
+
+        }
+
+        if (!shouldBeVisible) {
+            return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+        }
+
+        // Handle cases when there can be a translucent split-screen TaskFragment on top.
+        switch (windowingMode) {
+            case WINDOWING_MODE_FULLSCREEN:
+                if (gotTranslucentSplitScreenPrimary || gotTranslucentSplitScreenSecondary) {
+                    // At least one of the split-screen TaskFragment that covers this one is
+                    // translucent.
+                    // When in split mode, home will be reparented to the secondary split while
+                    // leaving TaskFragments not supporting split below. Due to
+                    // TaskDisplayArea#assignRootTaskOrdering always adjusts home surface layer to
+                    // the bottom, this makes sure TaskFragments not in split roots won't occlude
+                    // home task unexpectedly.
+                    return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+                }
+                break;
+            case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY:
+                if (gotTranslucentSplitScreenPrimary) {
+                    // Covered by translucent primary split-screen on top.
+                    return TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
+                }
+                break;
+            case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY:
+                if (gotTranslucentSplitScreenSecondary) {
+                    // Covered by translucent secondary split-screen on top.
+                    return TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
+                }
+                break;
+        }
+
+        // Lastly - check if there is a translucent fullscreen TaskFragment on top.
+        return gotTranslucentFullscreen
+                ? TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT
+                : TASK_FRAGMENT_VISIBILITY_VISIBLE;
+    }
+
+    private static boolean hasRunningActivity(WindowContainer wc) {
+        if (wc.asTaskFragment() != null) {
+            return wc.asTaskFragment().topRunningActivity() != null;
+        }
+        return wc.asActivityRecord() != null && !wc.asActivityRecord().finishing;
+    }
+
+    private static boolean isTranslucent(WindowContainer wc, ActivityRecord starting) {
+        if (wc.asTaskFragment() != null) {
+            return wc.asTaskFragment().isTranslucent(starting);
+        } else if (wc.asActivityRecord() != null) {
+            return !wc.asActivityRecord().occludesParent();
+        }
+        return false;
+    }
+
+
+    private boolean isTopActivityLaunchedBehind() {
+        final ActivityRecord top = topRunningActivity();
+        return top != null && top.mLaunchTaskBehind;
+    }
+
+    final void updateActivityVisibilities(@Nullable ActivityRecord starting, int configChanges,
+            boolean preserveWindows, boolean notifyClients) {
+        mTaskSupervisor.beginActivityVisibilityUpdate();
+        try {
+            mEnsureActivitiesVisibleHelper.process(
+                    starting, configChanges, preserveWindows, notifyClients);
+        } finally {
+            mTaskSupervisor.endActivityVisibilityUpdate();
+        }
+    }
+
+    final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,
+            boolean deferPause) {
+        ActivityRecord next = topRunningActivity(true /* focusableOnly */);
+        if (next == null || !next.canResumeByCompat()) {
+            return false;
+        }
+
+        next.delayedResume = false;
+        final TaskDisplayArea taskDisplayArea = getDisplayArea();
+
+        // If the top activity is the resumed one, nothing to do.
+        if (mResumedActivity == next && next.isState(RESUMED)
+                && taskDisplayArea.allResumedActivitiesComplete()) {
+            // Make sure we have executed any pending transitions, since there
+            // should be nothing left to do at this point.
+            executeAppTransition(options);
+            // For devices that are not in fullscreen mode (e.g. freeform windows), it's possible
+            // we still want to check if the visibility of other windows have changed (e.g. bringing
+            // a fullscreen window forward to cover another freeform activity.)
+            if (taskDisplayArea.inMultiWindowMode()) {
+                taskDisplayArea.ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
+                        false /* preserveWindows */, true /* notifyClients */);
+            }
+            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Top activity "
+                    + "resumed %s", next);
+            return false;
+        }
+
+        // If we are currently pausing an activity, then don't do anything until that is done.
+        final boolean allPausedComplete = mRootWindowContainer.allPausedActivitiesComplete();
+        if (!allPausedComplete) {
+            ProtoLog.v(WM_DEBUG_STATES,
+                    "resumeTopActivity: Skip resume: some activity pausing.");
+            return false;
+        }
+
+        // If we are sleeping, and there is no resumed activity, and the top activity is paused,
+        // well that is the state we want.
+        if (mLastPausedActivity == next && shouldSleepOrShutDownActivities()) {
+            // Make sure we have executed any pending transitions, since there
+            // should be nothing left to do at this point.
+            executeAppTransition(options);
+            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Going to sleep and"
+                    + " all paused");
+            return false;
+        }
+
+        // Make sure that the user who owns this activity is started.  If not,
+        // we will just leave it as is because someone should be bringing
+        // another user's activities to the top of the stack.
+        if (!mAtmService.mAmInternal.hasStartedUserState(next.mUserId)) {
+            Slog.w(TAG, "Skipping resume of top activity " + next
+                    + ": user " + next.mUserId + " is stopped");
+            return false;
+        }
+
+        // The activity may be waiting for stop, but that is no longer
+        // appropriate for it.
+        mTaskSupervisor.mStoppingActivities.remove(next);
+
+        if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Resuming " + next);
+
+        mTaskSupervisor.setLaunchSource(next.info.applicationInfo.uid);
+
+        ActivityRecord lastResumed = null;
+        final Task lastFocusedRootTask = taskDisplayArea.getLastFocusedRootTask();
+        if (lastFocusedRootTask != null && lastFocusedRootTask != getRootTaskFragment().asTask()) {
+            // So, why aren't we using prev here??? See the param comment on the method. prev
+            // doesn't represent the last resumed activity. However, the last focus stack does if
+            // it isn't null.
+            lastResumed = lastFocusedRootTask.getTopResumedActivity();
+        }
+
+        boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next);
+        if (mResumedActivity != null) {
+            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Pausing %s", mResumedActivity);
+            pausing |= startPausing(mTaskSupervisor.mUserLeaving, false /* uiSleeping */,
+                    next, "resumeTopActivity");
+        }
+        if (pausing) {
+            ProtoLog.v(WM_DEBUG_STATES, "resumeTopActivity: Skip resume: need to"
+                    + " start pausing");
+            // At this point we want to put the upcoming activity's process
+            // at the top of the LRU list, since we know we will be needing it
+            // very soon and it would be a waste to let it get killed if it
+            // happens to be sitting towards the end.
+            if (next.attachedToProcess()) {
+                next.app.updateProcessInfo(false /* updateServiceConnectionActivities */,
+                        true /* activityChange */, false /* updateOomAdj */,
+                        false /* addPendingTopUid */);
+            } else if (!next.isProcessRunning()) {
+                // Since the start-process is asynchronous, if we already know the process of next
+                // activity isn't running, we can start the process earlier to save the time to wait
+                // for the current activity to be paused.
+                final boolean isTop = this == taskDisplayArea.getFocusedRootTask();
+                mAtmService.startProcessAsync(next, false /* knownToBeDead */, isTop,
+                        isTop ? "pre-top-activity" : "pre-activity");
+            }
+            if (lastResumed != null) {
+                lastResumed.setWillCloseOrEnterPip(true);
+            }
+            return true;
+        } else if (mResumedActivity == next && next.isState(RESUMED)
+                && taskDisplayArea.allResumedActivitiesComplete()) {
+            // It is possible for the activity to be resumed when we paused back stacks above if the
+            // next activity doesn't have to wait for pause to complete.
+            // So, nothing else to-do except:
+            // Make sure we have executed any pending transitions, since there
+            // should be nothing left to do at this point.
+            executeAppTransition(options);
+            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Top activity resumed "
+                    + "(dontWaitForPause) %s", next);
+            return true;
+        }
+
+        // If the most recent activity was noHistory but was only stopped rather
+        // than stopped+finished because the device went to sleep, we need to make
+        // sure to finish it as we're making a new activity topmost.
+        if (shouldSleepActivities()) {
+            mTaskSupervisor.finishNoHistoryActivitiesIfNeeded(next);
+        }
+
+        if (prev != null && prev != next && next.nowVisible) {
+            // The next activity is already visible, so hide the previous
+            // activity's windows right now so we can show the new one ASAP.
+            // We only do this if the previous is finishing, which should mean
+            // it is on top of the one being resumed so hiding it quickly
+            // is good.  Otherwise, we want to do the normal route of allowing
+            // the resumed activity to be shown so we can decide if the
+            // previous should actually be hidden depending on whether the
+            // new one is found to be full-screen or not.
+            if (prev.finishing) {
+                prev.setVisibility(false);
+                if (DEBUG_SWITCH) {
+                    Slog.v(TAG_SWITCH, "Not waiting for visible to hide: " + prev
+                            + ", nowVisible=" + next.nowVisible);
+                }
+            } else {
+                if (DEBUG_SWITCH) {
+                    Slog.v(TAG_SWITCH, "Previous already visible but still waiting to hide: " + prev
+                            + ", nowVisible=" + next.nowVisible);
+                }
+            }
+        }
+
+        // Launching this app's activity, make sure the app is no longer
+        // considered stopped.
+        try {
+            mTaskSupervisor.getActivityMetricsLogger()
+                    .notifyBeforePackageUnstopped(next.packageName);
+            mAtmService.getPackageManager().setPackageStoppedState(
+                    next.packageName, false, next.mUserId); /* TODO: Verify if correct userid */
+        } catch (RemoteException e1) {
+        } catch (IllegalArgumentException e) {
+            Slog.w(TAG, "Failed trying to unstop package "
+                    + next.packageName + ": " + e);
+        }
+
+        // We are starting up the next activity, so tell the window manager
+        // that the previous one will be hidden soon.  This way it can know
+        // to ignore it when computing the desired screen orientation.
+        boolean anim = true;
+        final DisplayContent dc = taskDisplayArea.mDisplayContent;
+        if (prev != null) {
+            if (prev.finishing) {
+                if (DEBUG_TRANSITION) {
+                    Slog.v(TAG_TRANSITION, "Prepare close transition: prev=" + prev);
+                }
+                if (mTaskSupervisor.mNoAnimActivities.contains(prev)) {
+                    anim = false;
+                    dc.prepareAppTransition(TRANSIT_NONE);
+                } else {
+                    dc.prepareAppTransition(TRANSIT_CLOSE);
+                }
+                prev.setVisibility(false);
+            } else {
+                if (DEBUG_TRANSITION) {
+                    Slog.v(TAG_TRANSITION, "Prepare open transition: prev=" + prev);
+                }
+                if (mTaskSupervisor.mNoAnimActivities.contains(next)) {
+                    anim = false;
+                    dc.prepareAppTransition(TRANSIT_NONE);
+                } else {
+                    dc.prepareAppTransition(TRANSIT_OPEN,
+                            next.mLaunchTaskBehind ? TRANSIT_FLAG_OPEN_BEHIND : 0);
+                }
+            }
+        } else {
+            if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION, "Prepare open transition: no previous");
+            if (mTaskSupervisor.mNoAnimActivities.contains(next)) {
+                anim = false;
+                dc.prepareAppTransition(TRANSIT_NONE);
+            } else {
+                dc.prepareAppTransition(TRANSIT_OPEN);
+            }
+        }
+
+        if (anim) {
+            next.applyOptionsAnimation();
+        } else {
+            next.abortAndClearOptionsAnimation();
+        }
+
+        mTaskSupervisor.mNoAnimActivities.clear();
+
+        if (next.attachedToProcess()) {
+            if (DEBUG_SWITCH) {
+                Slog.v(TAG_SWITCH, "Resume running: " + next + " stopped=" + next.stopped
+                        + " visibleRequested=" + next.mVisibleRequested);
+            }
+
+            // If the previous activity is translucent, force a visibility update of
+            // the next activity, so that it's added to WM's opening app list, and
+            // transition animation can be set up properly.
+            // For example, pressing Home button with a translucent activity in focus.
+            // Launcher is already visible in this case. If we don't add it to opening
+            // apps, maybeUpdateTransitToWallpaper() will fail to identify this as a
+            // TRANSIT_WALLPAPER_OPEN animation, and run some funny animation.
+            final boolean lastActivityTranslucent = inMultiWindowMode()
+                    || mLastPausedActivity != null && !mLastPausedActivity.occludesParent();
+
+            // This activity is now becoming visible.
+            if (!next.mVisibleRequested || next.stopped || lastActivityTranslucent) {
+                next.setVisibility(true);
+            }
+
+            // schedule launch ticks to collect information about slow apps.
+            next.startLaunchTickingLocked();
+
+            ActivityRecord lastResumedActivity =
+                    lastFocusedRootTask == null ? null
+                            : lastFocusedRootTask.getTopResumedActivity();
+            final ActivityRecord.State lastState = next.getState();
+
+            mAtmService.updateCpuStats();
+
+            ProtoLog.v(WM_DEBUG_STATES, "Moving to RESUMED: %s (in existing)", next);
+
+            next.setState(RESUMED, "resumeTopActivity");
+
+            // Have the window manager re-evaluate the orientation of
+            // the screen based on the new activity order.
+            boolean notUpdated = true;
+
+            // Activity should also be visible if set mLaunchTaskBehind to true (see
+            // ActivityRecord#shouldBeVisibleIgnoringKeyguard()).
+            if (shouldBeVisible(next)) {
+                // We have special rotation behavior when here is some active activity that
+                // requests specific orientation or Keyguard is locked. Make sure all activity
+                // visibilities are set correctly as well as the transition is updated if needed
+                // to get the correct rotation behavior. Otherwise the following call to update
+                // the orientation may cause incorrect configurations delivered to client as a
+                // result of invisible window resize.
+                // TODO: Remove this once visibilities are set correctly immediately when
+                // starting an activity.
+                notUpdated = !mRootWindowContainer.ensureVisibilityAndConfig(next, getDisplayId(),
+                        true /* markFrozenIfConfigChanged */, false /* deferResume */);
+            }
+
+            if (notUpdated) {
+                // The configuration update wasn't able to keep the existing
+                // instance of the activity, and instead started a new one.
+                // We should be all done, but let's just make sure our activity
+                // is still at the top and schedule another run if something
+                // weird happened.
+                ActivityRecord nextNext = topRunningActivity();
+                ProtoLog.i(WM_DEBUG_STATES, "Activity config changed during resume: "
+                        + "%s, new next: %s", next, nextNext);
+                if (nextNext != next) {
+                    // Do over!
+                    mTaskSupervisor.scheduleResumeTopActivities();
+                }
+                if (!next.mVisibleRequested || next.stopped) {
+                    next.setVisibility(true);
+                }
+                next.completeResumeLocked();
+                return true;
+            }
+
+            try {
+                final ClientTransaction transaction =
+                        ClientTransaction.obtain(next.app.getThread(), next.appToken);
+                // Deliver all pending results.
+                ArrayList<ResultInfo> a = next.results;
+                if (a != null) {
+                    final int size = a.size();
+                    if (!next.finishing && size > 0) {
+                        if (DEBUG_RESULTS) {
+                            Slog.v(TAG_RESULTS, "Delivering results to " + next + ": " + a);
+                        }
+                        transaction.addCallback(ActivityResultItem.obtain(a));
+                    }
+                }
+
+                if (next.newIntents != null) {
+                    transaction.addCallback(
+                            NewIntentItem.obtain(next.newIntents, true /* resume */));
+                }
+
+                // Well the app will no longer be stopped.
+                // Clear app token stopped state in window manager if needed.
+                next.notifyAppResumed(next.stopped);
+
+                EventLogTags.writeWmResumeActivity(next.mUserId, System.identityHashCode(next),
+                        next.getTask().mTaskId, next.shortComponentName);
+
+                mAtmService.getAppWarningsLocked().onResumeActivity(next);
+                next.app.setPendingUiCleanAndForceProcessStateUpTo(mAtmService.mTopProcessState);
+                next.abortAndClearOptionsAnimation();
+                transaction.setLifecycleStateRequest(
+                        ResumeActivityItem.obtain(next.app.getReportedProcState(),
+                                dc.isNextTransitionForward()));
+                mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+
+                ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Resumed %s", next);
+            } catch (Exception e) {
+                // Whoops, need to restart this activity!
+                ProtoLog.v(WM_DEBUG_STATES, "Resume failed; resetting state to %s: "
+                        + "%s", lastState, next);
+                next.setState(lastState, "resumeTopActivityInnerLocked");
+
+                // lastResumedActivity being non-null implies there is a lastStack present.
+                if (lastResumedActivity != null) {
+                    lastResumedActivity.setState(RESUMED, "resumeTopActivityInnerLocked");
+                }
+
+                Slog.i(TAG, "Restarting because process died: " + next);
+                if (!next.hasBeenLaunched) {
+                    next.hasBeenLaunched = true;
+                } else if (SHOW_APP_STARTING_PREVIEW && lastFocusedRootTask != null
+                        && lastFocusedRootTask.isTopRootTaskInDisplayArea()) {
+                    next.showStartingWindow(false /* taskSwitch */);
+                }
+                mTaskSupervisor.startSpecificActivity(next, true, false);
+                return true;
+            }
+
+            // From this point on, if something goes wrong there is no way
+            // to recover the activity.
+            try {
+                next.completeResumeLocked();
+            } catch (Exception e) {
+                // If any exception gets thrown, toss away this
+                // activity and try the next one.
+                Slog.w(TAG, "Exception thrown during resume of " + next, e);
+                next.finishIfPossible("resume-exception", true /* oomAdj */);
+                return true;
+            }
+        } else {
+            // Whoops, need to restart this activity!
+            if (!next.hasBeenLaunched) {
+                next.hasBeenLaunched = true;
+            } else {
+                if (SHOW_APP_STARTING_PREVIEW) {
+                    next.showStartingWindow(false /* taskSwich */);
+                }
+                if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Restarting: " + next);
+            }
+            ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Restarting %s", next);
+            mTaskSupervisor.startSpecificActivity(next, true, true);
+        }
+
+        return true;
+    }
+
+    boolean shouldSleepOrShutDownActivities() {
+        return shouldSleepActivities() || mAtmService.mShuttingDown;
+    }
+
+    /**
+     * Returns true if the TaskFragment should be visible.
+     *
+     * @param starting The currently starting activity or null if there is none.
+     */
+    boolean shouldBeVisible(ActivityRecord starting) {
+        return getVisibility(starting) != TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+    }
+
+    boolean isFocusableAndVisible() {
+        return isTopActivityFocusable() && shouldBeVisible(null /* starting */);
+    }
+
+    final boolean startPausing(boolean uiSleeping, ActivityRecord resuming, String reason) {
+        return startPausing(mTaskSupervisor.mUserLeaving, uiSleeping, resuming, reason);
+    }
+
+    /**
+     * Start pausing the currently resumed activity.  It is an error to call this if there
+     * is already an activity being paused or there is no resumed activity.
+     *
+     * @param userLeaving True if this should result in an onUserLeaving to the current activity.
+     * @param uiSleeping True if this is happening with the user interface going to sleep (the
+     * screen turning off).
+     * @param resuming The activity we are currently trying to resume or null if this is not being
+     *                 called as part of resuming the top activity, so we shouldn't try to instigate
+     *                 a resume here if not null.
+     * @param reason The reason of pausing the activity.
+     * @return Returns true if an activity now is in the PAUSING state, and we are waiting for
+     * it to tell us when it is done.
+     */
+    boolean startPausing(boolean userLeaving, boolean uiSleeping, ActivityRecord resuming,
+            String reason) {
+        if (!hasDirectChildActivities()) {
+            return false;
+        }
+
+        ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag =%s " + "mResumedActivity=%s", this,
+                mResumedActivity);
+
+        if (mPausingActivity != null) {
+            Slog.wtf(TAG, "Going to pause when pause is already pending for " + mPausingActivity
+                    + " state=" + mPausingActivity.getState());
+            if (!shouldSleepActivities()) {
+                // Avoid recursion among check for sleep and complete pause during sleeping.
+                // Because activity will be paused immediately after resume, just let pause
+                // be completed by the order of activity paused from clients.
+                completePause(false, resuming);
+            }
+        }
+        ActivityRecord prev = mResumedActivity;
+
+        if (prev == null) {
+            if (resuming == null) {
+                Slog.wtf(TAG, "Trying to pause when nothing is resumed");
+                mRootWindowContainer.resumeFocusedTasksTopActivities();
+            }
+            return false;
+        }
+
+        if (prev == resuming) {
+            Slog.wtf(TAG, "Trying to pause activity that is in process of being resumed");
+            return false;
+        }
+
+        ProtoLog.v(WM_DEBUG_STATES, "Moving to PAUSING: %s", prev);
+        mPausingActivity = prev;
+        mLastPausedActivity = prev;
+        if (!prev.finishing && prev.isNoHistory()
+                && !mTaskSupervisor.mNoHistoryActivities.contains(prev)) {
+            mTaskSupervisor.mNoHistoryActivities.add(prev);
+        }
+        prev.setState(PAUSING, "startPausingLocked");
+        prev.getTask().touchActiveTime();
+
+        mAtmService.updateCpuStats();
+
+        boolean pauseImmediately = false;
+        boolean shouldAutoPip = false;
+        if (resuming != null && (resuming.info.flags & FLAG_RESUME_WHILE_PAUSING) != 0) {
+            // If the flag RESUME_WHILE_PAUSING is set, then continue to schedule the previous
+            // activity to be paused, while at the same time resuming the new resume activity
+            // only if the previous activity can't go into Pip since we want to give Pip
+            // activities a chance to enter Pip before resuming the next activity.
+            final boolean lastResumedCanPip = prev != null && prev.checkEnterPictureInPictureState(
+                    "shouldResumeWhilePausing", userLeaving);
+            if (lastResumedCanPip && prev.pictureInPictureArgs.isAutoEnterEnabled()) {
+                shouldAutoPip = true;
+            } else if (!lastResumedCanPip) {
+                pauseImmediately = true;
+            } else {
+                // The previous activity may still enter PIP even though it did not allow auto-PIP.
+            }
+        }
+
+        boolean didAutoPip = false;
+        if (prev.attachedToProcess()) {
+            if (shouldAutoPip) {
+                ProtoLog.d(WM_DEBUG_STATES, "Auto-PIP allowed, entering PIP mode "
+                        + "directly: %s", prev);
+
+                didAutoPip = mAtmService.enterPictureInPictureMode(prev, prev.pictureInPictureArgs);
+            } else {
+                schedulePauseActivity(prev, userLeaving, pauseImmediately, reason);
+            }
+        } else {
+            mPausingActivity = null;
+            mLastPausedActivity = null;
+            mTaskSupervisor.mNoHistoryActivities.remove(prev);
+        }
+
+        // If we are not going to sleep, we want to ensure the device is
+        // awake until the next activity is started.
+        if (!uiSleeping && !mAtmService.isSleepingOrShuttingDownLocked()) {
+            mTaskSupervisor.acquireLaunchWakelock();
+        }
+
+        // If already entered PIP mode, no need to keep pausing.
+        if (mPausingActivity != null) {
+            // Have the window manager pause its key dispatching until the new
+            // activity has started.  If we're pausing the activity just because
+            // the screen is being turned off and the UI is sleeping, don't interrupt
+            // key dispatch; the same activity will pick it up again on wakeup.
+            if (!uiSleeping) {
+                prev.pauseKeyDispatchingLocked();
+            } else {
+                ProtoLog.v(WM_DEBUG_STATES, "Key dispatch not paused for screen off");
+            }
+
+            if (pauseImmediately) {
+                // If the caller said they don't want to wait for the pause, then complete
+                // the pause now.
+                completePause(false, resuming);
+                return false;
+
+            } else {
+                prev.schedulePauseTimeout();
+                // Unset readiness since we now need to wait until this pause is complete.
+                mTransitionController.setReady(this, false /* ready */);
+                return true;
+            }
+
+        } else {
+            // This activity either failed to schedule the pause or it entered PIP mode,
+            // so just treat it as being paused now.
+            ProtoLog.v(WM_DEBUG_STATES, "Activity not running or entered PiP, resuming next.");
+            if (resuming == null) {
+                mRootWindowContainer.resumeFocusedTasksTopActivities();
+            }
+            return false;
+        }
+    }
+
+    void schedulePauseActivity(ActivityRecord prev, boolean userLeaving,
+            boolean pauseImmediately, String reason) {
+        ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev);
+        try {
+            EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
+                    prev.shortComponentName, "userLeaving=" + userLeaving, reason);
+
+            mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
+                    prev.appToken, PauseActivityItem.obtain(prev.finishing, userLeaving,
+                            prev.configChangeFlags, pauseImmediately));
+        } catch (Exception e) {
+            // Ignore exception, if process died other code will cleanup.
+            Slog.w(TAG, "Exception thrown during pause", e);
+            mPausingActivity = null;
+            mLastPausedActivity = null;
+            mTaskSupervisor.mNoHistoryActivities.remove(prev);
+        }
+    }
+
+    @VisibleForTesting
+    void completePause(boolean resumeNext, ActivityRecord resuming) {
+        // Complete the pausing process of a pausing activity, so it doesn't make sense to
+        // operate on non-leaf tasks.
+        // warnForNonLeafTask("completePauseLocked");
+
+        ActivityRecord prev = mPausingActivity;
+        ProtoLog.v(WM_DEBUG_STATES, "Complete pause: %s", prev);
+
+        if (prev != null) {
+            prev.setWillCloseOrEnterPip(false);
+            final boolean wasStopping = prev.isState(STOPPING);
+            prev.setState(PAUSED, "completePausedLocked");
+            if (prev.finishing) {
+                // We will update the activity visibility later, no need to do in
+                // completeFinishing(). Updating visibility here might also making the next
+                // activities to be resumed, and could result in wrong app transition due to
+                // lack of previous activity information.
+                ProtoLog.v(WM_DEBUG_STATES, "Executing finish of activity: %s", prev);
+                prev = prev.completeFinishing(false /* updateVisibility */,
+                        "completePausedLocked");
+            } else if (prev.hasProcess()) {
+                ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
+                                + "wasStopping=%b visibleRequested=%b",  prev,  wasStopping,
+                        prev.mVisibleRequested);
+                if (prev.deferRelaunchUntilPaused) {
+                    // Complete the deferred relaunch that was waiting for pause to complete.
+                    ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev);
+                    prev.relaunchActivityLocked(prev.preserveWindowOnDeferredRelaunch);
+                } else if (wasStopping) {
+                    // We are also stopping, the stop request must have gone soon after the pause.
+                    // We can't clobber it, because the stop confirmation will not be handled.
+                    // We don't need to schedule another stop, we only need to let it happen.
+                    prev.setState(STOPPING, "completePausedLocked");
+                } else if (!prev.mVisibleRequested || shouldSleepOrShutDownActivities()) {
+                    // Clear out any deferred client hide we might currently have.
+                    prev.setDeferHidingClient(false);
+                    // If we were visible then resumeTopActivities will release resources before
+                    // stopping.
+                    prev.addToStopping(true /* scheduleIdle */, false /* idleDelayed */,
+                            "completePauseLocked");
+                }
+            } else {
+                ProtoLog.v(WM_DEBUG_STATES, "App died during pause, not stopping: %s", prev);
+                prev = null;
+            }
+            // It is possible the activity was freezing the screen before it was paused.
+            // In that case go ahead and remove the freeze this activity has on the screen
+            // since it is no longer visible.
+            if (prev != null) {
+                prev.stopFreezingScreenLocked(true /*force*/);
+            }
+            mPausingActivity = null;
+        }
+
+        if (resumeNext) {
+            final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
+            if (topRootTask != null && !topRootTask.shouldSleepOrShutDownActivities()) {
+                mRootWindowContainer.resumeFocusedTasksTopActivities(topRootTask, prev,
+                        null /* targetOptions */);
+            } else {
+                // checkReadyForSleep();
+                final ActivityRecord top =
+                        topRootTask != null ? topRootTask.topRunningActivity() : null;
+                if (top == null || (prev != null && top != prev)) {
+                    // If there are no more activities available to run, do resume anyway to start
+                    // something. Also if the top activity on the root task is not the just paused
+                    // activity, we need to go ahead and resume it to ensure we complete an
+                    // in-flight app switch.
+                    mRootWindowContainer.resumeFocusedTasksTopActivities();
+                }
+            }
+        }
+
+        if (prev != null) {
+            prev.resumeKeyDispatchingLocked();
+        }
+
+        mRootWindowContainer.ensureActivitiesVisible(resuming, 0, !PRESERVE_WINDOWS);
+
+        // Notify when the task stack has changed, but only if visibilities changed (not just
+        // focus). Also if there is an active root pinned task - we always want to notify it about
+        // task stack changes, because its positioning may depend on it.
+        if (mTaskSupervisor.mAppVisibilitiesChangedSinceLastPause
+                || (getDisplayArea() != null && getDisplayArea().hasPinnedTask())) {
+            mAtmService.getTaskChangeNotificationController().notifyTaskStackChanged();
+            mTaskSupervisor.mAppVisibilitiesChangedSinceLastPause = false;
+        }
+    }
+
+    @Override
+    void forAllTaskFragments(Consumer<TaskFragment> callback, boolean traverseTopToBottom) {
+        super.forAllTaskFragments(callback, traverseTopToBottom);
+        callback.accept(this);
+    }
+
+    @Override
+    void forAllLeafTaskFragments(Consumer<TaskFragment> callback, boolean traverseTopToBottom) {
+        final int count = mChildren.size();
+        boolean isLeafTaskFrag = true;
+        if (traverseTopToBottom) {
+            for (int i = count - 1; i >= 0; --i) {
+                final TaskFragment child = mChildren.get(i).asTaskFragment();
+                if (child != null) {
+                    isLeafTaskFrag = false;
+                    child.forAllLeafTaskFragments(callback, traverseTopToBottom);
+                }
+            }
+        } else {
+            for (int i = 0; i < count; i++) {
+                final TaskFragment child = mChildren.get(i).asTaskFragment();
+                if (child != null) {
+                    isLeafTaskFrag = false;
+                    child.forAllLeafTaskFragments(callback, traverseTopToBottom);
+                }
+            }
+        }
+        if (isLeafTaskFrag) callback.accept(this);
+    }
+
+    @Override
+    boolean forAllLeafTaskFragments(Function<TaskFragment, Boolean> callback) {
+        boolean isLeafTaskFrag = true;
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            final TaskFragment child = mChildren.get(i).asTaskFragment();
+            if (child != null) {
+                isLeafTaskFrag = false;
+                if (child.forAllLeafTaskFragments(callback)) {
+                    return true;
+                }
+            }
+        }
+        if (isLeafTaskFrag) {
+            return callback.apply(this);
+        }
+        return false;
+    }
+
+    void addChild(ActivityRecord r) {
+        addChild(r, POSITION_TOP);
+    }
+
+    @Override
+    void addChild(WindowContainer child, int index) {
+        mClearedTaskForReuse = false;
+
+        boolean isAddingActivity = child.asActivityRecord() != null;
+        final Task task = isAddingActivity ? getTask() : null;
+
+        // If this task had any child before we added this one.
+        boolean taskHadChild = task != null && task.hasChild();
+        // getActivityType() looks at the top child, so we need to read the type before adding
+        // a new child in case the new child is on top and UNDEFINED.
+        final int activityType = task != null ? task.getActivityType() : ACTIVITY_TYPE_UNDEFINED;
+
+        super.addChild(child, index);
+
+        if (isAddingActivity && task != null) {
+            child.asActivityRecord().inHistory = true;
+            task.onDescendantActivityAdded(taskHadChild, activityType, child.asActivityRecord());
+        }
+    }
+
+    @Override
+    void onChildPositionChanged(WindowContainer child) {
+        super.onChildPositionChanged(child);
+
+        sendTaskFragmentInfoChanged();
+    }
+
+    void executeAppTransition(ActivityOptions options) {
+        // No app transition applied to the task fragment.
+    }
+
+    @Override
+    RemoteAnimationTarget createRemoteAnimationTarget(
+            RemoteAnimationController.RemoteAnimationRecord record) {
+        final ActivityRecord activity = record.getMode() == RemoteAnimationTarget.MODE_OPENING
+                // There may be a trampoline activity without window on top of the existing task
+                // which is moving to front. Exclude the finishing activity so the window of next
+                // activity can be chosen to create the animation target.
+                ? getTopNonFinishingActivity()
+                : getTopMostActivity();
+        return activity != null ? activity.createRemoteAnimationTarget(record) : null;
+    }
+
+    @Override
+    boolean canCreateRemoteAnimationTarget() {
+        return true;
+    }
+
+    boolean shouldSleepActivities() {
+        return false;
+    }
+
+    @Override
+    void resolveOverrideConfiguration(Configuration newParentConfig) {
+        mTmpBounds.set(getResolvedOverrideConfiguration().windowConfiguration.getBounds());
+        super.resolveOverrideConfiguration(newParentConfig);
+
+        int windowingMode =
+                getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
+        final int parentWindowingMode = newParentConfig.windowConfiguration.getWindowingMode();
+
+        // Resolve override windowing mode to fullscreen for home task (even on freeform
+        // display), or split-screen if in split-screen mode.
+        if (getActivityType() == ACTIVITY_TYPE_HOME && windowingMode == WINDOWING_MODE_UNDEFINED) {
+            windowingMode = WindowConfiguration.isSplitScreenWindowingMode(parentWindowingMode)
+                    ? parentWindowingMode : WINDOWING_MODE_FULLSCREEN;
+            getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode);
+        }
+
+        // Do not allow tasks not support multi window to be in a multi-window mode, unless it is in
+        // pinned windowing mode.
+        if (!supportsMultiWindow()) {
+            final int candidateWindowingMode =
+                    windowingMode != WINDOWING_MODE_UNDEFINED ? windowingMode : parentWindowingMode;
+            if (WindowConfiguration.inMultiWindowMode(candidateWindowingMode)
+                    && candidateWindowingMode != WINDOWING_MODE_PINNED) {
+                getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(
+                        WINDOWING_MODE_FULLSCREEN);
+            }
+        }
+
+        final Task thisTask = asTask();
+        // Embedded Task's configuration should go with parent TaskFragment, so we don't re-compute
+        // configuration here.
+        if (thisTask != null && !thisTask.isEmbedded()) {
+            thisTask.resolveLeafTaskOnlyOverrideConfigs(newParentConfig,
+                    mTmpBounds /* previousBounds */);
+        }
+        computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
+    }
+
+    boolean supportsMultiWindow() {
+        return supportsMultiWindowInDisplayArea(getDisplayArea());
+    }
+
+    /**
+     * @return whether this task supports multi-window if it is in the given
+     *         {@link TaskDisplayArea}.
+     */
+    boolean supportsMultiWindowInDisplayArea(@Nullable TaskDisplayArea tda) {
+        if (!mAtmService.mSupportsMultiWindow) {
+            return false;
+        }
+        final Task task = getTask();
+        if (task == null) {
+            return false;
+        }
+        if (tda == null) {
+            Slog.w(TAG, "Can't find TaskDisplayArea to determine support for multi"
+                    + " window. Task id=" + getTaskId() + " attached=" + isAttached());
+            return false;
+        }
+        if (!getTask().isResizeable() && !tda.supportsNonResizableMultiWindow()) {
+            // Not support non-resizable in multi window.
+            return false;
+        }
+
+        return tda.supportsActivityMinWidthHeightMultiWindow(mMinWidth, mMinHeight);
+    }
+
+    private int getTaskId() {
+        return getTask() != null ? getTask().mTaskId : INVALID_TASK_ID;
+    }
+
+    /**
+     * Ensures all visible activities at or below the input activity have the right configuration.
+     */
+    void ensureVisibleActivitiesConfiguration(ActivityRecord start, boolean preserveWindow) {
+        mEnsureVisibleActivitiesConfigHelper.process(start, preserveWindow);
+    }
+
+    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
+            @NonNull Configuration parentConfig) {
+        computeConfigResourceOverrides(inOutConfig, parentConfig, null /* overrideDisplayInfo */,
+                null /* compatInsets */);
+    }
+
+    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
+            @NonNull Configuration parentConfig, @Nullable DisplayInfo overrideDisplayInfo) {
+        if (overrideDisplayInfo != null) {
+            // Make sure the screen related configs can be computed by the provided display info.
+            inOutConfig.screenLayout = Configuration.SCREENLAYOUT_UNDEFINED;
+            invalidateAppBoundsConfig(inOutConfig);
+        }
+        computeConfigResourceOverrides(inOutConfig, parentConfig, overrideDisplayInfo,
+                null /* compatInsets */);
+    }
+
+    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
+            @NonNull Configuration parentConfig,
+            @Nullable ActivityRecord.CompatDisplayInsets compatInsets) {
+        if (compatInsets != null) {
+            // Make sure the app bounds can be computed by the compat insets.
+            invalidateAppBoundsConfig(inOutConfig);
+        }
+        computeConfigResourceOverrides(inOutConfig, parentConfig, null /* overrideDisplayInfo */,
+                compatInsets);
+    }
+
+    /**
+     * Forces the app bounds related configuration can be computed by
+     * {@link #computeConfigResourceOverrides(Configuration, Configuration, DisplayInfo,
+     * ActivityRecord.CompatDisplayInsets)}.
+     */
+    private static void invalidateAppBoundsConfig(@NonNull Configuration inOutConfig) {
+        final Rect appBounds = inOutConfig.windowConfiguration.getAppBounds();
+        if (appBounds != null) {
+            appBounds.setEmpty();
+        }
+        inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+        inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+    }
+
+    /**
+     * Calculates configuration values used by the client to get resources. This should be run
+     * using app-facing bounds (bounds unmodified by animations or transient interactions).
+     *
+     * This assumes bounds are non-empty/null. For the null-bounds case, the caller is likely
+     * configuring an "inherit-bounds" window which means that all configuration settings would
+     * just be inherited from the parent configuration.
+     **/
+    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
+            @NonNull Configuration parentConfig, @Nullable DisplayInfo overrideDisplayInfo,
+            @Nullable ActivityRecord.CompatDisplayInsets compatInsets) {
+        int windowingMode = inOutConfig.windowConfiguration.getWindowingMode();
+        if (windowingMode == WINDOWING_MODE_UNDEFINED) {
+            windowingMode = parentConfig.windowConfiguration.getWindowingMode();
+        }
+
+        float density = inOutConfig.densityDpi;
+        if (density == Configuration.DENSITY_DPI_UNDEFINED) {
+            density = parentConfig.densityDpi;
+        }
+        density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
+
+        // The bounds may have been overridden at this level. If the parent cannot cover these
+        // bounds, the configuration is still computed according to the override bounds.
+        final boolean insideParentBounds;
+
+        final Rect parentBounds = parentConfig.windowConfiguration.getBounds();
+        final Rect resolvedBounds = inOutConfig.windowConfiguration.getBounds();
+        if (resolvedBounds == null || resolvedBounds.isEmpty()) {
+            mTmpFullBounds.set(parentBounds);
+            insideParentBounds = true;
+        } else {
+            mTmpFullBounds.set(resolvedBounds);
+            insideParentBounds = parentBounds.contains(resolvedBounds);
+        }
+
+        // Non-null compatibility insets means the activity prefers to keep its original size, so
+        // out bounds doesn't need to be restricted by the parent or current display
+        final boolean customContainerPolicy = compatInsets != null;
+
+        Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+        if (outAppBounds == null || outAppBounds.isEmpty()) {
+            // App-bounds hasn't been overridden, so calculate a value for it.
+            inOutConfig.windowConfiguration.setAppBounds(mTmpFullBounds);
+            outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+
+            if (!customContainerPolicy && windowingMode != WINDOWING_MODE_FREEFORM) {
+                final Rect containingAppBounds;
+                if (insideParentBounds) {
+                    containingAppBounds = parentConfig.windowConfiguration.getAppBounds();
+                } else {
+                    // Restrict appBounds to display non-decor rather than parent because the
+                    // override bounds are beyond the parent. Otherwise, it won't match the
+                    // overridden bounds.
+                    final TaskDisplayArea displayArea = getDisplayArea();
+                    containingAppBounds = displayArea != null
+                            ? displayArea.getWindowConfiguration().getAppBounds() : null;
+                }
+                if (containingAppBounds != null && !containingAppBounds.isEmpty()) {
+                    outAppBounds.intersect(containingAppBounds);
+                }
+            }
+        }
+
+        if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED
+                || inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
+            if (!customContainerPolicy && WindowConfiguration.isFloating(windowingMode)) {
+                mTmpNonDecorBounds.set(mTmpFullBounds);
+                mTmpStableBounds.set(mTmpFullBounds);
+            } else if (!customContainerPolicy
+                    && (overrideDisplayInfo != null || getDisplayContent() != null)) {
+                final DisplayInfo di = overrideDisplayInfo != null
+                        ? overrideDisplayInfo
+                        : getDisplayContent().getDisplayInfo();
+
+                // For calculating screenWidthDp, screenWidthDp, we use the stable inset screen
+                // area, i.e. the screen area without the system bars.
+                // The non decor inset are areas that could never be removed in Honeycomb. See
+                // {@link WindowManagerPolicy#getNonDecorInsetsLw}.
+                calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, mTmpFullBounds, di);
+            } else {
+                // Apply the given non-decor and stable insets to calculate the corresponding bounds
+                // for screen size of configuration.
+                int rotation = inOutConfig.windowConfiguration.getRotation();
+                if (rotation == ROTATION_UNDEFINED) {
+                    rotation = parentConfig.windowConfiguration.getRotation();
+                }
+                if (rotation != ROTATION_UNDEFINED && customContainerPolicy) {
+                    mTmpNonDecorBounds.set(mTmpFullBounds);
+                    mTmpStableBounds.set(mTmpFullBounds);
+                    compatInsets.getBoundsByRotation(mTmpBounds, rotation);
+                    intersectWithInsetsIfFits(mTmpNonDecorBounds, mTmpBounds,
+                            compatInsets.mNonDecorInsets[rotation]);
+                    intersectWithInsetsIfFits(mTmpStableBounds, mTmpBounds,
+                            compatInsets.mStableInsets[rotation]);
+                    outAppBounds.set(mTmpNonDecorBounds);
+                } else {
+                    // Set to app bounds because it excludes decor insets.
+                    mTmpNonDecorBounds.set(outAppBounds);
+                    mTmpStableBounds.set(outAppBounds);
+                }
+            }
+
+            if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
+                final int overrideScreenWidthDp = (int) (mTmpStableBounds.width() / density);
+                inOutConfig.screenWidthDp = (insideParentBounds && !customContainerPolicy)
+                        ? Math.min(overrideScreenWidthDp, parentConfig.screenWidthDp)
+                        : overrideScreenWidthDp;
+            }
+            if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
+                final int overrideScreenHeightDp = (int) (mTmpStableBounds.height() / density);
+                inOutConfig.screenHeightDp = (insideParentBounds && !customContainerPolicy)
+                        ? Math.min(overrideScreenHeightDp, parentConfig.screenHeightDp)
+                        : overrideScreenHeightDp;
+            }
+
+            if (inOutConfig.smallestScreenWidthDp
+                    == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
+                if (WindowConfiguration.isFloating(windowingMode)) {
+                    // For floating tasks, calculate the smallest width from the bounds of the task
+                    inOutConfig.smallestScreenWidthDp = (int) (
+                            Math.min(mTmpFullBounds.width(), mTmpFullBounds.height()) / density);
+                }
+                // otherwise, it will just inherit
+            }
+        }
+
+        if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
+            inOutConfig.orientation = (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
+                    ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+        }
+        if (inOutConfig.screenLayout == Configuration.SCREENLAYOUT_UNDEFINED) {
+            // For calculating screen layout, we need to use the non-decor inset screen area for the
+            // calculation for compatibility reasons, i.e. screen area without system bars that
+            // could never go away in Honeycomb.
+            int compatScreenWidthDp = (int) (mTmpNonDecorBounds.width() / density);
+            int compatScreenHeightDp = (int) (mTmpNonDecorBounds.height() / density);
+            // Use overrides if provided. If both overrides are provided, mTmpNonDecorBounds is
+            // undefined so it can't be used.
+            if (inOutConfig.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
+                compatScreenWidthDp = inOutConfig.screenWidthDp;
+            }
+            if (inOutConfig.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
+                compatScreenHeightDp = inOutConfig.screenHeightDp;
+            }
+            // Reducing the screen layout starting from its parent config.
+            inOutConfig.screenLayout = computeScreenLayoutOverride(parentConfig.screenLayout,
+                    compatScreenWidthDp, compatScreenHeightDp);
+        }
+    }
+
+    /**
+     * Gets bounds with non-decor and stable insets applied respectively.
+     *
+     * If bounds overhangs the display, those edges will not get insets. See
+     * {@link #intersectWithInsetsIfFits}
+     *
+     * @param outNonDecorBounds where to place bounds with non-decor insets applied.
+     * @param outStableBounds where to place bounds with stable insets applied.
+     * @param bounds the bounds to inset.
+     */
+    void calculateInsetFrames(Rect outNonDecorBounds, Rect outStableBounds, Rect bounds,
+            DisplayInfo displayInfo) {
+        outNonDecorBounds.set(bounds);
+        outStableBounds.set(bounds);
+        final Task rootTask = getRootTaskFragment().asTask();
+        if (rootTask == null || rootTask.mDisplayContent == null) {
+            return;
+        }
+        mTmpBounds.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight);
+
+        final DisplayPolicy policy = rootTask.mDisplayContent.getDisplayPolicy();
+        policy.getNonDecorInsetsLw(displayInfo.rotation, displayInfo.logicalWidth,
+                displayInfo.logicalHeight, displayInfo.displayCutout, mTmpInsets);
+        intersectWithInsetsIfFits(outNonDecorBounds, mTmpBounds, mTmpInsets);
+
+        policy.convertNonDecorInsetsToStableInsets(mTmpInsets, displayInfo.rotation);
+        intersectWithInsetsIfFits(outStableBounds, mTmpBounds, mTmpInsets);
+    }
+
+    /**
+     * Intersects inOutBounds with intersectBounds-intersectInsets. If inOutBounds is larger than
+     * intersectBounds on a side, then the respective side will not be intersected.
+     *
+     * The assumption is that if inOutBounds is initially larger than intersectBounds, then the
+     * inset on that side is no-longer applicable. This scenario happens when a task's minimal
+     * bounds are larger than the provided parent/display bounds.
+     *
+     * @param inOutBounds the bounds to intersect.
+     * @param intersectBounds the bounds to intersect with.
+     * @param intersectInsets insets to apply to intersectBounds before intersecting.
+     */
+    static void intersectWithInsetsIfFits(
+            Rect inOutBounds, Rect intersectBounds, Rect intersectInsets) {
+        if (inOutBounds.right <= intersectBounds.right) {
+            inOutBounds.right =
+                    Math.min(intersectBounds.right - intersectInsets.right, inOutBounds.right);
+        }
+        if (inOutBounds.bottom <= intersectBounds.bottom) {
+            inOutBounds.bottom =
+                    Math.min(intersectBounds.bottom - intersectInsets.bottom, inOutBounds.bottom);
+        }
+        if (inOutBounds.left >= intersectBounds.left) {
+            inOutBounds.left =
+                    Math.max(intersectBounds.left + intersectInsets.left, inOutBounds.left);
+        }
+        if (inOutBounds.top >= intersectBounds.top) {
+            inOutBounds.top =
+                    Math.max(intersectBounds.top + intersectInsets.top, inOutBounds.top);
+        }
+    }
+
+    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
+    static int computeScreenLayoutOverride(int sourceScreenLayout, int screenWidthDp,
+            int screenHeightDp) {
+        sourceScreenLayout = sourceScreenLayout
+                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
+        final int longSize = Math.max(screenWidthDp, screenHeightDp);
+        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
+        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
+    }
+
+    @Override
+    public int getActivityType() {
+        final int applicationType = super.getActivityType();
+        if (applicationType != ACTIVITY_TYPE_UNDEFINED || !hasChild()) {
+            return applicationType;
+        }
+        return getTopChild().getActivityType();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newParentConfig) {
+        // Task will animate differently.
+        if (mTaskFragmentOrganizer != null) {
+            mTmpPrevBounds.set(getBounds());
+        }
+
+        super.onConfigurationChanged(newParentConfig);
+
+        if (shouldStartChangeTransition(mTmpPrevBounds)) {
+            initializeChangeTransition(mTmpPrevBounds);
+        } else if (mTaskFragmentOrganizer != null) {
+            // Update the surface here instead of in the organizer so that we can make sure
+            // it can be synced with the surface freezer.
+            final SurfaceControl.Transaction t = getSyncTransaction();
+            updateSurfacePosition(t);
+            updateOrganizedTaskFragmentSurfaceSize(t, false /* forceUpdate */);
+        }
+
+        sendTaskFragmentInfoChanged();
+    }
+
+    /** Updates the surface size so that the sub windows cannot be shown out of bounds. */
+    private void updateOrganizedTaskFragmentSurfaceSize(SurfaceControl.Transaction t,
+            boolean forceUpdate) {
+        if (mTaskFragmentOrganizer == null) {
+            // We only want to update for organized TaskFragment. Task will handle itself.
+            return;
+        }
+        if (mSurfaceControl == null || mSurfaceAnimator.hasLeash() || mSurfaceFreezer.hasLeash()) {
+            return;
+        }
+
+        final Rect bounds = getBounds();
+        final int width = bounds.width();
+        final int height = bounds.height();
+        if (!forceUpdate && width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) {
+            return;
+        }
+        t.setWindowCrop(mSurfaceControl, width, height);
+        mLastSurfaceSize.set(width, height);
+    }
+
+    @Override
+    public void onAnimationLeashCreated(SurfaceControl.Transaction t, SurfaceControl leash) {
+        super.onAnimationLeashCreated(t, leash);
+        // Reset surface bounds for animation. It will be taken care by the animation leash, and
+        // reset again onAnimationLeashLost.
+        if (mTaskFragmentOrganizer != null
+                && (mLastSurfaceSize.x != 0 || mLastSurfaceSize.y != 0)) {
+            t.setWindowCrop(mSurfaceControl, 0, 0);
+            mLastSurfaceSize.set(0, 0);
+        }
+    }
+
+    @Override
+    public void onAnimationLeashLost(SurfaceControl.Transaction t) {
+        super.onAnimationLeashLost(t);
+        // Update the surface bounds after animation.
+        if (mTaskFragmentOrganizer != null) {
+            updateOrganizedTaskFragmentSurfaceSize(t, true /* forceUpdate */);
+        }
+    }
+
+    /** Whether we should prepare a transition for this {@link TaskFragment} bounds change. */
+    private boolean shouldStartChangeTransition(Rect startBounds) {
+        if (mWmService.mDisableTransitionAnimation
+                || mDisplayContent == null
+                || mTaskFragmentOrganizer == null
+                || getSurfaceControl() == null
+                // The change transition will be covered by display.
+                || mDisplayContent.inTransition()
+                || !isVisible()) {
+            return false;
+        }
+
+        return !startBounds.equals(getBounds());
+    }
+
+    @Override
+    void setSurfaceControl(SurfaceControl sc) {
+        super.setSurfaceControl(sc);
+        if (mTaskFragmentOrganizer != null) {
+            final SurfaceControl.Transaction t = getSyncTransaction();
+            updateSurfacePosition(t);
+            updateOrganizedTaskFragmentSurfaceSize(t, false /* forceUpdate */);
+            // If the TaskFragmentOrganizer was set before we created the SurfaceControl, we need to
+            // emit the callbacks now.
+            sendTaskFragmentAppeared();
+        }
+    }
+
+    void sendTaskFragmentInfoChanged() {
+        if (mTaskFragmentOrganizer != null) {
+            mTaskFragmentOrganizerController
+                    .onTaskFragmentInfoChanged(mTaskFragmentOrganizer, this);
+        }
+    }
+
+    private void sendTaskFragmentAppeared() {
+        if (mTaskFragmentOrganizer != null) {
+            mTaskFragmentOrganizerController.onTaskFragmentAppeared(mTaskFragmentOrganizer, this);
+        }
+    }
+
+    private void sendTaskFragmentVanished() {
+        if (mTaskFragmentOrganizer != null) {
+            mTaskFragmentOrganizerController.onTaskFragmentVanished(mTaskFragmentOrganizer, this);
+        }
+    }
+
+    /**
+     * Returns a {@link TaskFragmentInfo} with information from this TaskFragment. Should not be
+     * called from {@link Task}.
+     */
+    TaskFragmentInfo getTaskFragmentInfo() {
+        List<IBinder> childActivities = new ArrayList<>();
+        for (int i = 0; i < getChildCount(); i++) {
+            WindowContainer wc = getChildAt(i);
+            if (mTaskFragmentOrganizerPid != ActivityRecord.INVALID_PID
+                    && wc.asActivityRecord() != null
+                    && wc.asActivityRecord().getPid() == mTaskFragmentOrganizerPid) {
+                // Only includes Activities that belong to the organizer process for security.
+                childActivities.add(wc.asActivityRecord().appToken);
+            }
+        }
+        final Point positionInParent = new Point();
+        getRelativePosition(positionInParent);
+        final int[] runningActivityCount = new int[1];
+        forAllActivities(a -> {
+            if (!a.finishing) {
+                runningActivityCount[0]++;
+            }
+        });
+        return new TaskFragmentInfo(
+                mFragmentToken,
+                mRemoteToken.toWindowContainerToken(),
+                getConfiguration(),
+                getChildCount() == 0,
+                runningActivityCount[0],
+                isVisible(),
+                childActivities,
+                positionInParent,
+                mClearedTaskForReuse);
+    }
+
+    @Nullable
+    IBinder getFragmentToken() {
+        return mFragmentToken;
+    }
+
+    @Nullable
+    ITaskFragmentOrganizer getTaskFragmentOrganizer() {
+        return mTaskFragmentOrganizer;
+    }
+
+    @Override
+    boolean isOrganized() {
+        return mTaskFragmentOrganizer != null;
+    }
+
+    /** Whether this is an organized {@link TaskFragment} and not a {@link Task}. */
+    final boolean isOrganizedTaskFragment() {
+        return mTaskFragmentOrganizer != null;
+    }
+
+    boolean isReadyToTransit() {
+        // We don't want to start the transition if the organized TaskFragment is empty, unless
+        // it is requested to be removed.
+        return !isOrganizedTaskFragment() || getTopNonFinishingActivity() != null
+                || mIsRemovalRequested;
+    }
+
+    /** Clear {@link #mLastPausedActivity} for all {@link TaskFragment} children */
+    void clearLastPausedActivity() {
+        forAllTaskFragments(taskFragment -> taskFragment.mLastPausedActivity = null);
+    }
+
+    /**
+     * Sets {@link #mMinWidth} and {@link #mMinWidth} to this TaskFragment.
+     * It is usually set from the parent {@link Task} when adding the TaskFragment to the window
+     * hierarchy.
+     */
+    void setMinDimensions(int minWidth, int minHeight) {
+        if (asTask() != null) {
+            throw new UnsupportedOperationException("This method must not be used to Task. The "
+                    + " minimum dimension of Task should be passed from Task constructor.");
+        }
+        mMinWidth = minWidth;
+        mMinHeight = minHeight;
+    }
+
+    @Override
+    void removeChild(WindowContainer child) {
+        removeChild(child, true /* removeSelfIfPossible */);
+    }
+
+    void removeChild(WindowContainer child, boolean removeSelfIfPossible) {
+        super.removeChild(child);
+        if (removeSelfIfPossible && (!mCreatedByOrganizer || mIsRemovalRequested) && !hasChild()) {
+            removeImmediately("removeLastChild " + child);
+        }
+    }
+
+    /**
+     * Requests to remove this task fragment. If it doesn't have children, it is removed
+     * immediately. Otherwise it will be removed until all activities are destroyed.
+     *
+     * @param withTransition Whether to use transition animation when removing activities. Set to
+     *                       {@code false} if this is invisible to user, e.g. display removal.
+     */
+    void remove(boolean withTransition, String reason) {
+        if (!hasChild()) {
+            removeImmediately(reason);
+            return;
+        }
+        mIsRemovalRequested = true;
+        forAllActivities(r -> {
+            if (withTransition) {
+                r.finishIfPossible(reason, false /* oomAdj */);
+            } else {
+                r.destroyIfPossible(reason);
+            }
+        });
+    }
+
+    void setDelayLastActivityRemoval(boolean delay) {
+        if (!mIsEmbedded) {
+            Slog.w(TAG, "Set delaying last activity removal on a non-embedded TF.");
+        }
+        mDelayLastActivityRemoval = delay;
+    }
+
+    boolean isDelayLastActivityRemoval() {
+        return mDelayLastActivityRemoval;
+    }
+
+    boolean shouldDeferRemoval() {
+        if (!hasChild()) {
+            return false;
+        }
+        return isAnimating(TRANSITION | CHILDREN, WindowState.EXIT_ANIMATING_TYPES)
+                || inTransition();
+    }
+
+    @Override
+    boolean handleCompleteDeferredRemoval() {
+        if (shouldDeferRemoval()) {
+            return true;
+        }
+        return super.handleCompleteDeferredRemoval();
+    }
+
+    /** The overridden method must call {@link #removeImmediately()} instead of super. */
+    void removeImmediately(String reason) {
+        Slog.d(TAG, "Remove task fragment: " + reason);
+        removeImmediately();
+    }
+
+    @Override
+    void removeImmediately() {
+        mIsRemovalRequested = false;
+        resetAdjacentTaskFragment();
+        super.removeImmediately();
+        sendTaskFragmentVanished();
+    }
+
+    @Override
+    boolean canBeAnimationTarget() {
+        return true;
+    }
+
+    boolean dump(String prefix, FileDescriptor fd, PrintWriter pw, boolean dumpAll,
+            boolean dumpClient, String dumpPackage, final boolean needSep, Runnable header) {
+        boolean printed = false;
+        Runnable headerPrinter = () -> {
+            if (needSep) {
+                pw.println();
+            }
+            if (header != null) {
+                header.run();
+            }
+
+            dumpInner(prefix, pw, dumpAll, dumpPackage);
+        };
+
+        if (dumpPackage == null) {
+            // If we are not filtering by package, we want to print absolutely everything,
+            // so always print the header even if there are no tasks/activities inside.
+            headerPrinter.run();
+            headerPrinter = null;
+            printed = true;
+        }
+
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            WindowContainer child = mChildren.get(i);
+            if (child.asTaskFragment() != null) {
+                printed |= child.asTaskFragment().dump(prefix + "  ", fd, pw, dumpAll,
+                        dumpClient, dumpPackage, needSep, headerPrinter);
+            } else if (child.asActivityRecord() != null) {
+                ActivityRecord.dumpActivity(fd, pw, i, child.asActivityRecord(), prefix + "  ",
+                        "Hist ", true, !dumpAll, dumpClient, dumpPackage, false, headerPrinter,
+                        getTask());
+            }
+        }
+
+        return printed;
+    }
+
+    void dumpInner(String prefix, PrintWriter pw, boolean dumpAll, String dumpPackage) {
+        pw.print(prefix); pw.print("* "); pw.println(this);
+        final Rect bounds = getRequestedOverrideBounds();
+        if (!bounds.isEmpty()) {
+            pw.println(prefix + "  mBounds=" + bounds);
+        }
+        if (mIsRemovalRequested) {
+            pw.println(prefix + "  mIsRemovalRequested=true");
+        }
+        if (dumpAll) {
+            printThisActivity(pw, mLastPausedActivity, dumpPackage, false,
+                    prefix + "  mLastPausedActivity: ", null);
+        }
+    }
+
+    @Override
+    void dump(PrintWriter pw, String prefix, boolean dumpAll) {
+        super.dump(pw, prefix, dumpAll);
+        pw.println(prefix + "bounds=" + getBounds().toShortString());
+        final String doublePrefix = prefix + "  ";
+        for (int i = mChildren.size() - 1; i >= 0; i--) {
+            final WindowContainer<?> child = mChildren.get(i);
+            pw.println(prefix + "* " + child);
+            // Only dump non-activity because full activity info is already printed by
+            // RootWindowContainer#dumpActivities.
+            if (child.asActivityRecord() == null) {
+                child.dump(pw, doublePrefix, dumpAll);
+            }
+        }
+    }
+
+    @Override
+    void writeIdentifierToProto(ProtoOutputStream proto, long fieldId) {
+        final long token = proto.start(fieldId);
+        proto.write(HASH_CODE, System.identityHashCode(this));
+        final ActivityRecord topActivity = topRunningActivity();
+        proto.write(USER_ID, topActivity != null ? topActivity.mUserId : USER_NULL);
+        proto.write(TITLE, topActivity != null ? topActivity.intent.getComponent()
+                .flattenToShortString() : "TaskFragment");
+        proto.end(token);
+    }
+
+    @Override
+    long getProtoFieldId() {
+        return TASK_FRAGMENT;
+    }
+
+    @Override
+    public void dumpDebug(ProtoOutputStream proto, long fieldId,
+            @WindowTraceLogLevel int logLevel) {
+        if (logLevel == WindowTraceLogLevel.CRITICAL && !isVisible()) {
+            return;
+        }
+
+        final long token = proto.start(fieldId);
+
+        super.dumpDebug(proto, WINDOW_CONTAINER, logLevel);
+
+        proto.write(DISPLAY_ID, getDisplayId());
+        proto.write(ACTIVITY_TYPE, getActivityType());
+        proto.write(MIN_WIDTH, mMinWidth);
+        proto.write(MIN_HEIGHT, mMinHeight);
+
+        proto.end(token);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
new file mode 100644
index 0000000..d911656
--- /dev/null
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.window.TaskFragmentOrganizer.putExceptionInBundle;
+
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
+import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.res.Configuration;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.RemoteAnimationDefinition;
+import android.view.SurfaceControl;
+import android.window.ITaskFragmentOrganizer;
+import android.window.ITaskFragmentOrganizerController;
+import android.window.TaskFragmentAppearedInfo;
+import android.window.TaskFragmentInfo;
+
+import com.android.internal.protolog.common.ProtoLog;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Stores and manages the client {@link android.window.TaskFragmentOrganizer}.
+ */
+public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerController.Stub {
+    private static final String TAG = "TaskFragmentOrganizerController";
+
+    private final ActivityTaskManagerService mAtmService;
+    private final WindowManagerGlobalLock mGlobalLock;
+    /**
+     * A Map which manages the relationship between
+     * {@link ITaskFragmentOrganizer} and {@link TaskFragmentOrganizerState}
+     */
+    private final ArrayMap<IBinder, TaskFragmentOrganizerState> mTaskFragmentOrganizerState =
+            new ArrayMap<>();
+    /**
+     * A List which manages the TaskFragment pending event {@link PendingTaskFragmentEvent}
+     */
+    private final ArrayList<PendingTaskFragmentEvent> mPendingTaskFragmentEvents =
+            new ArrayList<>();
+
+    TaskFragmentOrganizerController(ActivityTaskManagerService atm) {
+        mAtmService = atm;
+        mGlobalLock = atm.mGlobalLock;
+    }
+
+    /**
+     * A class to manage {@link ITaskFragmentOrganizer} and its organized
+     * {@link TaskFragment TaskFragments}.
+     */
+    private class TaskFragmentOrganizerState implements IBinder.DeathRecipient {
+        private final ArrayList<TaskFragment> mOrganizedTaskFragments = new ArrayList<>();
+        private final ITaskFragmentOrganizer mOrganizer;
+        private final Map<TaskFragment, TaskFragmentInfo> mLastSentTaskFragmentInfos =
+                new WeakHashMap<>();
+        private final Map<TaskFragment, Configuration> mLastSentTaskFragmentParentConfigs =
+                new WeakHashMap<>();
+
+        /**
+         * @see android.window.TaskFragmentOrganizer#registerRemoteAnimations(
+         * RemoteAnimationDefinition)
+         */
+        @Nullable
+        private RemoteAnimationDefinition mRemoteAnimationDefinition;
+
+        TaskFragmentOrganizerState(ITaskFragmentOrganizer organizer) {
+            mOrganizer = organizer;
+            try {
+                mOrganizer.asBinder().linkToDeath(this, 0 /*flags*/);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "TaskFragmentOrganizer failed to register death recipient");
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mGlobalLock) {
+                removeOrganizer(mOrganizer);
+            }
+        }
+
+        /**
+         * @return {@code true} if taskFragment is organized and not sent the appeared event before.
+         */
+        boolean addTaskFragment(TaskFragment taskFragment) {
+            if (taskFragment.mTaskFragmentAppearedSent) {
+                return false;
+            }
+            if (mOrganizedTaskFragments.contains(taskFragment)) {
+                return false;
+            }
+            mOrganizedTaskFragments.add(taskFragment);
+            return true;
+        }
+
+        void removeTaskFragment(TaskFragment taskFragment) {
+            mOrganizedTaskFragments.remove(taskFragment);
+        }
+
+        void dispose() {
+            while (!mOrganizedTaskFragments.isEmpty()) {
+                final TaskFragment taskFragment = mOrganizedTaskFragments.get(0);
+                taskFragment.removeImmediately();
+                mOrganizedTaskFragments.remove(taskFragment);
+            }
+            mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
+        }
+
+        void onTaskFragmentAppeared(ITaskFragmentOrganizer organizer, TaskFragment tf) {
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment appeared name=%s", tf.getName());
+            final TaskFragmentInfo info = tf.getTaskFragmentInfo();
+            final SurfaceControl outSurfaceControl = new SurfaceControl(tf.getSurfaceControl(),
+                    "TaskFragmentOrganizerController.onTaskFragmentInfoAppeared");
+            try {
+                organizer.onTaskFragmentAppeared(
+                        new TaskFragmentAppearedInfo(info, outSurfaceControl));
+                mLastSentTaskFragmentInfos.put(tf, info);
+                tf.mTaskFragmentAppearedSent = true;
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Exception sending onTaskFragmentAppeared callback", e);
+            }
+            onTaskFragmentParentInfoChanged(organizer, tf);
+        }
+
+        void onTaskFragmentVanished(ITaskFragmentOrganizer organizer, TaskFragment tf) {
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment vanished name=%s", tf.getName());
+            try {
+                organizer.onTaskFragmentVanished(tf.getTaskFragmentInfo());
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Exception sending onTaskFragmentVanished callback", e);
+            }
+            tf.mTaskFragmentAppearedSent = false;
+            mLastSentTaskFragmentInfos.remove(tf);
+            mLastSentTaskFragmentParentConfigs.remove(tf);
+        }
+
+        void onTaskFragmentInfoChanged(ITaskFragmentOrganizer organizer, TaskFragment tf) {
+            // Parent config may have changed. The controller will check if there is any important
+            // config change for the organizer.
+            onTaskFragmentParentInfoChanged(organizer, tf);
+
+            // Check if the info is different from the last reported info.
+            final TaskFragmentInfo info = tf.getTaskFragmentInfo();
+            final TaskFragmentInfo lastInfo = mLastSentTaskFragmentInfos.get(tf);
+            if (info.equalsForTaskFragmentOrganizer(lastInfo) && configurationsAreEqualForOrganizer(
+                    info.getConfiguration(), lastInfo.getConfiguration())) {
+                return;
+            }
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment info changed name=%s",
+                    tf.getName());
+            try {
+                organizer.onTaskFragmentInfoChanged(tf.getTaskFragmentInfo());
+                mLastSentTaskFragmentInfos.put(tf, info);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Exception sending onTaskFragmentInfoChanged callback", e);
+            }
+        }
+
+        void onTaskFragmentParentInfoChanged(ITaskFragmentOrganizer organizer, TaskFragment tf) {
+            // Check if the parent info is different from the last reported parent info.
+            if (tf.getParent() == null || tf.getParent().asTask() == null) {
+                mLastSentTaskFragmentParentConfigs.remove(tf);
+                return;
+            }
+            final Task parent = tf.getParent().asTask();
+            final Configuration parentConfig = parent.getConfiguration();
+            final Configuration lastParentConfig = mLastSentTaskFragmentParentConfigs.get(tf);
+            if (configurationsAreEqualForOrganizer(parentConfig, lastParentConfig)) {
+                return;
+            }
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
+                    "TaskFragment parent info changed name=%s parentTaskId=%d",
+                    tf.getName(), parent.mTaskId);
+            try {
+                organizer.onTaskFragmentParentInfoChanged(tf.getFragmentToken(), parentConfig);
+                mLastSentTaskFragmentParentConfigs.put(tf, new Configuration(parentConfig));
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Exception sending onTaskFragmentParentInfoChanged callback", e);
+            }
+        }
+
+        void onTaskFragmentError(ITaskFragmentOrganizer organizer, IBinder errorCallbackToken,
+                Throwable exception) {
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
+                    "Sending TaskFragment error exception=%s", exception.toString());
+            final Bundle exceptionBundle = putExceptionInBundle(exception);
+            try {
+                organizer.onTaskFragmentError(errorCallbackToken, exceptionBundle);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Exception sending onTaskFragmentError callback", e);
+            }
+        }
+    }
+
+    @Override
+    public void registerOrganizer(ITaskFragmentOrganizer organizer) {
+        final int pid = Binder.getCallingPid();
+        final long uid = Binder.getCallingUid();
+        synchronized (mGlobalLock) {
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
+                    "Register task fragment organizer=%s uid=%d pid=%d",
+                    organizer.asBinder(), uid, pid);
+            if (mTaskFragmentOrganizerState.containsKey(organizer.asBinder())) {
+                throw new IllegalStateException(
+                        "Replacing existing organizer currently unsupported");
+            }
+            mTaskFragmentOrganizerState.put(organizer.asBinder(),
+                    new TaskFragmentOrganizerState(organizer));
+        }
+    }
+
+    @Override
+    public void unregisterOrganizer(ITaskFragmentOrganizer organizer) {
+        validateAndGetState(organizer);
+        final int pid = Binder.getCallingPid();
+        final long uid = Binder.getCallingUid();
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
+                        "Unregister task fragment organizer=%s uid=%d pid=%d",
+                        organizer.asBinder(), uid, pid);
+                removeOrganizer(organizer);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    @Override
+    public void registerRemoteAnimations(ITaskFragmentOrganizer organizer,
+            RemoteAnimationDefinition definition) {
+        final int pid = Binder.getCallingPid();
+        final int uid = Binder.getCallingUid();
+        synchronized (mGlobalLock) {
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
+                    "Register remote animations for organizer=%s uid=%d pid=%d",
+                    organizer.asBinder(), uid, pid);
+            final TaskFragmentOrganizerState organizerState =
+                    mTaskFragmentOrganizerState.get(organizer.asBinder());
+            if (organizerState == null) {
+                throw new IllegalStateException("The organizer hasn't been registered.");
+            }
+            if (organizerState.mRemoteAnimationDefinition != null) {
+                throw new IllegalStateException(
+                        "The organizer has already registered remote animations="
+                                + organizerState.mRemoteAnimationDefinition);
+            }
+
+            definition.setCallingPidUid(pid, uid);
+            organizerState.mRemoteAnimationDefinition = definition;
+        }
+    }
+
+    @Override
+    public void unregisterRemoteAnimations(ITaskFragmentOrganizer organizer) {
+        final int pid = Binder.getCallingPid();
+        final long uid = Binder.getCallingUid();
+        synchronized (mGlobalLock) {
+            ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
+                    "Unregister remote animations for organizer=%s uid=%d pid=%d",
+                    organizer.asBinder(), uid, pid);
+            final TaskFragmentOrganizerState organizerState =
+                    mTaskFragmentOrganizerState.get(organizer.asBinder());
+            if (organizerState == null) {
+                Slog.e(TAG, "The organizer hasn't been registered.");
+                return;
+            }
+
+            organizerState.mRemoteAnimationDefinition = null;
+        }
+    }
+
+    /** Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. */
+    @Nullable
+    public RemoteAnimationDefinition getRemoteAnimationDefinition(
+            ITaskFragmentOrganizer organizer) {
+        synchronized (mGlobalLock) {
+            final TaskFragmentOrganizerState organizerState =
+                    mTaskFragmentOrganizerState.get(organizer.asBinder());
+            return organizerState != null ? organizerState.mRemoteAnimationDefinition : null;
+        }
+    }
+
+    void onTaskFragmentAppeared(ITaskFragmentOrganizer organizer, TaskFragment taskFragment) {
+        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+        if (!state.addTaskFragment(taskFragment)) {
+            return;
+        }
+        PendingTaskFragmentEvent pendingEvent = getPendingTaskFragmentEvent(taskFragment,
+                PendingTaskFragmentEvent.EVENT_APPEARED);
+        if (pendingEvent == null) {
+            pendingEvent = new PendingTaskFragmentEvent(taskFragment, organizer,
+                    PendingTaskFragmentEvent.EVENT_APPEARED);
+            mPendingTaskFragmentEvents.add(pendingEvent);
+        }
+    }
+
+    void onTaskFragmentInfoChanged(ITaskFragmentOrganizer organizer, TaskFragment taskFragment) {
+        handleTaskFragmentInfoChanged(organizer, taskFragment,
+                PendingTaskFragmentEvent.EVENT_INFO_CHANGED);
+    }
+
+    void onTaskFragmentParentInfoChanged(ITaskFragmentOrganizer organizer,
+            TaskFragment taskFragment) {
+        handleTaskFragmentInfoChanged(organizer, taskFragment,
+                PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED);
+    }
+
+    private void handleTaskFragmentInfoChanged(ITaskFragmentOrganizer organizer,
+            TaskFragment taskFragment, int eventType) {
+        validateAndGetState(organizer);
+        if (!taskFragment.mTaskFragmentAppearedSent) {
+            // Skip if TaskFragment still not appeared.
+            return;
+        }
+        PendingTaskFragmentEvent pendingEvent = getLastPendingLifecycleEvent(taskFragment);
+        if (pendingEvent == null) {
+            pendingEvent = new PendingTaskFragmentEvent(taskFragment, organizer, eventType);
+        } else {
+            if (pendingEvent.mEventType == PendingTaskFragmentEvent.EVENT_VANISHED) {
+                // Skipped the info changed event if vanished event is pending.
+                return;
+            }
+            // Remove and add for re-ordering.
+            mPendingTaskFragmentEvents.remove(pendingEvent);
+        }
+        mPendingTaskFragmentEvents.add(pendingEvent);
+    }
+
+    void onTaskFragmentVanished(ITaskFragmentOrganizer organizer, TaskFragment taskFragment) {
+        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+        for (int i = mPendingTaskFragmentEvents.size() - 1; i >= 0; i--) {
+            PendingTaskFragmentEvent entry = mPendingTaskFragmentEvents.get(i);
+            if (taskFragment == entry.mTaskFragment) {
+                mPendingTaskFragmentEvents.remove(i);
+                if (entry.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED) {
+                    // If taskFragment appeared callback is pending, ignore the vanished request.
+                    return;
+                }
+            }
+        }
+        if (!taskFragment.mTaskFragmentAppearedSent) {
+            return;
+        }
+        PendingTaskFragmentEvent pendingEvent = new PendingTaskFragmentEvent(taskFragment,
+                organizer, PendingTaskFragmentEvent.EVENT_VANISHED);
+        mPendingTaskFragmentEvents.add(pendingEvent);
+        state.removeTaskFragment(taskFragment);
+    }
+
+    void onTaskFragmentError(ITaskFragmentOrganizer organizer, IBinder errorCallbackToken,
+            Throwable exception) {
+        validateAndGetState(organizer);
+        Slog.w(TAG, "onTaskFragmentError ", exception);
+        PendingTaskFragmentEvent pendingEvent = new PendingTaskFragmentEvent(organizer,
+                errorCallbackToken, exception, PendingTaskFragmentEvent.EVENT_ERROR);
+        mPendingTaskFragmentEvents.add(pendingEvent);
+    }
+
+    private void removeOrganizer(ITaskFragmentOrganizer organizer) {
+        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+        // remove all of the children of the organized TaskFragment
+        state.dispose();
+        mTaskFragmentOrganizerState.remove(organizer.asBinder());
+    }
+
+    /**
+     * Makes sure that the organizer has been correctly registered to prevent any Sidecar
+     * implementation from organizing {@link TaskFragment} without registering first. In such case,
+     * we wouldn't register {@link DeathRecipient} for the organizer, and might not remove the
+     * {@link TaskFragment} after the organizer process died.
+     */
+    private TaskFragmentOrganizerState validateAndGetState(ITaskFragmentOrganizer organizer) {
+        final TaskFragmentOrganizerState state =
+                mTaskFragmentOrganizerState.get(organizer.asBinder());
+        if (state == null) {
+            throw new IllegalArgumentException(
+                    "TaskFragmentOrganizer has not been registered. Organizer=" + organizer);
+        }
+        return state;
+    }
+
+    /**
+     * A class to store {@link ITaskFragmentOrganizer} and its organized
+     * {@link TaskFragment TaskFragments} with different pending event request.
+     */
+    private static class PendingTaskFragmentEvent {
+        static final int EVENT_APPEARED = 0;
+        static final int EVENT_VANISHED = 1;
+        static final int EVENT_INFO_CHANGED = 2;
+        static final int EVENT_PARENT_INFO_CHANGED = 3;
+        static final int EVENT_ERROR = 4;
+
+        @IntDef(prefix = "EVENT_", value = {
+                EVENT_APPEARED,
+                EVENT_VANISHED,
+                EVENT_INFO_CHANGED,
+                EVENT_PARENT_INFO_CHANGED,
+                EVENT_ERROR
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface EventType {}
+
+        @EventType
+        private final int mEventType;
+        private final ITaskFragmentOrganizer mTaskFragmentOrg;
+        private final TaskFragment mTaskFragment;
+        private final IBinder mErrorCallback;
+        private final Throwable mException;
+
+        private PendingTaskFragmentEvent(TaskFragment taskFragment,
+                ITaskFragmentOrganizer taskFragmentOrg, @EventType int eventType) {
+            this(taskFragment, taskFragmentOrg, null /* errorCallback */,
+                    null /* exception */, eventType);
+
+        }
+
+        private PendingTaskFragmentEvent(ITaskFragmentOrganizer taskFragmentOrg,
+                IBinder errorCallback, Throwable exception, @EventType int eventType) {
+            this(null /* taskFragment */, taskFragmentOrg, errorCallback, exception,
+                    eventType);
+        }
+
+        private PendingTaskFragmentEvent(TaskFragment taskFragment,
+                ITaskFragmentOrganizer taskFragmentOrg, IBinder errorCallback, Throwable exception,
+                @EventType int eventType) {
+            mTaskFragment = taskFragment;
+            mTaskFragmentOrg = taskFragmentOrg;
+            mErrorCallback = errorCallback;
+            mException = exception;
+            mEventType = eventType;
+        }
+
+        /**
+         * @return {@code true} if the pending event is related with taskFragment created, vanished
+         * and information changed.
+         */
+        boolean isLifecycleEvent() {
+            switch (mEventType) {
+                case EVENT_APPEARED:
+                case EVENT_VANISHED:
+                case EVENT_INFO_CHANGED:
+                case EVENT_PARENT_INFO_CHANGED:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+    }
+
+    @Nullable
+    private PendingTaskFragmentEvent getLastPendingLifecycleEvent(TaskFragment tf) {
+        for (int i = mPendingTaskFragmentEvents.size() - 1; i >= 0; i--) {
+            PendingTaskFragmentEvent entry = mPendingTaskFragmentEvents.get(i);
+            if (tf == entry.mTaskFragment && entry.isLifecycleEvent()) {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    private PendingTaskFragmentEvent getPendingTaskFragmentEvent(TaskFragment taskFragment,
+            int type) {
+        for (int i = mPendingTaskFragmentEvents.size() - 1; i >= 0; i--) {
+            PendingTaskFragmentEvent entry = mPendingTaskFragmentEvents.get(i);
+            if (taskFragment == entry.mTaskFragment && type == entry.mEventType) {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    void dispatchPendingEvents() {
+        if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()
+                || mPendingTaskFragmentEvents.isEmpty()) {
+            return;
+        }
+        for (int i = 0, n = mPendingTaskFragmentEvents.size(); i < n; i++) {
+            PendingTaskFragmentEvent event = mPendingTaskFragmentEvents.get(i);
+            dispatchEvent(event);
+        }
+        mPendingTaskFragmentEvents.clear();
+    }
+
+    void dispatchPendingInfoChangedEvent(TaskFragment taskFragment) {
+        PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment,
+                PendingTaskFragmentEvent.EVENT_INFO_CHANGED);
+        if (event == null) {
+            return;
+        }
+
+        dispatchEvent(event);
+        mPendingTaskFragmentEvents.remove(event);
+    }
+
+    private void dispatchEvent(PendingTaskFragmentEvent event) {
+        final ITaskFragmentOrganizer taskFragmentOrg = event.mTaskFragmentOrg;
+        final TaskFragment taskFragment = event.mTaskFragment;
+        final TaskFragmentOrganizerState state =
+                mTaskFragmentOrganizerState.get(taskFragmentOrg.asBinder());
+        if (state == null) {
+            return;
+        }
+        switch (event.mEventType) {
+            case PendingTaskFragmentEvent.EVENT_APPEARED:
+                state.onTaskFragmentAppeared(taskFragmentOrg, taskFragment);
+                break;
+            case PendingTaskFragmentEvent.EVENT_VANISHED:
+                state.onTaskFragmentVanished(taskFragmentOrg, taskFragment);
+                break;
+            case PendingTaskFragmentEvent.EVENT_INFO_CHANGED:
+                state.onTaskFragmentInfoChanged(taskFragmentOrg, taskFragment);
+                break;
+            case PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED:
+                state.onTaskFragmentParentInfoChanged(taskFragmentOrg, taskFragment);
+                break;
+            case PendingTaskFragmentEvent.EVENT_ERROR:
+                state.onTaskFragmentError(taskFragmentOrg, event.mErrorCallback,
+                        event.mException);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index f43cd7a..d543c1f 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -261,7 +261,7 @@
             if (launchMode == WINDOWING_MODE_PINNED) {
                 if (DEBUG) appendLog("picture-in-picture");
             } else if (!root.isResizeable()) {
-                if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea)) {
+                if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea, options)) {
                     launchMode = WINDOWING_MODE_FREEFORM;
                     if (outParams.mBounds.isEmpty()) {
                         getTaskBounds(root, suggestedDisplayArea, layout, launchMode,
@@ -617,7 +617,11 @@
     }
 
     private boolean shouldLaunchUnresizableAppInFreeform(ActivityRecord activity,
-            TaskDisplayArea displayArea) {
+            TaskDisplayArea displayArea, @Nullable ActivityOptions options) {
+        if (options != null && options.getLaunchWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+            // Do not launch the activity in freeform if it explicitly requested fullscreen mode.
+            return false;
+        }
         if (!activity.supportsFreeformInDisplayArea(displayArea) || activity.isResizeable()) {
             return false;
         }
@@ -713,7 +717,7 @@
         }
 
         // First we get the default size we want.
-        getDefaultFreeformSize(displayArea, layout, orientation, mTmpBounds);
+        getDefaultFreeformSize(root.info, displayArea, layout, orientation, mTmpBounds);
         if (hasInitialBounds || sizeMatches(inOutBounds, mTmpBounds)) {
             // We're here because either input parameters specified initial bounds, or the suggested
             // bounds have the same size of the default freeform size. We should use the suggested
@@ -781,7 +785,8 @@
         return orientation;
     }
 
-    private void getDefaultFreeformSize(@NonNull TaskDisplayArea displayArea,
+    private void getDefaultFreeformSize(@NonNull ActivityInfo info,
+            @NonNull TaskDisplayArea displayArea,
             @NonNull ActivityInfo.WindowLayout layout, int orientation, @NonNull Rect bounds) {
         // Default size, which is letterboxing/pillarboxing in displayArea. That's to say the large
         // dimension of default size is the small dimension of displayArea size, and the small
@@ -812,11 +817,38 @@
         final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth;
         final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight;
 
-        // Final result.
+        // Aspect ratio requirements.
+        final float minAspectRatio = info.getMinAspectRatio();
+        final float maxAspectRatio = info.getMaxAspectRatio();
+
         final int width = Math.min(defaultWidth, Math.max(phoneWidth, layoutMinWidth));
         final int height = Math.min(defaultHeight, Math.max(phoneHeight, layoutMinHeight));
+        final float aspectRatio = (float) Math.max(width, height) / (float) Math.min(width, height);
 
-        bounds.set(0, 0, width, height);
+        // Adjust the width and height to the aspect ratio requirements.
+        int adjWidth = width;
+        int adjHeight = height;
+        if (minAspectRatio >= 1 && aspectRatio < minAspectRatio) {
+            // The aspect ratio is below the minimum, adjust it to the minimum.
+            if (orientation == SCREEN_ORIENTATION_LANDSCAPE) {
+                // Fix the width, scale the height.
+                adjHeight = (int) (adjWidth / minAspectRatio + 0.5f);
+            } else {
+                // Fix the height, scale the width.
+                adjWidth = (int) (adjHeight / minAspectRatio + 0.5f);
+            }
+        } else if (maxAspectRatio >= 1 && aspectRatio > maxAspectRatio) {
+            // The aspect ratio exceeds the maximum, adjust it to the maximum.
+            if (orientation == SCREEN_ORIENTATION_LANDSCAPE) {
+                // Fix the width, scale the height.
+                adjHeight = (int) (adjWidth / maxAspectRatio + 0.5f);
+            } else {
+                // Fix the height, scale the width.
+                adjWidth = (int) (adjHeight / maxAspectRatio + 0.5f);
+            }
+        }
+
+        bounds.set(0, 0, adjWidth, adjHeight);
         bounds.offset(stableBounds.left, stableBounds.top);
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 88467ba..3d5f988 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -16,24 +16,19 @@
 
 package com.android.server.wm;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
 import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
-import static com.android.server.wm.WindowOrganizerController.CONTROLLABLE_CONFIGS;
-import static com.android.server.wm.WindowOrganizerController.CONTROLLABLE_WINDOW_CONFIGS;
+import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.pm.ParceledListSlice;
-import android.graphics.Rect;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.Parcel;
@@ -45,6 +40,7 @@
 import android.window.ITaskOrganizerController;
 import android.window.SplashScreenView;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskSnapshot;
 import android.window.WindowContainerToken;
@@ -69,21 +65,6 @@
 class TaskOrganizerController extends ITaskOrganizerController.Stub {
     private static final String TAG = "TaskOrganizerController";
 
-    /**
-     * Masks specifying which configurations are important to report back to an organizer when
-     * changed.
-     */
-    private static final int REPORT_CONFIGS = CONTROLLABLE_CONFIGS;
-    private static final int REPORT_WINDOW_CONFIGS = CONTROLLABLE_WINDOW_CONFIGS;
-
-    // The set of modes that are currently supports
-    // TODO: Remove once the task organizer can support all modes
-    @VisibleForTesting
-    static final int[] UNSUPPORTED_WINDOWING_MODES = {
-            WINDOWING_MODE_UNDEFINED,
-            WINDOWING_MODE_FREEFORM
-    };
-
     private class DeathRecipient implements IBinder.DeathRecipient {
         ITaskOrganizer mTaskOrganizer;
 
@@ -122,109 +103,6 @@
             return mTaskOrganizer.asBinder();
         }
 
-        void addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
-                TaskSnapshot taskSnapshot) {
-            final StartingWindowInfo info = task.getStartingWindowInfo(activity);
-            if (launchTheme != 0) {
-                info.splashScreenThemeResId = launchTheme;
-            }
-            info.mTaskSnapshot = taskSnapshot;
-            // make this happen prior than prepare surface
-            try {
-                mTaskOrganizer.addStartingWindow(info, activity.token);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Exception sending onTaskStart callback", e);
-            }
-        }
-
-        // Capture the animation surface control for activity's main window
-        private class StartingWindowAnimationAdaptor implements AnimationAdapter {
-            private SurfaceControl mAnimationLeash;
-            @Override
-            public boolean getShowWallpaper() {
-                return false;
-            }
-
-            @Override
-            public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-                    int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
-                mAnimationLeash = animationLeash;
-            }
-
-            @Override
-            public void onAnimationCancelled(SurfaceControl animationLeash) {
-                if (mAnimationLeash == animationLeash) {
-                    mAnimationLeash = null;
-                }
-            }
-
-            @Override
-            public long getDurationHint() {
-                return 0;
-            }
-
-            @Override
-            public long getStatusBarTransitionsStartTime() {
-                return 0;
-            }
-
-            @Override
-            public void dump(PrintWriter pw, String prefix) {
-                pw.print(prefix + "StartingWindowAnimationAdaptor mCapturedLeash=");
-                pw.print(mAnimationLeash);
-                pw.println();
-            }
-
-            @Override
-            public void dumpDebug(ProtoOutputStream proto) {
-            }
-        }
-
-        void removeStartingWindow(Task task, boolean prepareAnimation) {
-            SurfaceControl windowAnimationLeash = null;
-            Rect mainFrame = null;
-            final boolean playShiftUpAnimation = !task.inMultiWindowMode();
-            if (prepareAnimation && playShiftUpAnimation) {
-                final ActivityRecord topActivity = task.topActivityContainsStartingWindow();
-                if (topActivity != null) {
-                    final WindowState mainWindow =
-                            topActivity.findMainWindow(false/* includeStartingApp */);
-                    if (mainWindow != null) {
-                        final StartingWindowAnimationAdaptor adaptor =
-                                new StartingWindowAnimationAdaptor();
-                        final SurfaceControl.Transaction t = mainWindow.getPendingTransaction();
-                        mainWindow.startAnimation(t, adaptor, false,
-                                ANIMATION_TYPE_STARTING_REVEAL);
-                        windowAnimationLeash = adaptor.mAnimationLeash;
-                        mainFrame = mainWindow.getRelativeFrame();
-                        t.setPosition(windowAnimationLeash, mainFrame.left, mainFrame.top);
-                    }
-                }
-            }
-            try {
-                mTaskOrganizer.removeStartingWindow(task.mTaskId, windowAnimationLeash,
-                        mainFrame, prepareAnimation);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Exception sending onStartTaskFinished callback", e);
-            }
-        }
-
-        void copySplashScreenView(Task task) {
-            try {
-                mTaskOrganizer.copySplashScreenView(task.mTaskId);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Exception sending copyStartingWindowView callback", e);
-            }
-        }
-
-        void onAppSplashScreenViewRemoved(Task task) {
-            try {
-                mTaskOrganizer.onAppSplashScreenViewRemoved(task.mTaskId);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Exception sending onAppSplashScreenViewRemoved callback", e);
-            }
-        }
-
         SurfaceControl prepareLeash(Task task, String reason) {
             return new SurfaceControl(task.getSurfaceControl(), reason);
         }
@@ -311,23 +189,6 @@
             mUid = uid;
         }
 
-        void addStartingWindow(Task t, ActivityRecord activity, int launchTheme,
-                TaskSnapshot taskSnapshot) {
-            mOrganizer.addStartingWindow(t, activity, launchTheme, taskSnapshot);
-        }
-
-        void removeStartingWindow(Task t, boolean prepareAnimation) {
-            mOrganizer.removeStartingWindow(t, prepareAnimation);
-        }
-
-        void copySplashScreenView(Task t) {
-            mOrganizer.copySplashScreenView(t);
-        }
-
-        public void onAppSplashScreenViewRemoved(Task t) {
-            mOrganizer.onAppSplashScreenViewRemoved(t);
-        }
-
         /**
          * Register this task with this state, but doesn't trigger the task appeared callback to
          * the organizer.
@@ -389,6 +250,15 @@
                                 mOrganizer.mTaskOrganizer, t);
                     }
                 }
+                if (mService.getTransitionController().isShellTransitionsEnabled()) {
+                    // dispose is only called outside of transitions (eg during unregister). Since
+                    // we "migrate" surfaces when replacing organizers, visibility gets delegated
+                    // to transitions; however, since there is no transition at this point, we have
+                    // to manually show the surface here.
+                    if (t.mTaskOrganizer != null && t.getSurfaceControl() != null) {
+                        t.getSyncTransaction().show(t.getSurfaceControl());
+                    }
+                }
             }
 
             // Remove organizer state after removing tasks so we get a chance to send
@@ -481,7 +351,8 @@
         final int uid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
         try {
-            synchronized (mGlobalLock) {
+            final ArrayList<TaskAppearedInfo> taskInfos = new ArrayList<>();
+            final Runnable withGlobalLock = () -> {
                 ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Register task organizer=%s uid=%d",
                         organizer.asBinder(), uid);
                 if (!mTaskOrganizerStates.containsKey(organizer.asBinder())) {
@@ -490,24 +361,27 @@
                             new TaskOrganizerState(organizer, uid));
                 }
 
-                final ArrayList<TaskAppearedInfo> taskInfos = new ArrayList<>();
                 final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder());
                 mService.mRootWindowContainer.forAllTasks((task) -> {
-                    if (ArrayUtils.contains(UNSUPPORTED_WINDOWING_MODES, task.getWindowingMode())) {
-                        return;
-                    }
-
                     boolean returnTask = !task.mCreatedByOrganizer;
                     task.updateTaskOrganizerState(true /* forceUpdate */,
                             returnTask /* skipTaskAppeared */);
                     if (returnTask) {
                         SurfaceControl outSurfaceControl = state.addTaskWithoutCallback(task,
                                 "TaskOrganizerController.registerTaskOrganizer");
-                        taskInfos.add(new TaskAppearedInfo(task.getTaskInfo(), outSurfaceControl));
+                        taskInfos.add(
+                                new TaskAppearedInfo(task.getTaskInfo(), outSurfaceControl));
                     }
                 });
-                return new ParceledListSlice<>(taskInfos);
+            };
+            if (mService.getTransitionController().isShellTransitionsEnabled()) {
+                mService.getTransitionController().mRunningLock.runWhenIdle(1000, withGlobalLock);
+            } else {
+                synchronized (mGlobalLock) {
+                    withGlobalLock.run();
+                }
             }
+            return new ParceledListSlice<>(taskInfos);
         } finally {
             Binder.restoreCallingIdentity(origId);
         }
@@ -519,7 +393,7 @@
         final int uid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
         try {
-            synchronized (mGlobalLock) {
+            final Runnable withGlobalLock = () -> {
                 final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder());
                 if (state == null) {
                     return;
@@ -528,6 +402,13 @@
                         organizer.asBinder(), uid);
                 state.unlinkDeath();
                 state.dispose();
+            };
+            if (mService.getTransitionController().isShellTransitionsEnabled()) {
+                mService.getTransitionController().mRunningLock.runWhenIdle(1000, withGlobalLock);
+            } else {
+                synchronized (mGlobalLock) {
+                    withGlobalLock.run();
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -537,46 +418,136 @@
     /**
      * @return the task organizer key for a given windowing mode.
      */
-    ITaskOrganizer getTaskOrganizer(int windowingMode) {
-        return isSupportedWindowingMode(windowingMode)
-                ? mTaskOrganizers.peekLast()
-                : null;
+    ITaskOrganizer getTaskOrganizer() {
+        return mTaskOrganizers.peekLast();
     }
 
-    boolean isSupportedWindowingMode(int winMode) {
-        return !ArrayUtils.contains(UNSUPPORTED_WINDOWING_MODES, winMode);
+    // Capture the animation surface control for activity's main window
+    static class StartingWindowAnimationAdaptor implements AnimationAdapter {
+        SurfaceControl mAnimationLeash;
+        @Override
+        public boolean getShowWallpaper() {
+            return false;
+        }
+
+        @Override
+        public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
+                int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+            mAnimationLeash = animationLeash;
+        }
+
+        @Override
+        public void onAnimationCancelled(SurfaceControl animationLeash) {
+            if (mAnimationLeash == animationLeash) {
+                mAnimationLeash = null;
+            }
+        }
+
+        @Override
+        public long getDurationHint() {
+            return 0;
+        }
+
+        @Override
+        public long getStatusBarTransitionsStartTime() {
+            return 0;
+        }
+
+        @Override
+        public void dump(PrintWriter pw, String prefix) {
+            pw.print(prefix + "StartingWindowAnimationAdaptor mCapturedLeash=");
+            pw.print(mAnimationLeash);
+            pw.println();
+        }
+
+        @Override
+        public void dumpDebug(ProtoOutputStream proto) {
+        }
+    }
+
+    static SurfaceControl applyStartingWindowAnimation(WindowContainer window) {
+        final StartingWindowAnimationAdaptor adaptor = new StartingWindowAnimationAdaptor();
+        window.startAnimation(window.getPendingTransaction(), adaptor, false,
+                ANIMATION_TYPE_STARTING_REVEAL);
+        return adaptor.mAnimationLeash;
     }
 
     boolean addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
             TaskSnapshot taskSnapshot) {
         final Task rootTask = task.getRootTask();
-        if (rootTask == null || rootTask.mTaskOrganizer == null || activity.mStartingData == null) {
+        if (rootTask == null || activity.mStartingData == null) {
             return false;
         }
-        final TaskOrganizerState state =
-                mTaskOrganizerStates.get(rootTask.mTaskOrganizer.asBinder());
-        state.addStartingWindow(task, activity, launchTheme, taskSnapshot);
+        final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast();
+        if (lastOrganizer == null) {
+            return false;
+        }
+        final StartingWindowInfo info = task.getStartingWindowInfo(activity);
+        if (launchTheme != 0) {
+            info.splashScreenThemeResId = launchTheme;
+        }
+        info.taskSnapshot = taskSnapshot;
+        // make this happen prior than prepare surface
+        try {
+            lastOrganizer.addStartingWindow(info, activity.token);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Exception sending onTaskStart callback", e);
+            return false;
+        }
         return true;
     }
 
     void removeStartingWindow(Task task, boolean prepareAnimation) {
         final Task rootTask = task.getRootTask();
-        if (rootTask == null || rootTask.mTaskOrganizer == null) {
+        if (rootTask == null) {
             return;
         }
-        final TaskOrganizerState state =
-                mTaskOrganizerStates.get(rootTask.mTaskOrganizer.asBinder());
-        state.removeStartingWindow(task, prepareAnimation);
+        final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast();
+        if (lastOrganizer == null) {
+            return;
+        }
+        final StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo();
+        removalInfo.taskId = task.mTaskId;
+        removalInfo.playRevealAnimation = prepareAnimation;
+        final boolean playShiftUpAnimation = !task.inMultiWindowMode();
+        final ActivityRecord topActivity = task.topActivityContainsStartingWindow();
+        if (topActivity != null) {
+            removalInfo.deferRemoveForIme = topActivity.mDisplayContent
+                    .mayImeShowOnLaunchingActivity(topActivity);
+            if (prepareAnimation && playShiftUpAnimation) {
+                final WindowState mainWindow =
+                        topActivity.findMainWindow(false/* includeStartingApp */);
+                if (mainWindow != null) {
+                    final SurfaceControl.Transaction t = mainWindow.getPendingTransaction();
+                    removalInfo.windowAnimationLeash = applyStartingWindowAnimation(mainWindow);
+                    removalInfo.mainFrame = mainWindow.getRelativeFrame();
+                    t.setPosition(removalInfo.windowAnimationLeash,
+                            removalInfo.mainFrame.left, removalInfo.mainFrame.top);
+                }
+            }
+        }
+        try {
+            lastOrganizer.removeStartingWindow(removalInfo);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Exception sending onStartTaskFinished callback", e);
+        }
     }
 
     boolean copySplashScreenView(Task task) {
         final Task rootTask = task.getRootTask();
-        if (rootTask == null || rootTask.mTaskOrganizer == null) {
+        if (rootTask == null) {
             return false;
         }
-        final TaskOrganizerState state =
-                mTaskOrganizerStates.get(rootTask.mTaskOrganizer.asBinder());
-        state.copySplashScreenView(task);
+        final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast();
+        if (lastOrganizer == null) {
+            return false;
+        }
+        try {
+            lastOrganizer.copySplashScreenView(task.mTaskId);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Exception sending copyStartingWindowView callback", e);
+            return false;
+        }
         return true;
     }
 
@@ -588,12 +559,18 @@
      */
     public void onAppSplashScreenViewRemoved(Task task) {
         final Task rootTask = task.getRootTask();
-        if (rootTask == null || rootTask.mTaskOrganizer == null) {
+        if (rootTask == null) {
             return;
         }
-        final TaskOrganizerState state =
-                mTaskOrganizerStates.get(rootTask.mTaskOrganizer.asBinder());
-        state.onAppSplashScreenViewRemoved(task);
+        final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast();
+        if (lastOrganizer == null) {
+            return;
+        }
+        try {
+            lastOrganizer.onAppSplashScreenViewRemoved(task.mTaskId);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Exception sending onAppSplashScreenViewRemoved callback", e);
+        }
     }
 
     void onTaskAppeared(ITaskOrganizer organizer, Task task) {
@@ -688,7 +665,7 @@
 
                 ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Delete root task display=%d winMode=%d",
                         task.getDisplayId(), task.getWindowingMode());
-                task.removeImmediately("deleteRootTask");
+                task.remove(true /* withTransition */, "deleteRootTask");
                 return true;
             }
         } finally {
@@ -718,6 +695,7 @@
                     if (state != null) {
                         state.mOrganizer.onTaskVanished(task);
                     }
+                    mLastSentTaskInfos.remove(task);
                     break;
                 case PendingTaskEvent.EVENT_INFO_CHANGED:
                     dispatchTaskInfoChanged(event.mTask, event.mForce);
@@ -777,18 +755,9 @@
         mTmpTaskInfo.configuration.unset();
         task.fillTaskInfo(mTmpTaskInfo);
 
-        boolean changed = !mTmpTaskInfo.equalsForTaskOrganizer(lastInfo);
-        if (!changed) {
-            int cfgChanges = mTmpTaskInfo.configuration.diff(lastInfo.configuration);
-            final int winCfgChanges = (cfgChanges & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0
-                    ? (int) mTmpTaskInfo.configuration.windowConfiguration.diff(
-                            lastInfo.configuration.windowConfiguration,
-                            true /* compareUndefined */) : 0;
-            if ((winCfgChanges & REPORT_WINDOW_CONFIGS) == 0) {
-                cfgChanges &= ~ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
-            }
-            changed = (cfgChanges & REPORT_CONFIGS) != 0;
-        }
+        boolean changed = !mTmpTaskInfo.equalsForTaskOrganizer(lastInfo)
+                || !configurationsAreEqualForOrganizer(
+                        mTmpTaskInfo.configuration, lastInfo.configuration);
         if (!(changed || force)) {
             // mTmpTaskInfo will be reused next time.
             return;
@@ -1021,9 +990,6 @@
             for (int k = 0; k < tasks.size(); k++) {
                 final Task task = tasks.get(k);
                 final int mode = task.getWindowingMode();
-                if (ArrayUtils.contains(UNSUPPORTED_WINDOWING_MODES, mode)) {
-                    continue;
-                }
                 pw.println(innerPrefix + "    ("
                         + WindowConfiguration.windowingModeToString(mode) + ") " + task);
             }
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index e743710..ce93f24 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -168,6 +168,9 @@
      */
     @VisibleForTesting
     void addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks) {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
         mSkipClosingAppSnapshotTasks.addAll(tasks);
     }
 
@@ -175,43 +178,46 @@
         snapshotTasks(tasks, false /* allowSnapshotHome */);
     }
 
+    void recordTaskSnapshot(Task task, boolean allowSnapshotHome) {
+        final TaskSnapshot snapshot;
+        final boolean snapshotHome = allowSnapshotHome && task.isActivityTypeHome();
+        if (snapshotHome) {
+            snapshot = snapshotTask(task);
+        } else {
+            switch (getSnapshotMode(task)) {
+                case SNAPSHOT_MODE_NONE:
+                    return;
+                case SNAPSHOT_MODE_APP_THEME:
+                    snapshot = drawAppThemeSnapshot(task);
+                    break;
+                case SNAPSHOT_MODE_REAL:
+                    snapshot = snapshotTask(task);
+                    break;
+                default:
+                    snapshot = null;
+                    break;
+            }
+        }
+        if (snapshot != null) {
+            final HardwareBuffer buffer = snapshot.getHardwareBuffer();
+            if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
+                buffer.close();
+                Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x"
+                        + buffer.getHeight());
+            } else {
+                mCache.putSnapshot(task, snapshot);
+                // Don't persist or notify the change for the temporal snapshot.
+                if (!snapshotHome) {
+                    mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
+                    task.onSnapshotChanged(snapshot);
+                }
+            }
+        }
+    }
+
     private void snapshotTasks(ArraySet<Task> tasks, boolean allowSnapshotHome) {
         for (int i = tasks.size() - 1; i >= 0; i--) {
-            final Task task = tasks.valueAt(i);
-            final TaskSnapshot snapshot;
-            final boolean snapshotHome = allowSnapshotHome && task.isActivityTypeHome();
-            if (snapshotHome) {
-                snapshot = snapshotTask(task);
-            } else {
-                switch (getSnapshotMode(task)) {
-                    case SNAPSHOT_MODE_NONE:
-                        continue;
-                    case SNAPSHOT_MODE_APP_THEME:
-                        snapshot = drawAppThemeSnapshot(task);
-                        break;
-                    case SNAPSHOT_MODE_REAL:
-                        snapshot = snapshotTask(task);
-                        break;
-                    default:
-                        snapshot = null;
-                        break;
-                }
-            }
-            if (snapshot != null) {
-                final HardwareBuffer buffer = snapshot.getHardwareBuffer();
-                if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
-                    buffer.close();
-                    Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x"
-                            + buffer.getHeight());
-                } else {
-                    mCache.putSnapshot(task, snapshot);
-                    // Don't persist or notify the change for the temporal snapshot.
-                    if (!snapshotHome) {
-                        mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
-                        task.onSnapshotChanged(snapshot);
-                    }
-                }
-            }
+            recordTaskSnapshot(tasks.valueAt(i), allowSnapshotHome);
         }
     }
 
@@ -683,7 +689,8 @@
     }
 
     static Rect getSystemBarInsets(Rect frame, InsetsState state) {
-        return state.calculateInsets(frame, Type.systemBars(), false /* ignoreVisibility */);
+        return state.calculateInsets(
+                frame, Type.systemBars(), false /* ignoreVisibility */).toRect();
     }
 
     void dump(PrintWriter pw, String prefix) {
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
index cc4abab..059eb87 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
@@ -72,6 +72,7 @@
 import android.view.IWindowSession;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.View;
@@ -166,6 +167,7 @@
         final ClientWindowFrames tmpFrames = new ClientWindowFrames();
         final Rect taskBounds;
         final InsetsState mTmpInsetsState = new InsetsState();
+        final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
         final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0];
         final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration();
         final TaskDescription taskDescription = new TaskDescription();
@@ -227,7 +229,8 @@
         int displayId = activity.getDisplayContent().getDisplayId();
         try {
             final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId,
-                    mTmpInsetsState, null /* outInputChannel */, mTmpInsetsState, mTempControls);
+                    mRequestedVisibilities, null /* outInputChannel */, mTmpInsetsState,
+                    mTempControls);
             if (res < 0) {
                 Slog.w(TAG, "Failed to add snapshot starting window res=" + res);
                 return null;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 0cd09807..8bd6257 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -16,11 +16,19 @@
 
 package com.android.server.wm;
 
-
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
@@ -31,34 +39,45 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.WindowManager.TransitionFlags;
+import static android.view.WindowManager.TransitionType;
 import static android.view.WindowManager.transitTypeToString;
+import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
 import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
+import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
+import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
+import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.content.pm.ActivityInfo;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
 import android.view.SurfaceControl;
-import android.view.WindowManager;
 import android.view.animation.Animation;
-import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -100,12 +119,12 @@
     @Retention(RetentionPolicy.SOURCE)
     @interface TransitionState {}
 
-    final @WindowManager.TransitionType int mType;
+    final @TransitionType int mType;
     private int mSyncId;
-    private @WindowManager.TransitionFlags int mFlags;
+    private @TransitionFlags int mFlags;
     private final TransitionController mController;
     private final BLASTSyncEngine mSyncEngine;
-    private IRemoteTransition mRemoteTransition = null;
+    private RemoteTransition mRemoteTransition = null;
 
     /** Only use for clean-up after binder death! */
     private SurfaceControl.Transaction mStartTransaction = null;
@@ -124,10 +143,29 @@
     /** The final animation targets derived from participants after promotion. */
     private ArraySet<WindowContainer> mTargets = null;
 
-    private @TransitionState int mState = STATE_COLLECTING;
-    private boolean mReadyCalled = false;
+    /**
+     * Set of participating windowtokens (activity/wallpaper) which are visible at the end of
+     * the transition animation.
+     */
+    private final ArraySet<WindowToken> mVisibleAtTransitionEndTokens = new ArraySet<>();
 
-    Transition(@WindowManager.TransitionType int type, @WindowManager.TransitionFlags int flags,
+    /** Set of transient activities (lifecycle initially tied to this transition). */
+    private ArraySet<ActivityRecord> mTransientLaunches = null;
+
+    /** Custom activity-level animation options and callbacks. */
+    private TransitionInfo.AnimationOptions mOverrideOptions;
+    private IRemoteCallback mClientAnimationStartCallback = null;
+    private IRemoteCallback mClientAnimationFinishCallback = null;
+
+    private @TransitionState int mState = STATE_COLLECTING;
+    private final ReadyTracker mReadyTracker = new ReadyTracker();
+
+    // TODO(b/188595497): remove when not needed.
+    /** @see RecentsAnimationController#mNavigationBarAttachedToApp */
+    private boolean mNavBarAttachedToApp = false;
+    private int mRecentsDisplayId = INVALID_DISPLAY;
+
+    Transition(@TransitionType int type, @TransitionFlags int flags,
             TransitionController controller, BLASTSyncEngine syncEngine) {
         mType = type;
         mFlags = flags;
@@ -136,11 +174,34 @@
         mSyncId = mSyncEngine.startSyncSet(this);
     }
 
+    void addFlag(int flag) {
+        mFlags |= flag;
+    }
+
+    /** Records an activity as transient-launch. This activity must be already collected. */
+    void setTransientLaunch(@NonNull ActivityRecord activity) {
+        if (mTransientLaunches == null) {
+            mTransientLaunches = new ArraySet<>();
+        }
+        mTransientLaunches.add(activity);
+        ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as "
+                + "transient-launch", mSyncId, activity);
+    }
+
+    boolean isTransientLaunch(@NonNull ActivityRecord activity) {
+        return mTransientLaunches != null && mTransientLaunches.contains(activity);
+    }
+
     @VisibleForTesting
     int getSyncId() {
         return mSyncId;
     }
 
+    @TransitionFlags
+    int getFlags() {
+        return mFlags;
+    }
+
     /**
      * Formally starts the transition. Participants can be collected before this is started,
      * but this won't consider itself ready until started -- even if all the participants have
@@ -153,9 +214,7 @@
         mState = STATE_STARTED;
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Starting Transition %d",
                 mSyncId);
-        if (mReadyCalled) {
-            setReady();
-        }
+        applyReady();
     }
 
     /**
@@ -170,6 +229,11 @@
         for (WindowContainer curr = wc.getParent(); curr != null && !mChanges.containsKey(curr);
                 curr = curr.getParent()) {
             mChanges.put(curr, new ChangeInfo(curr));
+            if (isReadyGroup(curr)) {
+                mReadyTracker.addGroup(curr);
+                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for"
+                                + " Transition %d with root=%s", mSyncId, curr);
+            }
         }
         if (mParticipants.contains(wc)) return;
         mSyncEngine.addToSyncSet(mSyncId, wc);
@@ -207,25 +271,71 @@
     }
 
     /**
+     * Specifies configuration change explicitly for the window container, so it can be chosen as
+     * transition target. This is usually used with transition mode
+     * {@link android.view.WindowManager#TRANSIT_CHANGE}.
+     */
+    void setKnownConfigChanges(WindowContainer<?> wc, @ActivityInfo.Config int changes) {
+        final ChangeInfo changeInfo = mChanges.get(wc);
+        if (changeInfo != null) {
+            changeInfo.mKnownConfigChanges = changes;
+        }
+    }
+
+    private void sendRemoteCallback(@Nullable IRemoteCallback callback) {
+        if (callback == null) return;
+        mController.mAtm.mH.sendMessage(PooledLambda.obtainMessage(cb -> {
+            try {
+                cb.sendResult(null);
+            } catch (RemoteException e) { }
+        }, callback));
+    }
+
+    /**
+     * Set animation options for collecting transition by ActivityRecord.
+     * @param options AnimationOptions captured from ActivityOptions
+     */
+    void setOverrideAnimation(TransitionInfo.AnimationOptions options,
+            @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) {
+        if (mSyncId < 0) return;
+        mOverrideOptions = options;
+        sendRemoteCallback(mClientAnimationStartCallback);
+        mClientAnimationStartCallback = startCallback;
+        mClientAnimationFinishCallback = finishCallback;
+    }
+
+    /**
      * Call this when all known changes related to this transition have been applied. Until
      * all participants have finished drawing, the transition can still collect participants.
      *
      * If this is called before the transition is started, it will be deferred until start.
+     *
+     * @param wc A reference point to determine which ready-group to update. For now, each display
+     *           has its own ready-group, so this is used to look-up which display to mark ready.
+     *           The transition will wait for all groups to be ready.
      */
-    void setReady(boolean ready) {
+    void setReady(WindowContainer wc, boolean ready) {
         if (mSyncId < 0) return;
-        if (mState < STATE_STARTED) {
-            mReadyCalled = ready;
-            return;
-        }
+        mReadyTracker.setReadyFrom(wc, ready);
+        applyReady();
+    }
+
+    private void applyReady() {
+        if (mState < STATE_STARTED) return;
+        final boolean ready = mReadyTracker.allReady();
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                 "Set transition ready=%b %d", ready, mSyncId);
         mSyncEngine.setReady(mSyncId, ready);
     }
 
-    /** @see #setReady . This calls with parameter true. */
-    void setReady() {
-        setReady(true);
+    /**
+     * Sets all possible ready groups to ready.
+     * @see ReadyTracker#setAllReady.
+     */
+    void setAllReady() {
+        if (mSyncId < 0) return;
+        mReadyTracker.setAllReady();
+        applyReady();
     }
 
     /**
@@ -275,20 +385,82 @@
         }
 
         // Commit all going-invisible containers
+        boolean activitiesWentInvisible = false;
         for (int i = 0; i < mParticipants.size(); ++i) {
             final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
-            if (ar != null && !ar.isVisibleRequested()) {
-                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
-                        "  Commit activity becoming invisible: %s", ar);
-                ar.commitVisibility(false /* visible */, false /* performLayout */);
+            if (ar != null) {
+                boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(ar);
+                // We need both the expected visibility AND current requested-visibility to be
+                // false. If it is expected-visible but not currently visible, it means that
+                // another animation is queued-up to animate this to invisibility, so we can't
+                // remove the surfaces yet. If it is currently visible, but not expected-visible,
+                // then doing commitVisibility here would actually be out-of-order and leave the
+                // activity in a bad state.
+                if (!visibleAtTransitionEnd && !ar.isVisibleRequested()) {
+                    boolean commitVisibility = true;
+                    if (ar.getDeferHidingClient() && ar.getTask() != null) {
+                        if (ar.pictureInPictureArgs != null
+                                && ar.pictureInPictureArgs.isAutoEnterEnabled()) {
+                            mController.mAtm.enterPictureInPictureMode(ar, ar.pictureInPictureArgs);
+                            // Avoid commit visibility to false here, or else we will get a sudden
+                            // "flash" / surface going invisible for a split second.
+                            commitVisibility = false;
+                        } else {
+                            mController.mAtm.mTaskSupervisor.mUserLeaving = true;
+                            ar.getTaskFragment().startPausing(false /* uiSleeping */,
+                                    null /* resuming */, "finishTransition");
+                            mController.mAtm.mTaskSupervisor.mUserLeaving = false;
+                        }
+                    }
+                    if (commitVisibility) {
+                        ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+                                "  Commit activity becoming invisible: %s", ar);
+                        final Task task = ar.getTask();
+                        if (task != null && !task.isVisibleRequested()
+                                && mTransientLaunches != null) {
+                            // If transition is transient, then snapshots are taken at end of
+                            // transition.
+                            mController.mTaskSnapshotController.recordTaskSnapshot(
+                                    task, false /* allowSnapshotHome */);
+                        }
+                        ar.commitVisibility(false /* visible */, false /* performLayout */,
+                                true /* fromTransition */);
+                        activitiesWentInvisible = true;
+                    }
+                }
+                if (mChanges.get(ar).mVisible != visibleAtTransitionEnd) {
+                    // Legacy dispatch relies on this (for now).
+                    ar.mEnteringAnimation = visibleAtTransitionEnd;
+                }
+                mController.dispatchLegacyAppTransitionFinished(ar);
             }
             final WallpaperWindowToken wt = mParticipants.valueAt(i).asWallpaperToken();
-            if (wt != null && !wt.isVisibleRequested()) {
-                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
-                        "  Commit wallpaper becoming invisible: %s", ar);
-                wt.commitVisibility(false /* visible */);
+            if (wt != null) {
+                final boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(wt);
+                if (!visibleAtTransitionEnd && !wt.isVisibleRequested()) {
+                    ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+                            "  Commit wallpaper becoming invisible: %s", wt);
+                    wt.commitVisibility(false /* visible */);
+                }
             }
         }
+        if (activitiesWentInvisible) {
+            // Always schedule stop processing when transition finishes because activities don't
+            // stop while they are in a transition thus their stop could still be pending.
+            mController.mAtm.mTaskSupervisor
+                    .scheduleProcessStoppingAndFinishingActivitiesIfNeeded();
+        }
+
+        sendRemoteCallback(mClientAnimationFinishCallback);
+
+        legacyRestoreNavigationBarFromApp();
+
+        if (mRecentsDisplayId != INVALID_DISPLAY) {
+            // Clean up input monitors (for recents)
+            final DisplayContent dc =
+                    mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId);
+            dc.getInputMonitor().setActiveRecents(null /* activity */, null /* layer */);
+        }
     }
 
     void abort() {
@@ -297,16 +469,17 @@
         if (mState != STATE_COLLECTING) {
             throw new IllegalStateException("Too late to abort.");
         }
+        mController.dispatchLegacyAppTransitionCancelled();
         mState = STATE_ABORT;
         // Syncengine abort will call through to onTransactionReady()
         mSyncEngine.abort(mSyncId);
     }
 
-    void setRemoteTransition(IRemoteTransition remoteTransition) {
+    void setRemoteTransition(RemoteTransition remoteTransition) {
         mRemoteTransition = remoteTransition;
     }
 
-    IRemoteTransition getRemoteTransition() {
+    RemoteTransition getRemoteTransition() {
         return mRemoteTransition;
     }
 
@@ -327,6 +500,7 @@
             mController.mAtm.mRootWindowContainer.getDisplayContent(displayId)
                     .getPendingTransaction().merge(transaction);
             mSyncId = -1;
+            mOverrideOptions = null;
             return;
         }
 
@@ -340,9 +514,18 @@
         // Resolve the animating targets from the participants
         mTargets = calculateTargets(mParticipants, mChanges);
         final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, mChanges);
+        info.setAnimationOptions(mOverrideOptions);
+
+        // TODO(b/188669821): Move to animation impl in shell.
+        handleLegacyRecentsStartBehavior(displayId, info);
 
         handleNonAppWindowsInTransition(displayId, mType, mFlags);
 
+        reportStartReasonsToLogger();
+
+        // The callback is only populated for custom activity-level client animations
+        sendRemoteCallback(mClientAnimationStartCallback);
+
         // Manually show any activities that are visibleRequested. This is needed to properly
         // support simultaneous animation queueing/merging. Specifically, if transition A makes
         // an activity invisible, it's finishTransaction (which is applied *after* the animation)
@@ -354,12 +537,54 @@
             final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
             if (ar == null || !ar.mVisibleRequested) continue;
             transaction.show(ar.getSurfaceControl());
+
+            // Also manually show any non-reported parents. This is necessary in a few cases
+            // where a task is NOT organized but had its visibility changed within its direct
+            // parent. An example of this is if an alternate home leaf-task HB is started atop the
+            // normal home leaf-task HA: these are both in the Home root-task HR, so there will be a
+            // transition containing HA and HB where HA surface is hidden. If a standard task SA is
+            // launched on top, then HB finishes, no transition will happen since neither home is
+            // visible. When SA finishes, the transition contains HR rather than HA. Since home
+            // leaf-tasks are NOT organized, HA won't be in the transition and thus its surface
+            // wouldn't be shown. Just show is safe here since all other properties will have
+            // already been reset by the original hiding-transition's finishTransaction (we can't
+            // show in the finishTransaction because by then the activity doesn't hide until
+            // surface placement).
+            for (WindowContainer p = ar.getParent(); p != null && !mTargets.contains(p);
+                    p = p.getParent()) {
+                if (p.getSurfaceControl() != null) {
+                    transaction.show(p.getSurfaceControl());
+                }
+            }
+        }
+
+        // Record windowtokens (activity/wallpaper) that are expected to be visible after the
+        // transition animation. This will be used in finishTransition to prevent prematurely
+        // committing visibility.
+        for (int i = mParticipants.size() - 1; i >= 0; --i) {
+            final WindowContainer wc = mParticipants.valueAt(i);
+            if (wc.asWindowToken() == null || !wc.isVisibleRequested()) continue;
+            mVisibleAtTransitionEndTokens.add(wc.asWindowToken());
+        }
+
+        // Take task snapshots before the animation so that we can capture IME before it gets
+        // transferred. If transition is transient, IME won't be moved during the transition and
+        // the tasks are still live, so we take the snapshot at the end of the transition instead.
+        if (mTransientLaunches == null) {
+            for (int i = mParticipants.size() - 1; i >= 0; --i) {
+                final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
+                if (ar == null || ar.isVisibleRequested() || ar.getTask() == null
+                        || ar.getTask().isVisibleRequested()) continue;
+                mController.mTaskSnapshotController.recordTaskSnapshot(
+                        ar.getTask(), false /* allowSnapshotHome */);
+            }
         }
 
         mStartTransaction = transaction;
         mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
         buildFinishTransaction(mFinishTransaction, info.getRootLeash());
         if (mController.getTransitionPlayer() != null) {
+            mController.dispatchLegacyAppTransitionStarting(info);
             try {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                         "Calling onTransitionReady: %s", info);
@@ -375,6 +600,7 @@
             cleanUpOnFailure();
         }
         mSyncId = -1;
+        mOverrideOptions = null;
     }
 
     /**
@@ -391,17 +617,152 @@
         if (mFinishTransaction != null) {
             mFinishTransaction.apply();
         }
-        finishTransition();
+        mController.finishTransition(this);
+    }
+
+    /** @see RecentsAnimationController#attachNavigationBarToApp */
+    private void handleLegacyRecentsStartBehavior(int displayId, TransitionInfo info) {
+        if ((mFlags & TRANSIT_FLAG_IS_RECENTS) == 0) {
+            return;
+        }
+        final DisplayContent dc =
+                mController.mAtm.mRootWindowContainer.getDisplayContent(displayId);
+        if (dc == null) return;
+        mRecentsDisplayId = displayId;
+
+        // Recents has an input-consumer to grab input from the "live tile" app. Set that up here
+        final InputConsumerImpl recentsAnimationInputConsumer =
+                dc.getInputMonitor().getInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
+        if (recentsAnimationInputConsumer != null) {
+            // find the top-most going-away activity and the recents activity. The top-most
+            // is used as layer reference while the recents is used for registering the consumer
+            // override.
+            ActivityRecord recentsActivity = null;
+            ActivityRecord topActivity = null;
+            for (int i = 0; i < info.getChanges().size(); ++i) {
+                final TransitionInfo.Change change = info.getChanges().get(i);
+                if (change.getTaskInfo() == null) continue;
+                final Task task = Task.fromWindowContainerToken(
+                        info.getChanges().get(i).getTaskInfo().token);
+                if (task == null) continue;
+                final int activityType = change.getTaskInfo().topActivityType;
+                final boolean isRecents = activityType == ACTIVITY_TYPE_HOME
+                        || activityType == ACTIVITY_TYPE_RECENTS;
+                if (isRecents && recentsActivity == null) {
+                    recentsActivity = task.getTopVisibleActivity();
+                } else if (!isRecents && topActivity == null) {
+                    topActivity = task.getTopNonFinishingActivity();
+                }
+            }
+            if (recentsActivity != null && topActivity != null) {
+                recentsAnimationInputConsumer.mWindowHandle.touchableRegion.set(
+                        topActivity.getBounds());
+                dc.getInputMonitor().setActiveRecents(recentsActivity, topActivity);
+            }
+        }
+
+        // The rest of this function handles nav-bar reparenting
+
+        if (!dc.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
+                // Skip the case where the nav bar is controlled by fade rotation.
+                || dc.getFadeRotationAnimationController() != null) {
+            return;
+        }
+
+        WindowContainer topWC = null;
+        // Find the top-most non-home, closing app.
+        for (int i = 0; i < info.getChanges().size(); ++i) {
+            final TransitionInfo.Change c = info.getChanges().get(i);
+            if (c.getTaskInfo() == null || c.getTaskInfo().displayId != displayId
+                    || c.getTaskInfo().getActivityType() != ACTIVITY_TYPE_STANDARD
+                    || !(c.getMode() == TRANSIT_CLOSE || c.getMode() == TRANSIT_TO_BACK)) {
+                continue;
+            }
+            topWC = WindowContainer.fromBinder(c.getContainer().asBinder());
+            break;
+        }
+        if (topWC == null || topWC.inMultiWindowMode()) {
+            return;
+        }
+
+        final WindowState navWindow = dc.getDisplayPolicy().getNavigationBar();
+        if (navWindow == null || navWindow.mToken == null) {
+            return;
+        }
+        mNavBarAttachedToApp = true;
+        navWindow.mToken.cancelAnimation();
+        final SurfaceControl.Transaction t = navWindow.mToken.getPendingTransaction();
+        final SurfaceControl navSurfaceControl = navWindow.mToken.getSurfaceControl();
+        t.reparent(navSurfaceControl, topWC.getSurfaceControl());
+        t.show(navSurfaceControl);
+
+        final WindowContainer imeContainer = dc.getImeContainer();
+        if (imeContainer.isVisible()) {
+            t.setRelativeLayer(navSurfaceControl, imeContainer.getSurfaceControl(), 1);
+        } else {
+            // Place the nav bar on top of anything else in the top activity.
+            t.setLayer(navSurfaceControl, Integer.MAX_VALUE);
+        }
+        if (mController.mStatusBar != null) {
+            mController.mStatusBar.setNavigationBarLumaSamplingEnabled(displayId, false);
+        }
+    }
+
+    /** @see RecentsAnimationController#restoreNavigationBarFromApp */
+    void legacyRestoreNavigationBarFromApp() {
+        if (!mNavBarAttachedToApp) return;
+        mNavBarAttachedToApp = false;
+
+        if (mRecentsDisplayId == INVALID_DISPLAY) {
+            Slog.e(TAG, "Reparented navigation bar without a valid display");
+            mRecentsDisplayId = DEFAULT_DISPLAY;
+        }
+
+        if (mController.mStatusBar != null) {
+            mController.mStatusBar.setNavigationBarLumaSamplingEnabled(mRecentsDisplayId, true);
+        }
+
+        final DisplayContent dc =
+                mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId);
+        final WindowState navWindow = dc.getDisplayPolicy().getNavigationBar();
+        if (navWindow == null) return;
+        navWindow.setSurfaceTranslationY(0);
+
+        final WindowToken navToken = navWindow.mToken;
+        if (navToken == null) return;
+        final SurfaceControl.Transaction t = dc.getPendingTransaction();
+        final WindowContainer parent = navToken.getParent();
+        t.setLayer(navToken.getSurfaceControl(), navToken.getLastLayer());
+
+        boolean animate = false;
+        // Search for the home task. If it is supposed to be visible, then the navbar is not at
+        // the bottom of the screen, so we need to animate it.
+        for (int i = 0; i < mTargets.size(); ++i) {
+            final Task task = mTargets.valueAt(i).asTask();
+            if (task == null || !task.isHomeOrRecentsRootTask()) continue;
+            animate = task.isVisibleRequested();
+            break;
+        }
+
+        if (animate) {
+            final NavBarFadeAnimationController controller =
+                    new NavBarFadeAnimationController(dc);
+            controller.fadeWindowToken(true);
+        } else {
+            // Reparent the SurfaceControl of nav bar token back.
+            t.reparent(navToken.getSurfaceControl(), parent.getSurfaceControl());
+        }
     }
 
     private void handleNonAppWindowsInTransition(int displayId,
-            @WindowManager.TransitionType int transit, int flags) {
+            @TransitionType int transit, @TransitionFlags int flags) {
         final DisplayContent dc =
                 mController.mAtm.mRootWindowContainer.getDisplayContent(displayId);
         if (dc == null) {
             return;
         }
-        if (transit == TRANSIT_KEYGUARD_GOING_AWAY
+        if ((transit == TRANSIT_KEYGUARD_GOING_AWAY
+                || (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0)
                 && !WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation) {
             if ((flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER) != 0
                     && (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0
@@ -419,14 +780,37 @@
                     (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER) != 0,
                     (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE) != 0,
                     (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION) != 0);
-            mController.mAtm.mWindowManager.mPolicy.startKeyguardExitAnimation(
-                    SystemClock.uptimeMillis(), 0 /* duration */);
+            if (!WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation) {
+                // When remote animation is enabled for KEYGUARD_GOING_AWAY transition, SysUI
+                // receives IRemoteAnimationRunner#onAnimationStart to start animation, so we don't
+                // need to call IKeyguardService#keyguardGoingAway here.
+                mController.mAtm.mWindowManager.mPolicy.startKeyguardExitAnimation(
+                        SystemClock.uptimeMillis(), 0 /* duration */);
+            }
         }
         if ((flags & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) {
-            mController.mAtm.mWindowManager.mPolicy.applyKeyguardOcclusionChange();
+            mController.mAtm.mWindowManager.mPolicy.applyKeyguardOcclusionChange(
+                    true /* keyguardOccludingStarted */);
         }
     }
 
+    private void reportStartReasonsToLogger() {
+        // Record transition start in metrics logger. We just assume everything is "DRAWN"
+        // at this point since splash-screen is a presentation (shell) detail.
+        ArrayMap<WindowContainer, Integer> reasons = new ArrayMap<>();
+        for (int i = mParticipants.size() - 1; i >= 0; --i) {
+            ActivityRecord r = mParticipants.valueAt(i).asActivityRecord();
+            if (r == null || !r.mVisibleRequested) continue;
+            // At this point, r is "ready", but if it's not "ALL ready" then it is probably only
+            // ready due to starting-window.
+            reasons.put(r, (r.mStartingData instanceof SplashScreenStartingData
+                    && !r.mLastAllReadyAtSync)
+                    ? APP_TRANSITION_SPLASH_SCREEN : APP_TRANSITION_WINDOWS_DRAWN);
+        }
+        mController.mAtm.mTaskSupervisor.getActivityMetricsLogger().notifyTransitionStarting(
+                reasons);
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder(64);
@@ -465,6 +849,22 @@
         return wc.asWallpaperToken() != null;
     }
 
+    private static boolean occludesKeyguard(WindowContainer wc) {
+        final ActivityRecord ar = wc.asActivityRecord();
+        if (ar != null) {
+            return ar.canShowWhenLocked();
+        }
+        final Task t = wc.asTask();
+        if (t != null) {
+            // Get the top activity which was visible (since this is going away, it will remain
+            // client visible until the transition is finished).
+            // skip hidden (or about to hide) apps
+            final ActivityRecord top = t.getActivity(WindowToken::isClientVisible);
+            return top != null && top.canShowWhenLocked();
+        }
+        return false;
+    }
+
     /**
      * Under some conditions (eg. all visible targets within a parent container are transitioning
      * the same way) the transition can be "promoted" to the parent container. This means an
@@ -612,7 +1012,11 @@
         // of participants that should always be reported even if they aren't top.
         for (WindowContainer wc : participants) {
             // Don't include detached windows.
-            if (!wc.isAttached()) continue;
+            if (!wc.isAttached()) {
+                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+                        "  Rejecting as detached: %s", wc);
+                continue;
+            }
 
             final ChangeInfo changeInfo = changes.get(wc);
 
@@ -629,15 +1033,19 @@
             if (reportIfNotTop(wc)) {
                 tmpList.add(wc);
             }
+            // Wallpaper must be the top (regardless of how nested it is in DisplayAreas).
+            boolean skipIntermediateReports = isWallpaper(wc);
             for (WindowContainer p = wc.getParent(); p != null; p = p.getParent()) {
-                if (!p.isAttached() || !changes.get(p).hasChanged(p)) {
+                if (!p.isAttached() || changes.get(p) == null || !changes.get(p).hasChanged(p)) {
                     // Again, we're skipping no-ops
                     break;
                 }
                 if (participants.contains(p)) {
                     topParent = p;
                     break;
-                } else if (reportIfNotTop(p)) {
+                } else if (isWallpaper(p)) {
+                    skipIntermediateReports = true;
+                } else if (reportIfNotTop(p) && !skipIntermediateReports) {
                     tmpList.add(p);
                 }
             }
@@ -707,12 +1115,21 @@
     }
 
     /**
+     * A ready group is defined by a root window-container where all transitioning windows under
+     * it are expected to animate together as a group. At the moment, this treats each display as
+     * a ready-group to match the existing legacy transition behavior.
+     */
+    private static boolean isReadyGroup(WindowContainer wc) {
+        return wc instanceof DisplayContent;
+    }
+
+    /**
      * Construct a TransitionInfo object from a set of targets and changes. Also populates the
      * root surface.
      */
     @VisibleForTesting
     @NonNull
-    static TransitionInfo calculateTransitionInfo(int type, int flags,
+    static TransitionInfo calculateTransitionInfo(@TransitionType int type, int flags,
             ArraySet<WindowContainer> targets, ArrayMap<WindowContainer, ChangeInfo> changes) {
         final TransitionInfo out = new TransitionInfo(type, flags);
 
@@ -723,17 +1140,11 @@
         }
 
         // Find the top-most shared ancestor of app targets
-        WindowContainer ancestor = null;
-        for (int i = appTargets.size() - 1; i >= 0; --i) {
-            final WindowContainer wc = appTargets.valueAt(i);
-            ancestor = wc;
-            break;
-        }
-        if (ancestor == null) {
+        if (appTargets.isEmpty()) {
             out.setRootLeash(new SurfaceControl(), 0, 0);
             return out;
         }
-        ancestor = ancestor.getParent();
+        WindowContainer ancestor = appTargets.valueAt(appTargets.size() - 1).getParent();
 
         // Go up ancestor parent chain until all targets are descendants.
         ancestorLoop:
@@ -798,6 +1209,10 @@
                 final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo();
                 task.fillTaskInfo(tinfo);
                 change.setTaskInfo(tinfo);
+                change.setRotationAnimation(getTaskRotationAnimation(task));
+                final ActivityRecord topMostActivity = task.getTopMostActivity();
+                change.setAllowEnterPip(topMostActivity != null
+                        && topMostActivity.checkEnterPictureInPictureAppOpsState());
             }
             out.addChange(change);
         }
@@ -805,6 +1220,27 @@
         return out;
     }
 
+    private static int getTaskRotationAnimation(@NonNull Task task) {
+        final ActivityRecord top = task.getTopVisibleActivity();
+        if (top == null) return ROTATION_ANIMATION_UNSPECIFIED;
+        final WindowState mainWin = top.findMainWindow(false);
+        if (mainWin == null) return ROTATION_ANIMATION_UNSPECIFIED;
+        int anim = mainWin.getRotationAnimationHint();
+        if (anim >= 0) return anim;
+        anim = mainWin.getAttrs().rotationAnimation;
+        if (anim != ROTATION_ANIMATION_SEAMLESS) return anim;
+        if (mainWin != task.mDisplayContent.getDisplayPolicy().getTopFullscreenOpaqueWindow()
+                || !top.matchParentBounds()) {
+            // At the moment, we only support seamless rotation if there is only one window showing.
+            return ROTATION_ANIMATION_UNSPECIFIED;
+        }
+        return mainWin.getAttrs().rotationAnimation;
+    }
+
+    boolean getLegacyIsReady() {
+        return mState == STATE_STARTED && mSyncId >= 0 && mSyncEngine.isReady(mSyncId);
+    }
+
     static Transition fromBinder(IBinder binder) {
         return (Transition) binder;
     }
@@ -823,6 +1259,7 @@
         final Rect mAbsoluteBounds = new Rect();
         boolean mShowWallpaper;
         int mRotation = ROTATION_UNDEFINED;
+        @ActivityInfo.Config int mKnownConfigChanges;
 
         ChangeInfo(@NonNull WindowContainer origState) {
             mVisible = origState.isVisibleRequested();
@@ -845,6 +1282,7 @@
             final boolean currVisible = newState.isVisibleRequested();
             if (currVisible == mVisible && !mVisible) return false;
             return currVisible != mVisible
+                    || mKnownConfigChanges != 0
                     // if mWindowingMode is 0, this container wasn't attached at collect time, so
                     // assume no change in windowing-mode.
                     || (mWindowingMode != 0 && newState.getWindowingMode() != mWindowingMode)
@@ -891,9 +1329,19 @@
                     flags |= FLAG_IS_VOICE_INTERACTION;
                 }
             }
+            final DisplayContent dc = wc.asDisplayContent();
+            if (dc != null) {
+                flags |= FLAG_IS_DISPLAY;
+                if (dc.hasAlertWindowSurfaces()) {
+                    flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+                }
+            }
             if (isWallpaper(wc)) {
                 flags |= FLAG_IS_WALLPAPER;
             }
+            if (occludesKeyguard(wc)) {
+                flags |= FLAG_OCCLUDES_KEYGUARD;
+            }
             return flags;
         }
 
@@ -910,4 +1358,95 @@
             mChildren.addAll(wcs);
         }
     }
+
+    /**
+     * The transition sync mechanism has 2 parts:
+     *   1. Whether all WM operations for a particular transition are "ready" (eg. did the app
+     *      launch or stop or get a new configuration?).
+     *   2. Whether all the windows involved have finished drawing their final-state content.
+     *
+     * A transition animation can play once both parts are complete. This ready-tracker keeps track
+     * of part (1). Currently, WM code assumes that "readiness" (part 1) is grouped. This means that
+     * even if the WM operations in one group are ready, the whole transition itself may not be
+     * ready if there are WM operations still pending in another group. This class helps keep track
+     * of readiness across the multiple groups. Currently, we assume that each display is a group
+     * since that is how it has been until now.
+     */
+    private static class ReadyTracker {
+        private final ArrayMap<WindowContainer, Boolean> mReadyGroups = new ArrayMap<>();
+
+        /**
+         * Ensures that this doesn't report as allReady before it has been used. This is needed
+         * in very niche cases where a transition is a no-op (nothing has been collected) but we
+         * still want to be marked ready (via. setAllReady).
+         */
+        private boolean mUsed = false;
+
+        /**
+         * If true, this overrides all ready groups and reports ready. Used by shell-initiated
+         * transitions via {@link #setAllReady()}.
+         */
+        private boolean mReadyOverride = false;
+
+        /**
+         * Adds a ready-group. Any setReady calls in this subtree will be tracked together. For
+         * now these are only DisplayContents.
+         */
+        void addGroup(WindowContainer wc) {
+            if (mReadyGroups.containsKey(wc)) {
+                Slog.e(TAG, "Trying to add a ready-group twice: " + wc);
+                return;
+            }
+            mReadyGroups.put(wc, false);
+        }
+
+        /**
+         * Sets a group's ready state.
+         * @param wc Any container within a group's subtree. Used to identify the ready-group.
+         */
+        void setReadyFrom(WindowContainer wc, boolean ready) {
+            mUsed = true;
+            WindowContainer current = wc;
+            while (current != null) {
+                if (isReadyGroup(current)) {
+                    mReadyGroups.put(current, ready);
+                    ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Setting Ready-group to"
+                            + " %b. group=%s from %s", ready, current, wc);
+                    break;
+                }
+                current = current.getParent();
+            }
+        }
+
+        /** Marks this as ready regardless of individual groups. */
+        void setAllReady() {
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Setting allReady override");
+            mUsed = true;
+            mReadyOverride = true;
+        }
+
+        /** @return true if all tracked subtrees are ready. */
+        boolean allReady() {
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " allReady query: used=%b "
+                    + "override=%b states=[%s]", mUsed, mReadyOverride, groupsToString());
+            if (!mUsed) return false;
+            if (mReadyOverride) return true;
+            for (int i = mReadyGroups.size() - 1; i >= 0; --i) {
+                final WindowContainer wc = mReadyGroups.keyAt(i);
+                if (!wc.isAttached() || !wc.isVisibleRequested()) continue;
+                if (!mReadyGroups.valueAt(i)) return false;
+            }
+            return true;
+        }
+
+        private String groupsToString() {
+            StringBuilder b = new StringBuilder();
+            for (int i = 0; i < mReadyGroups.size(); ++i) {
+                if (i != 0) b.append(',');
+                b.append(mReadyGroups.keyAt(i)).append(':')
+                        .append(mReadyGroups.valueAt(i));
+            }
+            return b.toString();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index cc63c49..bf4cf5f 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -16,24 +16,38 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.ArrayMap;
 import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
 import android.view.WindowManager;
 import android.window.IRemoteTransition;
+import android.window.ITransitionMetricsReporter;
 import android.window.ITransitionPlayer;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
 
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
 
 import java.util.ArrayList;
+import java.util.function.LongConsumer;
 
 /**
  * Handles all the aspects of recording and synchronizing transitions.
@@ -41,8 +55,19 @@
 class TransitionController {
     private static final String TAG = "TransitionController";
 
+    // State constants to line-up with legacy app-transition proto expectations.
+    private static final int LEGACY_STATE_IDLE = 0;
+    private static final int LEGACY_STATE_READY = 1;
+    private static final int LEGACY_STATE_RUNNING = 2;
+
     private ITransitionPlayer mTransitionPlayer;
+    final TransitionMetricsReporter mTransitionMetricsReporter = new TransitionMetricsReporter();
+
     final ActivityTaskManagerService mAtm;
+    final TaskSnapshotController mTaskSnapshotController;
+
+    private final ArrayList<WindowManagerInternal.AppTransitionListener> mLegacyListeners =
+            new ArrayList<>();
 
     /**
      * Currently playing transitions (in the order they were started). When finished, records are
@@ -50,20 +75,32 @@
      */
     private final ArrayList<Transition> mPlayingTransitions = new ArrayList<>();
 
-    private final IBinder.DeathRecipient mTransitionPlayerDeath = () -> {
-        // clean-up/finish any playing transitions.
-        for (int i = 0; i < mPlayingTransitions.size(); ++i) {
-            mPlayingTransitions.get(i).cleanUpOnFailure();
-        }
-        mPlayingTransitions.clear();
-        mTransitionPlayer = null;
-    };
+    final Lock mRunningLock = new Lock();
+
+    private final IBinder.DeathRecipient mTransitionPlayerDeath;
 
     /** The transition currently being constructed (collecting participants). */
     private Transition mCollectingTransition = null;
 
-    TransitionController(ActivityTaskManagerService atm) {
+    // TODO(b/188595497): remove when not needed.
+    final StatusBarManagerInternal mStatusBar;
+
+    TransitionController(ActivityTaskManagerService atm,
+            TaskSnapshotController taskSnapshotController) {
         mAtm = atm;
+        mStatusBar = LocalServices.getService(StatusBarManagerInternal.class);
+        mTaskSnapshotController = taskSnapshotController;
+        mTransitionPlayerDeath = () -> {
+            synchronized (mAtm.mGlobalLock) {
+                // Clean-up/finish any playing transitions.
+                for (int i = 0; i < mPlayingTransitions.size(); ++i) {
+                    mPlayingTransitions.get(i).cleanUpOnFailure();
+                }
+                mPlayingTransitions.clear();
+                mTransitionPlayer = null;
+                mRunningLock.doNotifyLocked();
+            }
+        };
     }
 
     /** @see #createTransition(int, int) */
@@ -76,7 +113,7 @@
      * Creates a transition. It can immediately collect participants.
      */
     @NonNull
-    Transition createTransition(@WindowManager.TransitionType int type,
+    private Transition createTransition(@WindowManager.TransitionType int type,
             @WindowManager.TransitionFlags int flags) {
         if (mTransitionPlayer == null) {
             throw new IllegalStateException("Shell Transitions not enabled");
@@ -87,16 +124,22 @@
         mCollectingTransition = new Transition(type, flags, this, mAtm.mWindowManager.mSyncEngine);
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Creating Transition: %s",
                 mCollectingTransition);
+        dispatchLegacyAppTransitionPending();
         return mCollectingTransition;
     }
 
     void registerTransitionPlayer(@Nullable ITransitionPlayer player) {
         try {
+            // Note: asBinder() can be null if player is same process (likely in a test).
             if (mTransitionPlayer != null) {
-                mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
+                if (mTransitionPlayer.asBinder() != null) {
+                    mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
+                }
                 mTransitionPlayer = null;
             }
-            player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
+            if (player.asBinder() != null) {
+                player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
+            }
             mTransitionPlayer = player;
         } catch (RemoteException e) {
             throw new RuntimeException("Unable to set transition player");
@@ -155,12 +198,22 @@
     }
 
     /**
-     * @see #requestTransitionIfNeeded(int, int, WindowContainer, IRemoteTransition)
+     * @return {@code true} if {@param ar} is part of a transient-launch activity in an active
+     * transition.
      */
-    @Nullable
-    Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
-            @Nullable WindowContainer trigger) {
-        return requestTransitionIfNeeded(type, 0 /* flags */, trigger);
+    boolean isTransientLaunch(@NonNull ActivityRecord ar) {
+        if (mCollectingTransition != null && mCollectingTransition.isTransientLaunch(ar)) {
+            return true;
+        }
+        for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+            if (mPlayingTransitions.get(i).isTransientLaunch(ar)) return true;
+        }
+        return false;
+    }
+
+    @WindowManager.TransitionType
+    int getCollectingTransitionType() {
+        return mCollectingTransition != null ? mCollectingTransition.mType : TRANSIT_NONE;
     }
 
     /**
@@ -168,8 +221,19 @@
      */
     @Nullable
     Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
-            @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger) {
-        return requestTransitionIfNeeded(type, flags, trigger, null /* remote */);
+            @NonNull WindowContainer trigger) {
+        return requestTransitionIfNeeded(type, 0 /* flags */, trigger, trigger /* readyGroupRef */);
+    }
+
+    /**
+     * @see #requestTransitionIfNeeded(int, int, WindowContainer, IRemoteTransition)
+     */
+    @Nullable
+    Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
+            @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger,
+            @NonNull WindowContainer readyGroupRef) {
+        return requestTransitionIfNeeded(type, flags, trigger, readyGroupRef,
+                null /* remoteTransition */);
     }
 
     private static boolean isExistenceType(@WindowManager.TransitionType int type) {
@@ -180,19 +244,24 @@
      * If a transition isn't requested yet, creates one and asks the TransitionPlayer (Shell) to
      * start it. Collection can start immediately.
      * @param trigger if non-null, this is the first container that will be collected
+     * @param readyGroupRef Used to identify which ready-group this request is for.
      * @return the created transition if created or null otherwise.
      */
     @Nullable
     Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
             @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger,
-            @Nullable IRemoteTransition remoteTransition) {
+            @NonNull WindowContainer readyGroupRef, @Nullable RemoteTransition remoteTransition) {
         if (mTransitionPlayer == null) {
             return null;
         }
         Transition newTransition = null;
         if (isCollecting()) {
             // Make the collecting transition wait until this request is ready.
-            mCollectingTransition.setReady(false);
+            mCollectingTransition.setReady(readyGroupRef, false);
+            if ((flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) {
+                // Add keyguard flag to dismiss keyguard
+                mCollectingTransition.addFlag(flags);
+            }
         } else {
             newTransition = requestStartTransition(createTransition(type, flags),
                     trigger != null ? trigger.asTask() : null, remoteTransition);
@@ -210,7 +279,7 @@
     /** Asks the transition player (shell) to start a created but not yet started transition. */
     @NonNull
     Transition requestStartTransition(@NonNull Transition transition, @Nullable Task startTask,
-            @Nullable IRemoteTransition remoteTransition) {
+            @Nullable RemoteTransition remoteTransition) {
         try {
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                     "Requesting StartTransition: %s", transition);
@@ -240,19 +309,28 @@
         mCollectingTransition.collectExistenceChange(wc);
     }
 
-    /** @see Transition#setReady */
-    void setReady(boolean ready) {
+    /** @see Transition#setOverrideAnimation */
+    void setOverrideAnimation(TransitionInfo.AnimationOptions options,
+            @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) {
         if (mCollectingTransition == null) return;
-        mCollectingTransition.setReady(ready);
+        mCollectingTransition.setOverrideAnimation(options, startCallback, finishCallback);
     }
 
     /** @see Transition#setReady */
-    void setReady() {
-        setReady(true);
+    void setReady(WindowContainer wc, boolean ready) {
+        if (mCollectingTransition == null) return;
+        mCollectingTransition.setReady(wc, ready);
+    }
+
+    /** @see Transition#setReady */
+    void setReady(WindowContainer wc) {
+        setReady(wc, true);
     }
 
     /** @see Transition#finishTransition */
     void finishTransition(@NonNull IBinder token) {
+        // It is usually a no-op but make sure that the metric consumer is removed.
+        mTransitionMetricsReporter.reportAnimationStart(token, 0 /* startTime */);
         final Transition record = Transition.fromBinder(token);
         if (record == null || !mPlayingTransitions.contains(record)) {
             Slog.e(TAG, "Trying to finish a non-playing transition " + token);
@@ -261,6 +339,7 @@
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record);
         mPlayingTransitions.remove(record);
         record.finishTransition();
+        mRunningLock.doNotifyLocked();
     }
 
     void moveToPlaying(Transition transition) {
@@ -279,4 +358,133 @@
         mCollectingTransition = null;
     }
 
+    /**
+     * Record that the launch of {@param activity} is transient (meaning its lifecycle is currently
+     * tied to the transition).
+     */
+    void setTransientLaunch(@NonNull ActivityRecord activity) {
+        if (mCollectingTransition == null) return;
+        mCollectingTransition.setTransientLaunch(activity);
+
+        // TODO(b/188669821): Remove once legacy recents behavior is moved to shell.
+        // Also interpret HOME transient launch as recents
+        if (activity.getActivityType() == ACTIVITY_TYPE_HOME) {
+            mCollectingTransition.addFlag(TRANSIT_FLAG_IS_RECENTS);
+        }
+    }
+
+    void legacyDetachNavigationBarFromApp(@NonNull IBinder token) {
+        final Transition transition = Transition.fromBinder(token);
+        if (transition == null || !mPlayingTransitions.contains(transition)) {
+            Slog.e(TAG, "Transition isn't playing: " + token);
+            return;
+        }
+        transition.legacyRestoreNavigationBarFromApp();
+    }
+
+    void registerLegacyListener(WindowManagerInternal.AppTransitionListener listener) {
+        mLegacyListeners.add(listener);
+    }
+
+    void dispatchLegacyAppTransitionPending() {
+        for (int i = 0; i < mLegacyListeners.size(); ++i) {
+            mLegacyListeners.get(i).onAppTransitionPendingLocked();
+        }
+    }
+
+    void dispatchLegacyAppTransitionStarting(TransitionInfo info) {
+        final boolean keyguardGoingAway = info.isKeyguardGoingAway();
+        for (int i = 0; i < mLegacyListeners.size(); ++i) {
+            // TODO(shell-transitions): handle (un)occlude transition.
+            mLegacyListeners.get(i).onAppTransitionStartingLocked(keyguardGoingAway,
+                    false /* keyguardOcclude */, 0 /* durationHint */,
+                    SystemClock.uptimeMillis(), AnimationAdapter.STATUS_BAR_TRANSITION_DURATION);
+        }
+    }
+
+    void dispatchLegacyAppTransitionFinished(ActivityRecord ar) {
+        for (int i = 0; i < mLegacyListeners.size(); ++i) {
+            mLegacyListeners.get(i).onAppTransitionFinishedLocked(ar.token);
+        }
+    }
+
+    void dispatchLegacyAppTransitionCancelled() {
+        for (int i = 0; i < mLegacyListeners.size(); ++i) {
+            mLegacyListeners.get(i).onAppTransitionCancelledLocked(
+                    false /* keyguardGoingAway */);
+        }
+    }
+
+    void dumpDebugLegacy(ProtoOutputStream proto, long fieldId) {
+        final long token = proto.start(fieldId);
+        int state = LEGACY_STATE_IDLE;
+        if (!mPlayingTransitions.isEmpty()) {
+            state = LEGACY_STATE_RUNNING;
+        } else if (mCollectingTransition != null && mCollectingTransition.getLegacyIsReady()) {
+            state = LEGACY_STATE_READY;
+        }
+        proto.write(AppTransitionProto.APP_TRANSITION_STATE, state);
+        proto.end(token);
+    }
+
+    static class TransitionMetricsReporter extends ITransitionMetricsReporter.Stub {
+        private final ArrayMap<IBinder, LongConsumer> mMetricConsumers = new ArrayMap<>();
+
+        void associate(IBinder transitionToken, LongConsumer consumer) {
+            synchronized (mMetricConsumers) {
+                mMetricConsumers.put(transitionToken, consumer);
+            }
+        }
+
+        @Override
+        public void reportAnimationStart(IBinder transitionToken, long startTime) {
+            final LongConsumer c;
+            synchronized (mMetricConsumers) {
+                if (mMetricConsumers.isEmpty()) return;
+                c = mMetricConsumers.remove(transitionToken);
+            }
+            if (c != null) {
+                c.accept(startTime);
+            }
+        }
+    }
+
+    class Lock {
+        private int mTransitionWaiters = 0;
+        void runWhenIdle(long timeout, Runnable r) {
+            synchronized (mAtm.mGlobalLock) {
+                if (!inTransition()) {
+                    r.run();
+                    return;
+                }
+                mTransitionWaiters += 1;
+            }
+            final long startTime = SystemClock.uptimeMillis();
+            final long endTime = startTime + timeout;
+            while (true) {
+                synchronized (mAtm.mGlobalLock) {
+                    if (!inTransition() || SystemClock.uptimeMillis() > endTime) {
+                        mTransitionWaiters -= 1;
+                        r.run();
+                        return;
+                    }
+                }
+                synchronized (this) {
+                    try {
+                        this.wait(timeout);
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+                }
+            }
+        }
+
+        void doNotifyLocked() {
+            synchronized (this) {
+                if (mTransitionWaiters > 0) {
+                    this.notifyAll();
+                }
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java b/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java
index 416b9df..2652723 100644
--- a/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java
@@ -20,6 +20,7 @@
 import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 
+import android.annotation.NonNull;
 import android.graphics.Point;
 import android.os.SystemClock;
 import android.util.proto.ProtoOutputStream;
@@ -64,18 +65,17 @@
      *
      * @return RemoteAnimationTarget[] targets for all the visible wallpaper windows
      */
-    public static RemoteAnimationTarget[] startWallpaperAnimations(WindowManagerService service,
+    public static RemoteAnimationTarget[] startWallpaperAnimations(DisplayContent displayContent,
             long durationHint, long statusBarTransitionDelay,
             Consumer<WallpaperAnimationAdapter> animationCanceledRunnable,
             ArrayList<WallpaperAnimationAdapter> adaptersOut) {
+        if (!displayContent.mWallpaperController.isWallpaperVisible()) {
+            ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS,
+                    "\tWallpaper of display=%s is not visible", displayContent);
+            return new RemoteAnimationTarget[0];
+        }
         final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
-        service.mRoot.forAllWallpaperWindows(wallpaperWindow -> {
-            if (!wallpaperWindow.getDisplayContent().mWallpaperController.isWallpaperVisible()) {
-                ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tNot visible=%s", wallpaperWindow);
-                return;
-            }
-
-            ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tvisible=%s", wallpaperWindow);
+        displayContent.forAllWallpaperWindows(wallpaperWindow -> {
             final WallpaperAnimationAdapter wallpaperAdapter = new WallpaperAnimationAdapter(
                     wallpaperWindow, durationHint, statusBarTransitionDelay,
                     animationCanceledRunnable);
@@ -93,7 +93,7 @@
     RemoteAnimationTarget createRemoteAnimationTarget() {
         mTarget = new RemoteAnimationTarget(-1, -1, getLeash(), false, null, null,
                 mWallpaperToken.getPrefixOrderIndex(), new Point(), null, null,
-                mWallpaperToken.getWindowConfiguration(), true, null, null, null);
+                mWallpaperToken.getWindowConfiguration(), true, null, null, null, false);
         return mTarget;
     }
 
@@ -134,7 +134,8 @@
 
     @Override
     public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-            @AnimationType int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+            @AnimationType int type,
+            @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation");
 
         // Restore z-layering until client has a chance to modify it.
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 4ff6d3c..d93b649 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -16,18 +16,20 @@
 
 package com.android.server.wm;
 
+import static android.app.WallpaperManager.COMMAND_FREEZE;
+import static android.app.WallpaperManager.COMMAND_UNFREEZE;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
 
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.WindowManagerService.H.WALLPAPER_DRAW_PENDING_TIMEOUT;
@@ -47,6 +49,8 @@
 import android.view.animation.Animation;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ToBooleanFunction;
 
 import java.io.PrintWriter;
@@ -79,6 +83,8 @@
     private int mLastWallpaperDisplayOffsetX = Integer.MIN_VALUE;
     private int mLastWallpaperDisplayOffsetY = Integer.MIN_VALUE;
     private final float mMaxWallpaperScale;
+    // Whether COMMAND_FREEZE was dispatched.
+    private boolean mLastFrozen = false;
 
     // This is set when we are waiting for a wallpaper to tell us it is done
     // changing its scroll position.
@@ -124,7 +130,7 @@
         }
 
         mFindResults.resetTopWallpaper = true;
-        if (mService.mAtmService.getTransitionController().getTransitionPlayer() == null) {
+        if (!w.mTransitionController.isShellTransitionsEnabled()) {
             if (w.mActivityRecord != null && !w.mActivityRecord.isVisible()
                     && !w.mActivityRecord.isAnimating(TRANSITION | PARENTS)) {
                 // If this window's app token is hidden and not animating, it is of no interest.
@@ -194,6 +200,7 @@
                 if (DEBUG_WALLPAPER) Slog.v(TAG,
                         "Win " + w + ": token animating, looking behind.");
             }
+            mFindResults.setIsWallpaperTargetForLetterbox(w.hasWallpaperForLetterboxBackground());
             // Found a target! End search.
             return true;
         }
@@ -286,10 +293,11 @@
         for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) {
             final WallpaperWindowToken token = mWallpaperTokens.get(i);
             token.setVisibility(false);
-            if (DEBUG_WALLPAPER_LIGHT && token.isVisible()) {
-                Slog.d(TAG, "Hiding wallpaper " + token
-                        + " from " + winGoingAway + " target=" + mWallpaperTarget + " prev="
-                        + mPrevWallpaperTarget + "\n" + Debug.getCallers(5, "  "));
+            if (ProtoLogImpl.isEnabled(WM_DEBUG_WALLPAPER) && token.isVisible()) {
+                ProtoLog.d(WM_DEBUG_WALLPAPER,
+                        "Hiding wallpaper %s from %s target=%s prev=%s callers=%s",
+                        token, winGoingAway, mWallpaperTarget, mPrevWallpaperTarget,
+                        Debug.getCallers(5));
             }
         }
     }
@@ -424,20 +432,25 @@
     Bundle sendWindowWallpaperCommand(
             WindowState window, String action, int x, int y, int z, Bundle extras, boolean sync) {
         if (window == mWallpaperTarget || window == mPrevWallpaperTarget) {
-            boolean doWait = sync;
-            for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
-                final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx);
-                token.sendWindowWallpaperCommand(action, x, y, z, extras, sync);
-            }
-
-            if (doWait) {
-                // TODO: Need to wait for result.
-            }
+            sendWindowWallpaperCommand(action, x, y, z, extras, sync);
         }
 
         return null;
     }
 
+    private void sendWindowWallpaperCommand(
+                String action, int x, int y, int z, Bundle extras, boolean sync) {
+        boolean doWait = sync;
+        for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
+            final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx);
+            token.sendWindowWallpaperCommand(action, x, y, z, extras, sync);
+        }
+
+        if (doWait) {
+            // TODO: Need to wait for result.
+        }
+    }
+
     private void updateWallpaperOffsetLocked(WindowState changingTarget, boolean sync) {
         WindowState target = mWallpaperTarget;
         if (target != null) {
@@ -534,15 +547,15 @@
 
             // Is it time to stop animating?
             if (!mPrevWallpaperTarget.isAnimatingLw()) {
-                if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "No longer animating wallpaper targets!");
+                ProtoLog.v(WM_DEBUG_WALLPAPER, "No longer animating wallpaper targets!");
                 mPrevWallpaperTarget = null;
                 mWallpaperTarget = wallpaperTarget;
             }
             return;
         }
 
-        if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG,
-                "New wallpaper target: " + wallpaperTarget + " prevTarget: " + mWallpaperTarget);
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "New wallpaper target: %s prevTarget: %s caller=%s",
+                wallpaperTarget, mWallpaperTarget, Debug.getCallers(5));
 
         mPrevWallpaperTarget = null;
 
@@ -560,8 +573,8 @@
         // then we are in our super special mode!
         boolean oldAnim = prevWallpaperTarget.isAnimatingLw();
         boolean foundAnim = wallpaperTarget.isAnimatingLw();
-        if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG,
-                "New animation: " + foundAnim + " old animation: " + oldAnim);
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "New animation: %s old animation: %s",
+                foundAnim, oldAnim);
 
         if (!foundAnim || !oldAnim) {
             return;
@@ -576,14 +589,14 @@
         final boolean oldTargetHidden = prevWallpaperTarget.mActivityRecord != null
                 && !prevWallpaperTarget.mActivityRecord.mVisibleRequested;
 
-        if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "Animating wallpapers:" + " old: "
-                + prevWallpaperTarget + " hidden=" + oldTargetHidden + " new: " + wallpaperTarget
-                + " hidden=" + newTargetHidden);
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "Animating wallpapers: "
+                + "old: %s hidden=%b new: %s hidden=%b",
+                prevWallpaperTarget, oldTargetHidden, wallpaperTarget, newTargetHidden);
 
         mPrevWallpaperTarget = prevWallpaperTarget;
 
         if (newTargetHidden && !oldTargetHidden) {
-            if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "Old wallpaper still the target.");
+            ProtoLog.v(WM_DEBUG_WALLPAPER, "Old wallpaper still the target.");
             // Use the old target if new target is hidden but old target
             // is not. If they're both hidden, still use the new target.
             mWallpaperTarget = prevWallpaperTarget;
@@ -644,8 +657,15 @@
 
         updateWallpaperTokens(visible);
 
-        if (DEBUG_WALLPAPER_LIGHT)  Slog.d(TAG, "New wallpaper: target=" + mWallpaperTarget
-                + " prev=" + mPrevWallpaperTarget);
+        if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) {
+            mLastFrozen = mFindResults.isWallpaperTargetForLetterbox;
+            sendWindowWallpaperCommand(
+                    mFindResults.isWallpaperTargetForLetterbox ? COMMAND_FREEZE : COMMAND_UNFREEZE,
+                    /* x= */ 0, /* y= */ 0, /* z= */ 0, /* extras= */ null, /* sync= */ false);
+        }
+
+        ProtoLog.d(WM_DEBUG_WALLPAPER, "New wallpaper: target=%s prev=%s",
+                mWallpaperTarget, mPrevWallpaperTarget);
     }
 
     boolean processWallpaperDrawPendingTimeout() {
@@ -781,6 +801,18 @@
                 wallpaperBuffer.getHardwareBuffer(), wallpaperBuffer.getColorSpace());
     }
 
+    /**
+     * Mirrors the visible wallpaper if it's available.
+     *
+     * @return A SurfaceControl for the parent of the mirrored wallpaper.
+     */
+    SurfaceControl mirrorWallpaperSurface() {
+        final WindowState wallpaperWindowState = getTopVisibleWallpaper();
+        return wallpaperWindowState != null
+                ? SurfaceControl.mirrorSurface(wallpaperWindowState.getSurfaceControl())
+                : null;
+    }
+
     WindowState getTopVisibleWallpaper() {
         mTmpTopWallpaper = null;
 
@@ -841,6 +873,7 @@
         boolean useTopWallpaperAsTarget = false;
         WindowState wallpaperTarget = null;
         boolean resetTopWallpaper = false;
+        boolean isWallpaperTargetForLetterbox = false;
 
         void setTopWallpaper(WindowState win) {
             topWallpaper = win;
@@ -854,11 +887,16 @@
             useTopWallpaperAsTarget = topWallpaperAsTarget;
         }
 
+        void setIsWallpaperTargetForLetterbox(boolean isWallpaperTargetForLetterbox) {
+            this.isWallpaperTargetForLetterbox = isWallpaperTargetForLetterbox;
+        }
+
         void reset() {
             topWallpaper = null;
             wallpaperTarget = null;
             useTopWallpaperAsTarget = false;
             resetTopWallpaper = false;
+            isWallpaperTargetForLetterbox = false;
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index b54e8b7..3a639f5 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -20,7 +20,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
@@ -28,7 +28,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Slog;
 import android.view.DisplayInfo;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -107,12 +106,12 @@
 
     void updateWallpaperWindows(boolean visible) {
         if (isVisible() != visible) {
-            if (DEBUG_WALLPAPER_LIGHT) Slog.d(TAG,
-                    "Wallpaper token " + token + " visible=" + visible);
+            ProtoLog.d(WM_DEBUG_WALLPAPER, "Wallpaper token %s visible=%b",
+                    token, visible);
             setVisibility(visible);
         }
         final WallpaperController wallpaperController = mDisplayContent.mWallpaperController;
-        if (mWmService.mAtmService.getTransitionController().getTransitionPlayer() != null) {
+        if (mTransitionController.isShellTransitionsEnabled()) {
             return;
         }
 
@@ -157,12 +156,12 @@
      */
     void setVisibility(boolean visible) {
         // Before setting mVisibleRequested so we can track changes.
-        mWmService.mAtmService.getTransitionController().collect(this);
+        mTransitionController.collect(this);
 
         setVisibleRequested(visible);
 
         // If in a transition, defer commits for activities that are going invisible
-        if (!visible && (mWmService.mAtmService.getTransitionController().inTransition()
+        if (!visible && (mTransitionController.inTransition()
                 || getDisplayContent().mAppTransition.isRunning())) {
             return;
         }
diff --git a/services/core/java/com/android/server/wm/Watermark.java b/services/core/java/com/android/server/wm/Watermark.java
index 66ab094..9780d33 100644
--- a/services/core/java/com/android/server/wm/Watermark.java
+++ b/services/core/java/com/android/server/wm/Watermark.java
@@ -31,6 +31,7 @@
 import android.view.Surface;
 import android.view.Surface.OutOfResourcesException;
 import android.view.SurfaceControl;
+import android.view.WindowManagerPolicyConstants;
 
 /**
  * Displays a watermark on top of the window manager's windows.
@@ -117,7 +118,7 @@
                     .setFormat(PixelFormat.TRANSLUCENT)
                     .setCallsite(TITLE)
                     .build();
-            t.setLayer(ctrl, WindowManagerService.TYPE_LAYER_MULTIPLIER * 100)
+            t.setLayer(ctrl, WindowManagerPolicyConstants.WATERMARK_LAYER)
                     .setPosition(ctrl, 0, 0)
                     .show(ctrl);
             // Ensure we aren't considered as obscuring for Input purposes.
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index eb32486..4a43f4f 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -164,7 +164,7 @@
                 final DisplayContent dc = root.getDisplayContent(displayId);
 
                 dc.checkAppWindowsReadyToShow();
-                if (accessibilityController != null) {
+                if (accessibilityController.hasCallbacks()) {
                     accessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId,
                             mTransaction);
                 }
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index d99aed1..51ecce0 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
@@ -30,6 +31,7 @@
 import static android.os.UserHandle.USER_NULL;
 import static android.view.SurfaceControl.Transaction;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_BACK;
@@ -55,6 +57,7 @@
 import static com.android.server.wm.WindowContainerProto.IDENTIFIER;
 import static com.android.server.wm.WindowContainerProto.ORIENTATION;
 import static com.android.server.wm.WindowContainerProto.SURFACE_ANIMATOR;
+import static com.android.server.wm.WindowContainerProto.SURFACE_CONTROL;
 import static com.android.server.wm.WindowContainerProto.VISIBLE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -68,7 +71,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
-import android.app.WindowConfiguration;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
@@ -109,6 +111,7 @@
 import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -125,26 +128,6 @@
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowContainer" : TAG_WM;
 
-    /** Animation layer that happens above all animating {@link Task}s. */
-    static final int ANIMATION_LAYER_STANDARD = 0;
-
-    /** Animation layer that happens above all {@link Task}s. */
-    static final int ANIMATION_LAYER_BOOSTED = 1;
-
-    /**
-     * Animation layer that is reserved for {@link WindowConfiguration#ACTIVITY_TYPE_HOME}
-     * activities and all activities that are being controlled by the recents animation. This
-     * layer is generally below all {@link Task}s.
-     */
-    static final int ANIMATION_LAYER_HOME = 2;
-
-    @IntDef(prefix = { "ANIMATION_LAYER_" }, value = {
-            ANIMATION_LAYER_STANDARD,
-            ANIMATION_LAYER_BOOSTED,
-            ANIMATION_LAYER_HOME,
-    })
-    @interface AnimationLayer {}
-
     static final int POSITION_TOP = Integer.MAX_VALUE;
     static final int POSITION_BOTTOM = Integer.MIN_VALUE;
 
@@ -195,10 +178,14 @@
      * Applied as part of the animation pass in "prepareSurfaces".
      */
     protected final SurfaceAnimator mSurfaceAnimator;
-    private boolean mAnyParentAnimating;
+
+    /** The parent leash added for animation. */
+    @Nullable
+    private SurfaceControl mAnimationLeash;
 
     final SurfaceFreezer mSurfaceFreezer;
     protected final WindowManagerService mWmService;
+    final TransitionController mTransitionController;
 
     /**
      * Sources which triggered a surface animation on this container. An animation target can be
@@ -329,6 +316,7 @@
 
     WindowContainer(WindowManagerService wms) {
         mWmService = wms;
+        mTransitionController = mWmService.mAtmService.getTransitionController();
         mPendingTransaction = wms.mTransactionFactory.get();
         mSyncTransaction = wms.mTransactionFactory.get();
         mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished, wms);
@@ -362,6 +350,10 @@
             throw new IllegalArgumentException("reparent: can't reparent to null " + this);
         }
 
+        if (newParent == this) {
+            throw new IllegalArgumentException("Can not reparent to itself " + this);
+        }
+
         final WindowContainer oldParent = mParent;
         if (mParent == newParent) {
             throw new IllegalArgumentException("WC=" + this + " already child of " + mParent);
@@ -865,7 +857,8 @@
     }
 
     boolean isAttached() {
-        return getDisplayArea() != null;
+        WindowContainer parent = getParent();
+        return parent != null && parent.isAttached();
     }
 
     void setWaitingForDrawnIfResizingChanged() {
@@ -1000,6 +993,10 @@
         return mDisplayContent != null && mDisplayContent.mChangingContainers.contains(this);
     }
 
+    boolean inTransition() {
+        return mTransitionController.inTransition(this);
+    }
+
     void sendAppVisibilityToClients() {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             final WindowContainer wc = mChildren.get(i);
@@ -1389,6 +1386,7 @@
         }
     }
 
+    /** Returns whether the window should be shown for current user. */
     boolean showToCurrentUser() {
         return true;
     }
@@ -1685,6 +1683,15 @@
         return false;
     }
 
+    boolean forAllLeafTaskFragments(Function<TaskFragment, Boolean> callback) {
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            if (mChildren.get(i).forAllLeafTaskFragments(callback)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * For all root tasks at or below this container call the callback.
      *
@@ -1740,6 +1747,28 @@
         }
     }
 
+    /**
+     * For all task fragments at or below this container call the callback.
+     *
+     * @param callback Callback to be called for every task.
+     */
+    void forAllTaskFragments(Consumer<TaskFragment> callback) {
+        forAllTaskFragments(callback, true /*traverseTopToBottom*/);
+    }
+
+    void forAllTaskFragments(Consumer<TaskFragment> callback, boolean traverseTopToBottom) {
+        final int count = mChildren.size();
+        if (traverseTopToBottom) {
+            for (int i = count - 1; i >= 0; --i) {
+                mChildren.get(i).forAllTaskFragments(callback, traverseTopToBottom);
+            }
+        } else {
+            for (int i = 0; i < count; i++) {
+                mChildren.get(i).forAllTaskFragments(callback, traverseTopToBottom);
+            }
+        }
+    }
+
     void forAllLeafTasks(Consumer<Task> callback, boolean traverseTopToBottom) {
         final int count = mChildren.size();
         if (traverseTopToBottom) {
@@ -1753,6 +1782,19 @@
         }
     }
 
+    void forAllLeafTaskFragments(Consumer<TaskFragment> callback, boolean traverseTopToBottom) {
+        final int count = mChildren.size();
+        if (traverseTopToBottom) {
+            for (int i = count - 1; i >= 0; --i) {
+                mChildren.get(i).forAllLeafTaskFragments(callback, traverseTopToBottom);
+            }
+        } else {
+            for (int i = 0; i < count; i++) {
+                mChildren.get(i).forAllLeafTaskFragments(callback, traverseTopToBottom);
+            }
+        }
+    }
+
     /**
      * For all root tasks at or below this container call the callback.
      *
@@ -2254,7 +2296,7 @@
     void assignLayer(Transaction t, int layer) {
         // Don't assign layers while a transition animation is playing
         // TODO(b/173528115): establish robust best-practices around z-order fighting.
-        if (mWmService.mAtmService.getTransitionController().isPlaying()) return;
+        if (mTransitionController.isPlaying()) return;
         final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null;
         if (mSurfaceControl != null && changed) {
             setLayer(t, layer);
@@ -2278,10 +2320,15 @@
     }
 
     protected void setLayer(Transaction t, int layer) {
-
-        // Route through surface animator to accommodate that our surface control might be
-        // attached to the leash, and leash is attached to parent container.
-        mSurfaceAnimator.setLayer(t, layer);
+        if (mSurfaceFreezer.hasLeash()) {
+            // When the freezer has created animation leash parent for the window, set the layer
+            // there instead.
+            mSurfaceFreezer.setLayer(t, layer);
+        } else {
+            // Route through surface animator to accommodate that our surface control might be
+            // attached to the leash, and leash is attached to parent container.
+            mSurfaceAnimator.setLayer(t, layer);
+        }
     }
 
     int getLastLayer() {
@@ -2289,10 +2336,15 @@
     }
 
     protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
-
-        // Route through surface animator to accommodate that our surface control might be
-        // attached to the leash, and leash is attached to parent container.
-        mSurfaceAnimator.setRelativeLayer(t, relativeTo, layer);
+        if (mSurfaceFreezer.hasLeash()) {
+            // When the freezer has created animation leash parent for the window, set the layer
+            // there instead.
+            mSurfaceFreezer.setRelativeLayer(t, relativeTo, layer);
+        } else {
+            // Route through surface animator to accommodate that our surface control might be
+            // attached to the leash, and leash is attached to parent container.
+            mSurfaceAnimator.setRelativeLayer(t, relativeTo, layer);
+        }
     }
 
     protected void reparentSurfaceControl(Transaction t, SurfaceControl newParent) {
@@ -2362,6 +2414,9 @@
         if (mSurfaceAnimator.isAnimating()) {
             mSurfaceAnimator.dumpDebug(proto, SURFACE_ANIMATOR);
         }
+        if (mSurfaceControl != null) {
+            mSurfaceControl.dumpDebug(proto, SURFACE_CONTROL);
+        }
 
         // add children to proto
         for (int i = 0; i < getChildCount(); i++) {
@@ -2511,11 +2566,14 @@
      * @param animationFinishedCallback The callback being triggered when the animation finishes.
      * @param animationCancelledCallback The callback is triggered after the SurfaceAnimator sends a
      *                                   cancel call to the underlying AnimationAdapter.
+     * @param snapshotAnim  The animation to run for the snapshot. {@code null} if there is no
+     *                      snapshot.
      */
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type,
             @Nullable OnAnimationFinishedCallback animationFinishedCallback,
-            @Nullable Runnable animationCancelledCallback) {
+            @Nullable Runnable animationCancelledCallback,
+            @Nullable AnimationAdapter snapshotAnim) {
         if (DEBUG_ANIM) {
             Slog.v(TAG, "Starting animation on " + this + ": type=" + type + ", anim=" + anim);
         }
@@ -2523,14 +2581,14 @@
         // TODO: This should use isVisible() but because isVisible has a really weird meaning at
         // the moment this doesn't work for all animatable window containers.
         mSurfaceAnimator.startAnimation(t, anim, hidden, type, animationFinishedCallback,
-                animationCancelledCallback, mSurfaceFreezer);
+                animationCancelledCallback, snapshotAnim, mSurfaceFreezer);
     }
 
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type,
             @Nullable OnAnimationFinishedCallback animationFinishedCallback) {
         startAnimation(t, anim, hidden, type, animationFinishedCallback,
-                null /* adapterAnimationCancelledCallback */);
+                null /* adapterAnimationCancelledCallback */, null /* snapshotAnim */);
     }
 
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
@@ -2548,13 +2606,47 @@
         mSurfaceFreezer.unfreeze(getPendingTransaction());
     }
 
+    /**
+     * Initializes a change transition. See {@link SurfaceFreezer} for more information.
+     *
+     * For now, this will only be called for the following cases:
+     * 1. {@link Task} is changing windowing mode between fullscreen and freeform.
+     * 2. {@link TaskFragment} is organized and is changing window bounds.
+     * 3. {@link ActivityRecord} is reparented into an organized {@link TaskFragment}. (The
+     *    transition will happen on the {@link TaskFragment} for this case).
+     *
+     * This shouldn't be called on other {@link WindowContainer} unless there is a valid
+     * use case.
+     *
+     * @param startBounds The original bounds (on screen) of the surface we are snapshotting.
+     * @param freezeTarget The surface to take snapshot from. If {@code null}, we will take a
+     *                     snapshot from {@link #getFreezeSnapshotTarget()}.
+     */
+    void initializeChangeTransition(Rect startBounds, @Nullable SurfaceControl freezeTarget) {
+        mDisplayContent.prepareAppTransition(TRANSIT_CHANGE);
+        mDisplayContent.mChangingContainers.add(this);
+        // Calculate the relative position in parent container.
+        final Rect parentBounds = getParent().getBounds();
+        mTmpPoint.set(startBounds.left - parentBounds.left, startBounds.top - parentBounds.top);
+        mSurfaceFreezer.freeze(getSyncTransaction(), startBounds, mTmpPoint, freezeTarget);
+    }
+
+    void initializeChangeTransition(Rect startBounds) {
+        initializeChangeTransition(startBounds, null /* freezeTarget */);
+    }
+
     ArraySet<WindowContainer> getAnimationSources() {
         return mSurfaceAnimationSources;
     }
 
     @Override
     public SurfaceControl getFreezeSnapshotTarget() {
-        return null;
+        // Only allow freezing if this window is in a TRANSIT_CHANGE
+        if (!mDisplayContent.mAppTransition.containsTransitRequest(TRANSIT_CHANGE)
+                || !mDisplayContent.mChangingContainers.contains(this)) {
+            return null;
+        }
+        return getSurfaceControl();
     }
 
     @Override
@@ -2567,17 +2659,6 @@
         return getParentSurfaceControl();
     }
 
-    /**
-     * @return The layer on which all app animations are happening.
-     */
-    SurfaceControl getAppAnimationLayer(@AnimationLayer int animationLayer) {
-        final WindowContainer parent = getParent();
-        if (parent != null) {
-            return parent.getAppAnimationLayer(animationLayer);
-        }
-        return null;
-    }
-
     // TODO: Remove this and use #getBounds() instead once we set an app transition animation
     // on TaskStack.
     Rect getAnimationBounds(int appRootTaskClipMode) {
@@ -2668,6 +2749,11 @@
             final RemoteAnimationController.RemoteAnimationRecord adapters =
                     controller.createRemoteAnimationRecord(this, mTmpPoint, localBounds,
                             screenBounds, (isChanging ? mSurfaceFreezer.mFreezeBounds : null));
+            if (!isChanging) {
+                adapters.setMode(enter
+                        ? RemoteAnimationTarget.MODE_OPENING
+                        : RemoteAnimationTarget.MODE_CLOSING);
+            }
             resultAdapters = new Pair<>(adapters.mAdapter, adapters.mThumbnailAdapter);
         } else if (isChanging) {
             final float durationScale = mWmService.getTransitionAnimationScaleLocked();
@@ -2750,21 +2836,26 @@
                 taskDisplayArea.setBackgroundColor(backgroundColor);
             }
 
+            // Atomic counter to make sure the clearColor callback is only called one.
+            // It will be called twice in the case we cancel the animation without restart
+            // (in that case it will run as the cancel and finished callbacks).
+            final AtomicInteger callbackCounter = new AtomicInteger(0);
+            final Runnable clearBackgroundColorHandler = () -> {
+                if (callbackCounter.getAndIncrement() == 0) {
+                    taskDisplayArea.clearBackgroundColor();
+                }
+            };
+
             final Runnable cleanUpCallback = isSettingBackgroundColor
-                    ? taskDisplayArea::clearBackgroundColor : () -> {};
+                    ? clearBackgroundColorHandler : () -> {};
 
             startAnimation(getPendingTransaction(), adapter, !isVisible(),
-                    ANIMATION_TYPE_APP_TRANSITION,
-                    (type, anim) -> cleanUpCallback.run(),
-                    cleanUpCallback);
+                    ANIMATION_TYPE_APP_TRANSITION, (type, anim) -> cleanUpCallback.run(),
+                    cleanUpCallback, thumbnailAdapter);
 
             if (adapter.getShowWallpaper()) {
                 getDisplayContent().pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
             }
-            if (thumbnailAdapter != null) {
-                mSurfaceFreezer.mSnapshot.startAnimation(getPendingTransaction(),
-                        thumbnailAdapter, ANIMATION_TYPE_APP_TRANSITION, (type, anim) -> { });
-            }
         }
     }
 
@@ -2783,7 +2874,8 @@
                                     boolean isVoiceInteraction) {
         if (isOrganized()
                 // TODO(b/161711458): Clean-up when moved to shell.
-                && getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+                && getWindowingMode() != WINDOWING_MODE_FULLSCREEN
+                && getWindowingMode() != WINDOWING_MODE_FREEFORM) {
             return null;
         }
 
@@ -2846,6 +2938,14 @@
         return false;
     }
 
+    /**
+     * {@code true} to indicate that this container can be a candidate of
+     * {@link AppTransitionController#getAnimationTargets(ArraySet, ArraySet, boolean) animation
+     * target}. */
+    boolean canBeAnimationTarget() {
+        return false;
+    }
+
     boolean okToDisplay() {
         final DisplayContent dc = getDisplayContent();
         return dc != null && dc.okToDisplay();
@@ -2885,6 +2985,7 @@
     @Override
     public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
         mLastLayer = -1;
+        mAnimationLeash = leash;
         reassignLayer(t);
 
         // Leash is now responsible for position, so set our position to 0.
@@ -2894,11 +2995,16 @@
     @Override
     public void onAnimationLeashLost(Transaction t) {
         mLastLayer = -1;
-        mSurfaceFreezer.unfreeze(t);
+        mAnimationLeash = null;
         reassignLayer(t);
         updateSurfacePosition(t);
     }
 
+    @Override
+    public SurfaceControl getAnimationLeash() {
+        return mAnimationLeash;
+    }
+
     private void doAnimationFinished(@AnimationType int type, AnimationAdapter anim) {
         for (int i = 0; i < mSurfaceAnimationSources.size(); ++i) {
             mSurfaceAnimationSources.valueAt(i).onAnimationFinished(type, anim);
@@ -3049,7 +3155,7 @@
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
     void updateSurfacePosition(Transaction t) {
-        if (mSurfaceControl == null || mSurfaceAnimator.hasLeash()) {
+        if (mSurfaceControl == null || mSurfaceAnimator.hasLeash() || mSurfaceFreezer.hasLeash()) {
             return;
         }
 
@@ -3124,6 +3230,11 @@
     }
 
     /** Cheap way of doing cast and instanceof. */
+    TaskFragment asTaskFragment() {
+        return null;
+    }
+
+    /** Cheap way of doing cast and instanceof. */
     WindowToken asWindowToken() {
         return null;
     }
@@ -3166,6 +3277,11 @@
         return false;
     }
 
+    /** @return {@code true} if this is a container for embedded activities or tasks. */
+    boolean isEmbedded() {
+        return false;
+    }
+
     /**
      * @return {@code true} if this container's surface should be shown when it is created.
      */
@@ -3331,6 +3447,20 @@
     }
 
     /**
+     * Special helper to check that all windows are synced (vs just top one). This is only
+     * used to differentiate between starting-window vs full-drawn in activity-metrics reporting.
+     */
+    boolean allSyncFinished() {
+        if (!isVisibleRequested()) return true;
+        if (mSyncState != SYNC_STATE_READY) return false;
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            final WindowContainer child = mChildren.get(i);
+            if (!child.allSyncFinished()) return false;
+        }
+        return true;
+    }
+
+    /**
      * Called during reparent to handle sync state when the hierarchy changes.
      * If this is in a sync group and gets reparented out, it will cancel syncing.
      * If this is not in a sync group and gets parented into one, it will prepare itself.
@@ -3385,6 +3515,29 @@
     }
 
     /**
+     * Forces the receiver container to always use the configuration of the supplier container as
+     * its requested override configuration. It allows to propagate configuration without changing
+     * the relationship between child and parent.
+     */
+    static void overrideConfigurationPropagation(WindowContainer<?> receiver,
+            WindowContainer<?> supplier) {
+        final ConfigurationContainerListener listener = new ConfigurationContainerListener() {
+            @Override
+            public void onMergedOverrideConfigurationChanged(Configuration mergedOverrideConfig) {
+                receiver.onRequestedOverrideConfigurationChanged(supplier.getConfiguration());
+            }
+        };
+        supplier.registerConfigurationChangeListener(listener);
+        receiver.registerWindowContainerListener(new WindowContainerListener() {
+            @Override
+            public void onRemoved() {
+                receiver.unregisterWindowContainerListener(this);
+                supplier.unregisterConfigurationChangeListener(listener);
+            }
+        });
+    }
+
+    /**
      * Returns the {@link WindowManager.LayoutParams.WindowType}.
      */
     @WindowManager.LayoutParams.WindowType int getWindowType() {
diff --git a/services/core/java/com/android/server/wm/WindowFrames.java b/services/core/java/com/android/server/wm/WindowFrames.java
index ffd6d21..baea854 100644
--- a/services/core/java/com/android/server/wm/WindowFrames.java
+++ b/services/core/java/com/android/server/wm/WindowFrames.java
@@ -89,6 +89,11 @@
     final Rect mCompatFrame = new Rect();
 
     /**
+     * {@code true} if the window frame is a simulated frame and attached to a decor window.
+     */
+    boolean mIsSimulatingDecorWindow = false;
+
+    /**
      * Whether the parent frame would have been different if there was no display cutout.
      */
     private boolean mParentFrameWasClippedByDisplayCutout;
diff --git a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
index 0840441..c954700 100644
--- a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
+++ b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
@@ -43,7 +43,6 @@
     static final boolean DEBUG_CONFIGURATION = false;
     static final boolean DEBUG_STARTING_WINDOW_VERBOSE = false;
     static final boolean DEBUG_WALLPAPER = false;
-    static final boolean DEBUG_WALLPAPER_LIGHT = false || DEBUG_WALLPAPER;
     static final boolean DEBUG_DRAG = true;
     static final boolean DEBUG_SCREENSHOT = false;
     static final boolean DEBUG_LAYOUT_REPEATS = false;
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 4fac05c..4e51a17 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -40,6 +40,7 @@
 import com.android.server.policy.WindowManagerPolicy;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * Window manager local system service interface.
@@ -54,17 +55,18 @@
      */
     public interface AccessibilityControllerInternal {
         /**
-         * Enable the accessibility trace logging.
+         * Start tracing for the given logging types.
+         * @param loggingTypeFlags flags of the logging types enabled.
          */
-        void startTrace();
+        void startTrace(long loggingTypeFlags);
 
         /**
-         * Disable the accessibility trace logging.
+         * Disable accessibility tracing for all logging types.
          */
         void stopTrace();
 
         /**
-         * Is trace enabled or not.
+         * Is tracing enabled for any logging type.
          */
         boolean isAccessibilityTracingEnabled();
 
@@ -73,20 +75,23 @@
          *
          * @param where A string to identify this log entry, which can be used to filter/search
          *        through the tracing file.
+         * @param loggingTypeFlags The flags for the logging types this log entry belongs to.
          * @param callingParams The parameters for the method to be logged.
          * @param a11yDump The proto byte array for a11y state when the entry is generated.
          * @param callingUid The calling uid.
          * @param stackTrace The stack trace, null if not needed.
+         * @param ignoreStackEntries The stack entries can be removed
          */
         void logTrace(
-                String where, String callingParams, byte[] a11yDump, int callingUid,
-                StackTraceElement[] stackTrace);
+                String where, long loggingTypeFlags, String callingParams, byte[] a11yDump,
+                int callingUid, StackTraceElement[] stackTrace, Set<String> ignoreStackEntries);
 
         /**
          * Add an accessibility trace entry.
          *
          * @param where A string to identify this log entry, which can be used to filter/search
          *        through the tracing file.
+         * @param loggingTypeFlags The flags for the logging types this log entry belongs to.
          * @param callingParams The parameters for the method to be logged.
          * @param a11yDump The proto byte array for a11y state when the entry is generated.
          * @param callingUid The calling uid.
@@ -94,9 +99,11 @@
          * @param timeStamp The time when the method to be logged is called.
          * @param processId The calling process Id.
          * @param threadId The calling thread Id.
+         * @param ignoreStackEntries The stack entries can be removed
          */
-        void logTrace(String where, String callingParams, byte[] a11yDump, int callingUid,
-                StackTraceElement[] callStack, long timeStamp, int processId, long threadId);
+        void logTrace(String where, long loggingTypeFlags, String callingParams,
+                byte[] a11yDump, int callingUid, StackTraceElement[] callStack, long timeStamp,
+                int processId, long threadId, Set<String> ignoreStackEntries);
     }
 
     /**
@@ -115,6 +122,16 @@
          */
         void onWindowsForAccessibilityChanged(boolean forceSend, int topFocusedDisplayId,
                 IBinder topFocusedWindowToken, @NonNull List<WindowInfo> windows);
+
+        /**
+         * Called when the display is reparented and becomes an embedded
+         * display. The {@link WindowsForAccessibilityCallback} with the given embedded
+         * display will be replaced by the {@link WindowsForAccessibilityCallback}
+         * associated with its parent display at the same time.
+         *
+         * @param embeddedDisplayId The embedded display Id.
+         */
+        void onDisplayReparented(int embeddedDisplayId);
     }
 
     /**
@@ -143,11 +160,11 @@
         void onRectangleOnScreenRequested(int left, int top, int right, int bottom);
 
         /**
-         * Notifies that the rotation changed.
+         * Notifies that the display size is changed when rotation or the
+         * logical display is changed.
          *
-         * @param rotation The current rotation.
          */
-        void onRotationChanged(int rotation);
+        void onDisplaySizeChanged();
 
         /**
          * Notifies that the context of the user changed. For example, an application
@@ -177,7 +194,7 @@
         /**
          * Called when a pending app transition gets cancelled.
          *
-         * @param keyguardGoingAway true if keyguard going away transition transition got cancelled.
+         * @param keyguardGoingAway true if keyguard going away transition got cancelled.
          */
         public void onAppTransitionCancelledLocked(boolean keyguardGoingAway) {}
 
@@ -190,6 +207,7 @@
          * Called when an app transition gets started
          *
          * @param keyguardGoingAway true if keyguard going away transition is started.
+         * @param keyguardOccluding true if keyguard (un)occlude transition is started.
          * @param duration the total duration of the transition
          * @param statusBarAnimationStartTime the desired start time for all visual animations in
          *        the status bar caused by this app transition in uptime millis
@@ -201,8 +219,9 @@
          * {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_WALLPAPER},
          * or {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_ANIM}.
          */
-        public int onAppTransitionStartingLocked(boolean keyguardGoingAway, long duration,
-                long statusBarAnimationStartTime, long statusBarAnimationDuration) {
+        public int onAppTransitionStartingLocked(boolean keyguardGoingAway,
+                boolean keyguardOccluding, long duration, long statusBarAnimationStartTime,
+                long statusBarAnimationDuration) {
             return 0;
         }
 
@@ -672,24 +691,43 @@
     public abstract String getWindowName(@NonNull IBinder binder);
 
     /**
-     * Return the window name of IME Insets control target.
+     * The callback after the request of show/hide input method is sent.
      *
+     * @param show Whether to show or hide input method.
+     * @param focusedToken The token of focused window.
+     * @param requestToken The token of window who requests the change.
      * @param displayId The ID of the display which input method is currently focused.
-     * @return The corresponding {@link WindowState#getName()}
+     * @return The information of the input method target.
      */
-    public abstract @Nullable String getImeControlTargetNameForLogging(int displayId);
+    public abstract ImeTargetInfo onToggleImeRequested(boolean show,
+            @NonNull IBinder focusedToken, @NonNull IBinder requestToken, int displayId);
 
-    /**
-     * Return the current window name of the input method is on top of.
-     *
-     * Note that the concept of this window is only reparent the target window behind the input
-     * method window, it may different with the window which reported by
-     * {@code InputMethodManagerService#reportStartInput} which has input connection.
-     *
-     * @param displayId The ID of the display which input method is currently focused.
-     * @return The corresponding {@link WindowState#getName()}
-     */
-    public abstract @Nullable String getImeTargetNameForLogging(int displayId);
+    /** The information of input method target when IME is requested to show or hide. */
+    public static class ImeTargetInfo {
+        public final String focusedWindowName;
+        public final String requestWindowName;
+
+        /** The window name of IME Insets control target. */
+        public final String imeControlTargetName;
+
+        /**
+         * The current window name of the input method is on top of.
+         * <p>
+         * Note that the concept of this window is only used to reparent the target window behind
+         * the input method window, it may be different from the window reported by
+         * {@link com.android.server.inputmethod.InputMethodManagerService#reportStartInput} which
+         * has input connection.
+         */
+        public final String imeLayerTargetName;
+
+        public ImeTargetInfo(String focusedWindowName, String requestWindowName,
+                String imeControlTargetName, String imeLayerTargetName) {
+            this.focusedWindowName = focusedWindowName;
+            this.requestWindowName = requestWindowName;
+            this.imeControlTargetName = imeControlTargetName;
+            this.imeLayerTargetName = imeLayerTargetName;
+        }
+    }
 
     /**
      * Moves the {@link WindowToken} {@code binder} to the display specified by {@code displayId}.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index e3b25a5..648ecbf 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -47,6 +47,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
@@ -84,8 +85,10 @@
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_BLAST_SYNC;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_INVALID;
+import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN;
+import static android.window.WindowProviderService.isWindowProviderService;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BOOT;
@@ -149,6 +152,7 @@
 import android.app.IAssistDataReceiver;
 import android.app.WindowConfiguration;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -158,6 +162,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.TestUtilityService;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
@@ -167,8 +172,10 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Region;
-import android.hardware.configstore.V1_0.ISurfaceFlingerConfigs;
 import android.hardware.configstore.V1_0.OptionalBool;
+import android.hardware.configstore.V1_1.DisplayOrientation;
+import android.hardware.configstore.V1_1.ISurfaceFlingerConfigs;
+import android.hardware.configstore.V1_1.OptionalDisplayOrientation;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.InputManager;
@@ -220,6 +227,7 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.Choreographer;
 import android.view.Display;
+import android.view.DisplayAddress;
 import android.view.DisplayInfo;
 import android.view.Gravity;
 import android.view.IAppTransitionAnimationSpecsFuture;
@@ -248,6 +256,7 @@
 import android.view.InputWindowHandle;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.KeyEvent;
 import android.view.MagnificationSpec;
 import android.view.MotionEvent;
@@ -337,27 +346,6 @@
 
     static final boolean PROFILE_ORIENTATION = false;
 
-    /** How much to multiply the policy's type layer, to reserve room
-     * for multiple windows of the same type and Z-ordering adjustment
-     * with TYPE_LAYER_OFFSET. */
-    static final int TYPE_LAYER_MULTIPLIER = 10000;
-
-    /** Offset from TYPE_LAYER_MULTIPLIER for moving a group of windows above
-     * or below others in the same layer. */
-    static final int TYPE_LAYER_OFFSET = 1000;
-
-    /** How much to increment the layer for each window, to reserve room
-     * for effect surfaces between them.
-     */
-    static final int WINDOW_LAYER_MULTIPLIER = 5;
-
-    /**
-     * Animation thumbnail is as far as possible below the window above
-     * the thumbnail (or in other words as far as possible above the window
-     * below it).
-     */
-    static final int LAYER_OFFSET_THUMBNAIL = WINDOW_LAYER_MULTIPLIER - 1;
-
     /** The maximum length we will accept for a loaded animation duration:
      * this is 10 seconds.
      */
@@ -450,14 +438,14 @@
     /**
      * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY
      */
-    public static final boolean sEnableRemoteKeyguardGoingAwayAnimation = !sEnableShellTransitions
-            && sEnableRemoteKeyguardAnimation >= 1;
+    public static final boolean sEnableRemoteKeyguardGoingAwayAnimation =
+            sEnableRemoteKeyguardAnimation >= 1;
 
     /**
      * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY
      */
-    public static final boolean sEnableRemoteKeyguardOccludeAnimation = !sEnableShellTransitions
-            && sEnableRemoteKeyguardAnimation >= 2;
+    public static final boolean sEnableRemoteKeyguardOccludeAnimation =
+            sEnableRemoteKeyguardAnimation >= 2;
 
     /**
      * Allows a fullscreen windowing mode activity to launch in its desired orientation directly
@@ -465,6 +453,8 @@
      */
     static final boolean ENABLE_FIXED_ROTATION_TRANSFORM =
             SystemProperties.getBoolean("persist.wm.fixed_rotation_transform", true);
+    private @Surface.Rotation int mPrimaryDisplayOrientation = Surface.ROTATION_0;
+    private DisplayAddress mPrimaryDisplayPhysicalAddress;
 
     // Enums for animation scale update types.
     @Retention(RetentionPolicy.SOURCE)
@@ -646,7 +636,7 @@
     /** List of window currently causing non-system overlay windows to be hidden. */
     private ArrayList<WindowState> mHidingNonSystemOverlayWindows = new ArrayList<>();
 
-    AccessibilityController mAccessibilityController;
+    final AccessibilityController mAccessibilityController;
     private RecentsAnimationController mRecentsAnimationController;
 
     Watermark mWatermark;
@@ -1065,6 +1055,10 @@
 
     final HighRefreshRateDenylist mHighRefreshRateDenylist;
 
+    // Maintainer of a collection of all possible DisplayInfo for all configurations of the
+    // logical displays.
+    final PossibleDisplayInfoMapper mPossibleDisplayInfoMapper;
+
     // If true, only the core apps and services are being launched because the device
     // is in a special boot mode, such as being encrypted or waiting for a decryption password.
     // For example, when this flag is true, there will be no wallpaper service.
@@ -1234,10 +1228,13 @@
         mAssistantOnTopOfDream = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_assistantOnTopOfDream);
 
-        mLetterboxConfiguration = new LetterboxConfiguration(context);
+        mLetterboxConfiguration = new LetterboxConfiguration(
+                // Using SysUI context to have access to Material colors extracted from Wallpaper.
+                ActivityThread.currentActivityThread().getSystemUiContext());
 
         mInputManager = inputManager; // Must be before createDisplayContentLocked.
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+        mPossibleDisplayInfoMapper = new PossibleDisplayInfoMapper(mDisplayManagerInternal);
 
         mSurfaceControlFactory = surfaceControlFactory;
         mTransactionFactory = transactionFactory;
@@ -1390,6 +1387,7 @@
         mStartingSurfaceController = new StartingSurfaceController(this);
 
         mBlurController = new BlurController(mContext, mPowerManager);
+        mAccessibilityController = new AccessibilityController(this);
     }
 
     DisplayAreaPolicy.Provider getDisplayAreaPolicyProvider() {
@@ -1455,7 +1453,7 @@
     }
 
     public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
-            int displayId, int requestUserId, InsetsState requestedVisibility,
+            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls) {
         Arrays.fill(outActiveControls, null);
@@ -1677,7 +1675,7 @@
 
             final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
             displayPolicy.adjustWindowParamsLw(win, win.mAttrs);
-            win.updateRequestedVisibility(requestedVisibility);
+            win.setRequestedVisibilities(requestedVisibilities);
 
             res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);
             if (res != ADD_OKAY) {
@@ -1728,16 +1726,22 @@
                     && mWindowContextListenerController.hasListener(windowContextToken)) {
                 final int windowContextType = mWindowContextListenerController
                         .getWindowType(windowContextToken);
+                final Bundle options = mWindowContextListenerController
+                        .getOptions(windowContextToken);
                 if (type != windowContextType) {
                     ProtoLog.w(WM_ERROR, "Window types in WindowContext and"
                             + " LayoutParams.type should match! Type from LayoutParams is %d,"
                             + " but type from WindowContext is %d", type, windowContextType);
-                    return WindowManagerGlobal.ADD_INVALID_TYPE;
+                    // We allow WindowProviderService to add window other than windowContextType,
+                    // but the WindowProviderService won't be associated with the window's
+                    // WindowToken.
+                    if (!isWindowProviderService(options)) {
+                        return WindowManagerGlobal.ADD_INVALID_TYPE;
+                    }
+                } else {
+                    mWindowContextListenerController.registerWindowContainerListener(
+                            windowContextToken, token, callingUid, type, options);
                 }
-                final Bundle options = mWindowContextListenerController
-                        .getOptions(windowContextToken);
-                mWindowContextListenerController.registerWindowContainerListener(
-                        windowContextToken, token, callingUid, type, options);
             }
 
             // From now on, no exceptions or errors allowed!
@@ -1767,18 +1771,15 @@
             final boolean hideSystemAlertWindows = !mHidingNonSystemOverlayWindows.isEmpty();
             win.setForceHideNonSystemOverlayWindowIfNeeded(hideSystemAlertWindows);
 
-            final ActivityRecord tokenActivity = token.asActivityRecord();
-            if (type == TYPE_APPLICATION_STARTING && tokenActivity != null) {
-                tokenActivity.mStartingWindow = win;
-                ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "addWindow: %s startingWindow=%s",
-                        activity, win);
-            }
-
             boolean imMayMove = true;
 
             win.mToken.addWindow(win);
             displayPolicy.addWindowLw(win, attrs);
-            if (type == TYPE_INPUT_METHOD) {
+            if (type == TYPE_APPLICATION_STARTING && activity != null) {
+                activity.attachStartingWindow(win);
+                ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "addWindow: %s startingWindow=%s",
+                        activity, win);
+            } else if (type == TYPE_INPUT_METHOD) {
                 displayContent.setInputMethodWindowLocked(win);
                 imMayMove = false;
             } else if (type == TYPE_INPUT_METHOD_DIALOG) {
@@ -1804,7 +1805,8 @@
             winAnimator.mEnterAnimationPending = true;
             winAnimator.mEnteringAnimation = true;
             // Check if we need to prepare a transition for replacing window first.
-            if (activity != null && activity.isVisible()
+            if (!win.mTransitionController.isShellTransitionsEnabled()
+                    && activity != null && activity.isVisible()
                     && !prepareWindowReplacementTransition(activity)) {
                 // If not, check if need to set up a dummy transition during display freeze
                 // so that the unfreeze wait for the apps to draw. This might be needed if
@@ -2163,7 +2165,7 @@
                     mWindowPlacerLocked.performSurfacePlacement();
 
                     // We need to report touchable region changes to accessibility.
-                    if (mAccessibilityController != null) {
+                    if (mAccessibilityController.hasCallbacks()) {
                         mAccessibilityController.onSomeWindowResizedOrMovedWithCallingUid(
                                 uid, w.getDisplayContent().getDisplayId());
                     }
@@ -2176,7 +2178,7 @@
 
     public void onRectangleOnScreenRequested(IBinder token, Rect rectangle) {
         synchronized (mGlobalLock) {
-            if (mAccessibilityController != null) {
+            if (mAccessibilityController.hasCallbacks()) {
                 WindowState window = mWindowMap.get(token);
                 if (window != null) {
                     mAccessibilityController.onRectangleOnScreenRequested(
@@ -2266,7 +2268,7 @@
                     win.mActivityRecord.checkKeyguardFlagsChanged();
                 }
                 if (((attrChanges & LayoutParams.ACCESSIBILITY_TITLE_CHANGED) != 0)
-                        && (mAccessibilityController != null)) {
+                        && (mAccessibilityController.hasCallbacks())) {
                     // No move or resize, but the controller checks for title changes as well
                     mAccessibilityController.onSomeWindowResizedOrMovedWithCallingUid(
                             uid, win.getDisplayContent().getDisplayId());
@@ -2461,16 +2463,22 @@
             configChanged = displayContent.updateOrientation();
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
 
-            final DisplayInfo rotatedDisplayInfo =
-                    win.mToken.getFixedRotationTransformDisplayInfo();
-            if (rotatedDisplayInfo != null) {
-                outSurfaceControl.setTransformHint(rotatedDisplayInfo.rotation);
-            } else {
-                // We have to update the transform hint of display here, but we need to get if from
-                // SurfaceFlinger, so set it as rotation of display for most cases, then
-                // SurfaceFlinger would still update the transform hint of display in next frame.
-                outSurfaceControl.setTransformHint(displayContent.getDisplayInfo().rotation);
+            final DisplayInfo displayInfo = win.getDisplayInfo();
+            int transformHint = displayInfo.rotation;
+            // If the window is on the primary display, use the panel orientation to adjust the
+            // transform hint
+            final boolean isPrimaryDisplay = displayInfo.address != null &&
+                    displayInfo.address.equals(mPrimaryDisplayPhysicalAddress);
+            if (isPrimaryDisplay) {
+                transformHint = (transformHint + mPrimaryDisplayOrientation) % 4;
             }
+            outSurfaceControl.setTransformHint(
+                    SurfaceControl.rotationToBufferTransform(transformHint));
+            ProtoLog.v(WM_DEBUG_ORIENTATION,
+                    "Passing transform hint %d for window %s%s",
+                    transformHint, win,
+                    isPrimaryDisplay ? " on primary display with orientation "
+                            + mPrimaryDisplayOrientation : "");
 
             if (toBeDisplayed && win.mIsWallpaper) {
                 displayContent.mWallpaperController.updateWallpaperOffset(win, false /* sync */);
@@ -2478,7 +2486,7 @@
             if (win.mActivityRecord != null) {
                 win.mActivityRecord.updateReportedVisibilityLocked();
             }
-            if (displayPolicy.areSystemBarsForcedShownLw(win)) {
+            if (displayPolicy.areSystemBarsForcedShownLw()) {
                 result |= WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS;
             }
             if (!win.isGoneForLayout()) {
@@ -2524,7 +2532,8 @@
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             }
             if (winAnimator.mSurfaceController != null) {
-                win.calculateSurfaceBounds(win.getAttrs(), mTmpRect);
+                win.calculateSurfaceBounds(win.getLayoutingAttrs(
+                        win.getWindowConfiguration().getRotation()), mTmpRect);
                 outSurfaceSize.set(mTmpRect.width(), mTmpRect.height());
             }
             getInsetsSourceControls(win, outActiveControls);
@@ -2562,7 +2571,7 @@
         if (win.mAttrs.type == TYPE_APPLICATION_STARTING) {
             transit = WindowManagerPolicy.TRANSIT_PREVIEW_DONE;
         }
-        if (mAtmService.getTransitionController().inTransition(win)) {
+        if (win.inTransition()) {
             focusMayChange = true;
             win.mAnimatingExit = true;
         } else if (win.isWinVisibleLw() && winAnimator.applyAnimationLocked(transit, false)) {
@@ -2574,20 +2583,24 @@
             // an exit.
             win.mAnimatingExit = true;
         } else if (win.mDisplayContent.okToAnimate()
-                && win.mDisplayContent.mWallpaperController.isWallpaperTarget(win)) {
-            // If the wallpaper is currently behind this
-            // window, we need to change both of them inside
-            // of a transaction to avoid artifacts.
+                && win.mDisplayContent.mWallpaperController.isWallpaperTarget(win)
+                && win.mAttrs.type != TYPE_NOTIFICATION_SHADE) {
+            // If the wallpaper is currently behind this app window, we need to change both of them
+            // inside of a transaction to avoid artifacts.
+            // For NotificationShade, sysui is in charge of running window animation and it updates
+            // the client view visibility only after both NotificationShade and the wallpaper are
+            // hidden. So we don't need to care about exit animation, but can destroy its surface
+            // immediately.
             win.mAnimatingExit = true;
         } else {
-            boolean stopped = win.mActivityRecord != null ? win.mActivityRecord.mAppStopped : true;
+            boolean stopped = win.mActivityRecord == null || win.mActivityRecord.mAppStopped;
             // We set mDestroying=true so ActivityRecord#notifyAppStopped in-to destroy surfaces
             // will later actually destroy the surface if we do not do so here. Normally we leave
             // this to the exit animation.
             win.mDestroying = true;
             win.destroySurface(false, stopped);
         }
-        if (mAccessibilityController != null) {
+        if (mAccessibilityController.hasCallbacks()) {
             mAccessibilityController.onWindowTransition(win, transit);
         }
 
@@ -3055,7 +3068,7 @@
         mSettingsObserver.updateSystemUiSettings(true /* handleChange */);
         synchronized (mGlobalLock) {
             // force a re-application of focused window sysui visibility on each display.
-            mRoot.forAllDisplayPolicies(DisplayPolicy::resetSystemUiVisibilityLw);
+            mRoot.forAllDisplayPolicies(DisplayPolicy::resetSystemBarAttributes);
         }
     }
 
@@ -3779,6 +3792,14 @@
         }
     }
 
+    @Override
+    public SurfaceControl mirrorWallpaperSurface(int displayId) {
+        synchronized (mGlobalLock) {
+            final DisplayContent dc = mRoot.getDisplayContent(displayId);
+            return dc.mWallpaperController.mirrorWallpaperSurface();
+        }
+    }
+
     /**
      * Takes a snapshot of the screen.  In landscape mode this grabs the whole screen.
      * In portrait mode, it grabs the upper region of the screen based on the vertical dimension
@@ -4044,7 +4065,7 @@
 
                     final boolean pendingRemoteRotation = rotationChanged
                             && (displayContent.getDisplayRotation().isWaitingForRemoteRotation()
-                            || mAtmService.getTransitionController().isCollecting());
+                            || displayContent.mTransitionController.isCollecting());
                     // Even if alwaysSend, we are waiting for a transition or remote to provide
                     // rotated configuration, so we can't update configuration yet.
                     if (!pendingRemoteRotation) {
@@ -4164,7 +4185,7 @@
     }
 
     @Override
-    public void modifyDisplayWindowInsets(int displayId, InsetsState state) {
+    public void updateDisplayWindowRequestedVisibilities(int displayId, InsetsVisibilities vis) {
         if (mContext.checkCallingOrSelfPermission(MANAGE_APP_TOKENS)
                 != PackageManager.PERMISSION_GRANTED) {
             throw new SecurityException("Must hold permission " + MANAGE_APP_TOKENS);
@@ -4176,7 +4197,7 @@
                 if (dc == null || dc.mRemoteInsetsControlTarget == null) {
                     return;
                 }
-                dc.mRemoteInsetsControlTarget.updateRequestedVisibility(state);
+                dc.mRemoteInsetsControlTarget.setRequestedVisibilities(vis);
                 dc.getInsetsStateController().onInsetsModified(dc.mRemoteInsetsControlTarget);
             }
         } finally {
@@ -4348,13 +4369,18 @@
         }
     }
 
-    /** Registers a hierarchy listener that gets callbacks when the hierarchy changes. */
+    /**
+     * Registers a hierarchy listener that gets callbacks when the hierarchy changes. The listener's
+     * onDisplayAdded() will not be called for the displays returned.
+     *
+     * @return the displayIds for the existing displays
+     */
     @Override
-    public void registerDisplayWindowListener(IDisplayWindowListener listener) {
+    public int[] registerDisplayWindowListener(IDisplayWindowListener listener) {
         mAtmService.enforceTaskPermission("registerDisplayWindowListener");
         final long ident = Binder.clearCallingIdentity();
         try {
-            mDisplayNotificationController.registerListener(listener);
+            return mDisplayNotificationController.registerListener(listener);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -4870,6 +4896,9 @@
         mTaskSnapshotController.systemReady();
         mHasWideColorGamutSupport = queryWideColorGamutSupport();
         mHasHdrSupport = queryHdrSupport();
+        mPrimaryDisplayOrientation = queryPrimaryDisplayOrientation();
+        mPrimaryDisplayPhysicalAddress =
+            DisplayAddress.fromPhysicalDisplayId(SurfaceControl.getPrimaryPhysicalDisplayId());
         UiThread.getHandler().post(mSettingsObserver::loadSettings);
         IVrManager vrManager = IVrManager.Stub.asInterface(
                 ServiceManager.getService(Context.VR_SERVICE));
@@ -4889,6 +4918,9 @@
         }
     }
 
+
+    // Keep logic in sync with SurfaceFlingerProperties.cpp
+    // Consider exposing properties via ISurfaceComposer instead.
     private static boolean queryWideColorGamutSupport() {
         boolean defaultValue = false;
         Optional<Boolean> hasWideColorProp = SurfaceFlingerProperties.has_wide_color_display();
@@ -4929,23 +4961,82 @@
         return false;
     }
 
-    void reportFocusChanged(IBinder oldToken, IBinder newToken) {
-        WindowState lastFocus;
-        WindowState newFocus;
-        synchronized (mGlobalLock) {
-            lastFocus = mInputToWindowMap.get(oldToken);
-            newFocus = mInputToWindowMap.get(newToken);
-            ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "Focus changing: %s -> %s", lastFocus, newFocus);
+    private static @Surface.Rotation int queryPrimaryDisplayOrientation() {
+        Optional<SurfaceFlingerProperties.primary_display_orientation_values> prop =
+                SurfaceFlingerProperties.primary_display_orientation();
+        if (prop.isPresent()) {
+            switch (prop.get()) {
+                case ORIENTATION_90: return Surface.ROTATION_90;
+                case ORIENTATION_180: return Surface.ROTATION_180;
+                case ORIENTATION_270: return Surface.ROTATION_270;
+                case ORIENTATION_0:
+                default:
+                    return Surface.ROTATION_0;
+            }
+        }
+        try {
+            ISurfaceFlingerConfigs surfaceFlinger = ISurfaceFlingerConfigs.getService();
+            OptionalDisplayOrientation primaryDisplayOrientation =
+                    surfaceFlinger.primaryDisplayOrientation();
+            if (primaryDisplayOrientation != null && primaryDisplayOrientation.specified) {
+                switch (primaryDisplayOrientation.value) {
+                    case DisplayOrientation.ORIENTATION_90: return Surface.ROTATION_90;
+                    case DisplayOrientation.ORIENTATION_180: return Surface.ROTATION_180;
+                    case DisplayOrientation.ORIENTATION_270: return Surface.ROTATION_270;
+                    case DisplayOrientation.ORIENTATION_0:
+                    default:
+                        return Surface.ROTATION_0;
+                }
+            }
+        } catch (Exception e) {
+            // Use default value if we can't talk to config store.
+        }
+        return Surface.ROTATION_0;
+    }
+
+    // Returns an input target which is mapped to the given input token. This can be a WindowState
+    // or an embedded window.
+    @Nullable InputTarget getInputTargetFromToken(IBinder inputToken) {
+        WindowState windowState = mInputToWindowMap.get(inputToken);
+        if (windowState != null) {
+            return windowState;
         }
 
-        if (newFocus != null) {
-            mAnrController.onFocusChanged(newFocus);
-            newFocus.reportFocusChangedSerialized(true);
+        EmbeddedWindowController.EmbeddedWindow embeddedWindow =
+                mEmbeddedWindowController.get(inputToken);
+        if (embeddedWindow != null) {
+            return embeddedWindow;
+        }
+
+        return null;
+    }
+
+    void reportFocusChanged(IBinder oldToken, IBinder newToken) {
+        InputTarget lastTarget;
+        InputTarget newTarget;
+        synchronized (mGlobalLock) {
+            lastTarget = getInputTargetFromToken(oldToken);
+            newTarget = getInputTargetFromToken(newToken);
+            if (newTarget == null && lastTarget == null) {
+                Slog.v(TAG_WM, "Unknown focus tokens, dropping reportFocusChanged");
+                return;
+            }
+
+            mAccessibilityController.onFocusChanged(lastTarget, newTarget);
+            ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "Focus changing: %s -> %s", lastTarget, newTarget);
+        }
+
+        // Call WindowState focus change observers
+        WindowState newFocusedWindow = newTarget != null ? newTarget.getWindowState() : null;
+        if (newFocusedWindow != null && newFocusedWindow.mInputChannelToken == newToken) {
+            mAnrController.onFocusChanged(newFocusedWindow);
+            newFocusedWindow.reportFocusChangedSerialized(true);
             notifyFocusChanged();
         }
 
-        if (lastFocus != null) {
-            lastFocus.reportFocusChangedSerialized(false);
+        WindowState lastFocusedWindow = lastTarget != null ? lastTarget.getWindowState() : null;
+        if (lastFocusedWindow != null && lastFocusedWindow.mInputChannelToken == oldToken) {
+            lastFocusedWindow.reportFocusChangedSerialized(false);
         }
     }
 
@@ -5297,6 +5388,7 @@
                 case LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED: {
                     synchronized (mGlobalLock) {
                         final DisplayContent displayContent = (DisplayContent) msg.obj;
+                        displayContent.mLayoutAndAssignWindowLayersScheduled = false;
                         displayContent.layoutAndAssignWindowLayersIfNeeded();
                     }
                     break;
@@ -5401,6 +5493,25 @@
         }
     }
 
+    void setSandboxDisplayApis(int displayId, boolean sandboxDisplayApis) {
+        if (mContext.checkCallingOrSelfPermission(WRITE_SECURE_SETTINGS)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Must hold permission " + WRITE_SECURE_SETTINGS);
+        }
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+                if (displayContent != null) {
+                    displayContent.setSandboxDisplayApis(sandboxDisplayApis);
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     /** The global settings only apply to default display. */
     private boolean applyForcedPropertiesForDefaultDisplay() {
         boolean changed = false;
@@ -6319,6 +6430,7 @@
         pw.print("  mGlobalConfiguration="); pw.println(mRoot.getConfiguration());
         pw.print("  mHasPermanentDpad="); pw.println(mHasPermanentDpad);
         mRoot.dumpTopFocusedDisplayId(pw);
+        mRoot.dumpDefaultMinSizeOfResizableTask(pw);
         mRoot.forAllDisplays(dc -> {
             final int displayId = dc.getDisplayId();
             final InsetsControlTarget imeLayeringTarget = dc.getImeTarget(IME_TARGET_LAYERING);
@@ -6352,7 +6464,7 @@
 
         mInputManagerCallback.dump(pw, "  ");
         mTaskSnapshotController.dump(pw, "  ");
-        if (mAccessibilityController != null) {
+        if (mAccessibilityController.hasCallbacks()) {
             mAccessibilityController.dump(pw, "  ");
         }
 
@@ -7372,7 +7484,7 @@
         @Override
         public void setMagnificationSpec(int displayId, MagnificationSpec spec) {
             synchronized (mGlobalLock) {
-                if (mAccessibilityController != null) {
+                if (mAccessibilityController.hasCallbacks()) {
                     mAccessibilityController.setMagnificationSpec(displayId, spec);
                 } else {
                     throw new IllegalStateException("Magnification callbacks not set!");
@@ -7383,7 +7495,7 @@
         @Override
         public void setForceShowMagnifiableBounds(int displayId, boolean show) {
             synchronized (mGlobalLock) {
-                if (mAccessibilityController != null) {
+                if (mAccessibilityController.hasCallbacks()) {
                     mAccessibilityController.setForceShowMagnifiableBounds(displayId, show);
                 } else {
                     throw new IllegalStateException("Magnification callbacks not set!");
@@ -7394,7 +7506,7 @@
         @Override
         public void getMagnificationRegion(int displayId, @NonNull Region magnificationRegion) {
             synchronized (mGlobalLock) {
-                if (mAccessibilityController != null) {
+                if (mAccessibilityController.hasCallbacks()) {
                     mAccessibilityController.getMagnificationRegion(displayId, magnificationRegion);
                 } else {
                     throw new IllegalStateException("Magnification callbacks not set!");
@@ -7410,7 +7522,7 @@
                     return null;
                 }
                 MagnificationSpec spec = null;
-                if (mAccessibilityController != null) {
+                if (mAccessibilityController.hasCallbacks()) {
                     spec = mAccessibilityController.getMagnificationSpecForWindow(windowState);
                 }
                 if ((spec == null || spec.isNop()) && windowState.mGlobalScale == 1.0f) {
@@ -7429,16 +7541,7 @@
         public boolean setMagnificationCallbacks(int displayId,
                 @Nullable MagnificationCallbacks callbacks) {
             synchronized (mGlobalLock) {
-                if (mAccessibilityController == null) {
-                    mAccessibilityController = new AccessibilityController(
-                            WindowManagerService.this);
-                }
-                boolean result = mAccessibilityController.setMagnificationCallbacks(
-                        displayId, callbacks);
-                if (!mAccessibilityController.hasCallbacks()) {
-                    mAccessibilityController = null;
-                }
-                return result;
+                return mAccessibilityController.setMagnificationCallbacks(displayId, callbacks);
             }
         }
 
@@ -7446,17 +7549,8 @@
         public boolean setWindowsForAccessibilityCallback(int displayId,
                 WindowsForAccessibilityCallback callback) {
             synchronized (mGlobalLock) {
-                if (mAccessibilityController == null) {
-                    mAccessibilityController = new AccessibilityController(
-                            WindowManagerService.this);
-                }
-                final boolean result =
-                        mAccessibilityController.setWindowsForAccessibilityCallback(
-                        displayId, callback);
-                if (!mAccessibilityController.hasCallbacks()) {
-                    mAccessibilityController = null;
-                }
-                return result;
+                return mAccessibilityController
+                        .setWindowsForAccessibilityCallback(displayId, callback);  
             }
         }
 
@@ -7468,11 +7562,7 @@
         @Override
         public IBinder getFocusedWindowToken() {
             synchronized (mGlobalLock) {
-                WindowState windowState = getFocusedWindowLocked();
-                if (windowState != null) {
-                    return windowState.mClient.asBinder();
-                }
-                return null;
+                return mAccessibilityController.getFocusedWindowToken();
             }
         }
 
@@ -7565,6 +7655,7 @@
         public void registerAppTransitionListener(AppTransitionListener listener) {
             synchronized (mGlobalLock) {
                 getDefaultDisplayContentLocked().mAppTransition.registerListenerLocked(listener);
+                mAtmService.getTransitionController().registerLegacyListener(listener);
             }
         }
 
@@ -7628,13 +7719,7 @@
 
         @Override
         public void computeWindowsForAccessibility(int displayId) {
-            final AccessibilityController accessibilityController;
-            synchronized (mGlobalLock) {
-                accessibilityController = mAccessibilityController;
-            }
-            if (accessibilityController != null) {
-                accessibilityController.performComputeChangedWindowsNot(displayId, true);
-            }
+            mAccessibilityController.performComputeChangedWindowsNot(displayId, true);
         }
 
         @Override
@@ -7708,7 +7793,7 @@
                 final WindowState currentFocus = displayContent.mCurrentFocus;
                 if (currentFocus != null && currentFocus.mSession.mUid == uid
                         && currentFocus.mSession.mPid == pid) {
-                    return true;
+                    return currentFocus.canBeImeTarget();
                 }
             }
             return false;
@@ -7867,30 +7952,37 @@
         }
 
         @Override
-        public String getImeControlTargetNameForLogging(int displayId) {
+        public ImeTargetInfo onToggleImeRequested(boolean show, IBinder focusedToken,
+                IBinder requestToken, int displayId) {
+            final String focusedWindowName;
+            final String requestWindowName;
+            final String imeControlTargetName;
+            final String imeLayerTargetName;
             synchronized (mGlobalLock) {
+                final WindowState focusedWin = mWindowMap.get(focusedToken);
+                focusedWindowName = focusedWin != null ? focusedWin.getName() : "null";
+                final WindowState requestWin = mWindowMap.get(requestToken);
+                requestWindowName = requestWin != null ? requestWin.getName() : "null";
                 final DisplayContent dc = mRoot.getDisplayContent(displayId);
-                if (dc == null) {
-                    return null;
+                if (dc != null) {
+                    final InsetsControlTarget controlTarget = dc.getImeTarget(IME_TARGET_CONTROL);
+                    if (controlTarget != null) {
+                        final WindowState w = InsetsControlTarget.asWindowOrNull(controlTarget);
+                        imeControlTargetName = w != null ? w.getName() : controlTarget.toString();
+                    } else {
+                        imeControlTargetName = "null";
+                    }
+                    final InsetsControlTarget target = dc.getImeTarget(IME_TARGET_LAYERING);
+                    imeLayerTargetName = target != null ? target.getWindow().getName() : "null";
+                    if (show) {
+                        dc.onShowImeRequested();
+                    }
+                } else {
+                    imeControlTargetName = imeLayerTargetName = "no-display";
                 }
-                final InsetsControlTarget target = dc.getImeTarget(IME_TARGET_CONTROL);
-                if (target == null) {
-                    return null;
-                }
-                final WindowState win = target.getWindow();
-                return win != null ? win.getName() : target.toString();
             }
-        }
-
-        @Override
-        public String getImeTargetNameForLogging(int displayId) {
-            synchronized (mGlobalLock) {
-                final DisplayContent dc = mRoot.getDisplayContent(displayId);
-                if (dc == null || dc.getImeTarget(IME_TARGET_LAYERING) == null) {
-                    return null;
-                }
-                return dc.getImeTarget(IME_TARGET_LAYERING).getWindow().getName();
-            }
+            return new ImeTargetInfo(focusedWindowName, requestWindowName, imeControlTargetName,
+                    imeLayerTargetName);
         }
 
         @Override
@@ -8088,11 +8180,20 @@
             // This could prevent if there is no container animation, we still have to apply the
             // pending transaction and exit waiting.
             mAnimator.mNotifyWhenNoAnimation = true;
+            boolean animateStarting = false;
             while (timeoutRemaining > 0) {
+                // Waiting until all starting windows has finished animating.
+                animateStarting = !mAtmService.getTransitionController().isShellTransitionsEnabled()
+                        && mRoot.forAllActivities(ActivityRecord::hasStartingWindow);
                 boolean isAnimating = mAnimator.isAnimationScheduled()
-                        || mRoot.isAnimating(TRANSITION | CHILDREN, ANIMATION_TYPE_ALL);
+                        || mRoot.isAnimating(TRANSITION | CHILDREN, ANIMATION_TYPE_ALL)
+                        || animateStarting;
                 if (!isAnimating) {
-                    break;
+                    // isAnimating is a legacy transition query and will be removed, so also add
+                    // a check for whether this is in a shell-transition when not using legacy.
+                    if (!mAtmService.getTransitionController().inTransition()) {
+                        break;
+                    }
                 }
                 long startTime = System.currentTimeMillis();
                 try {
@@ -8106,13 +8207,14 @@
             WindowContainer animatingContainer;
             animatingContainer = mRoot.getAnimatingContainer(TRANSITION | CHILDREN,
                     ANIMATION_TYPE_ALL);
-            if (mAnimator.isAnimationScheduled() || animatingContainer != null) {
+            if (mAnimator.isAnimationScheduled() || animatingContainer != null || animateStarting) {
                 Slog.w(TAG, "Timed out waiting for animations to complete,"
                         + " animatingContainer=" + animatingContainer
                         + " animationType=" + SurfaceAnimator.animationTypeToString(
                         animatingContainer != null
                                 ? animatingContainer.mSurfaceAnimator.getAnimationType()
-                                : SurfaceAnimator.ANIMATION_TYPE_NONE));
+                                : SurfaceAnimator.ANIMATION_TYPE_NONE)
+                        + " animateStarting=" + animateStarting);
             }
         }
     }
@@ -8154,11 +8256,11 @@
             displayContent.getParent().positionChildAt(WindowContainer.POSITION_TOP, displayContent,
                     true /* includingParents */);
         }
-        handleTaskFocusChange(touchedWindow.getTask());
+        handleTaskFocusChange(touchedWindow.getTask(), touchedWindow.mActivityRecord);
     }
 
     @VisibleForTesting
-    void handleTaskFocusChange(Task task) {
+    void handleTaskFocusChange(Task task, ActivityRecord touchedActivity) {
         if (task == null) {
             return;
         }
@@ -8177,7 +8279,7 @@
             }
         }
 
-        mAtmService.setFocusedTask(task.mTaskId);
+        mAtmService.setFocusedTask(task.mTaskId, touchedActivity);
     }
 
     /**
@@ -8201,11 +8303,11 @@
             clientChannel = win.openInputChannel();
             mEmbeddedWindowController.add(clientChannel.getToken(), win);
             applicationHandle = win.getApplicationHandle();
-            name = win.getName();
+            name = win.toString();
         }
 
         updateInputChannel(clientChannel.getToken(), callingUid, callingPid, displayId, surface,
-                name, applicationHandle, flags, privateFlags, type, null /* region */);
+                name, applicationHandle, flags, privateFlags, type, null /* region */, window);
 
         clientChannel.copyTo(outInputChannel);
     }
@@ -8213,9 +8315,10 @@
     private void updateInputChannel(IBinder channelToken, int callingUid, int callingPid,
                                     int displayId, SurfaceControl surface, String name,
                                     InputApplicationHandle applicationHandle, int flags,
-                                    int privateFlags, int type, Region region) {
+                                    int privateFlags, int type, Region region, IWindow window) {
         InputWindowHandle h = new InputWindowHandle(applicationHandle, displayId);
         h.token = channelToken;
+        h.setWindowToken(window);
         h.name = name;
 
         final int sanitizedFlags = flags & (LayoutParams.FLAG_NOT_TOUCHABLE
@@ -8266,12 +8369,12 @@
                 Slog.e(TAG, "Couldn't find window for provided channelToken.");
                 return;
             }
-            name = win.getName();
+            name = win.toString();
             applicationHandle = win.getApplicationHandle();
         }
 
         updateInputChannel(channelToken, win.mOwnerUid, win.mOwnerPid, displayId, surface, name,
-                applicationHandle, flags, privateFlags, win.mWindowType, region);
+                applicationHandle, flags, privateFlags, win.mWindowType, region, win.mClient);
     }
 
     /** Return whether layer tracing is enabled */
@@ -8414,6 +8517,52 @@
         }
     }
 
+    @Override
+    public List<DisplayInfo> getPossibleDisplayInfo(int displayId, String packageName) {
+        final int callingUid = Binder.getCallingUid();
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                if (packageName == null || !isRecentsComponent(packageName, callingUid)) {
+                    Slog.e(TAG, "Unable to verify uid for package " + packageName
+                            + " for getPossibleMaximumWindowMetrics");
+                    return new ArrayList<>();
+                }
+
+                // Retrieve the DisplayInfo for all possible rotations across all possible display
+                // layouts.
+                return List.copyOf(mPossibleDisplayInfoMapper.getPossibleDisplayInfos(displayId));
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    /**
+     * Returns {@code true} when the calling package is the recents component.
+     */
+    boolean isRecentsComponent(@NonNull String callingPackageName, int callingUid) {
+        String recentsPackage;
+        try {
+            String recentsComponent = mContext.getResources().getString(
+                    R.string.config_recentsComponentName);
+            if (recentsComponent == null) {
+                return false;
+            }
+            recentsPackage = ComponentName.unflattenFromString(recentsComponent).getPackageName();
+        } catch (Resources.NotFoundException e) {
+            Slog.e(TAG, "Unable to verify if recents component", e);
+            return false;
+        }
+        try {
+            return callingUid == mContext.getPackageManager().getPackageUid(callingPackageName, 0)
+                    && callingPackageName.equals(recentsPackage);
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(TAG, "Unable to verify if recents component", e);
+            return false;
+        }
+    }
+
     void grantEmbeddedWindowFocus(Session session, IBinder inputToken, boolean grantFocus) {
         synchronized (mGlobalLock) {
             final EmbeddedWindowController.EmbeddedWindow embeddedWindow =
@@ -8429,10 +8578,9 @@
             SurfaceControl.Transaction t = mTransactionFactory.get();
             final int displayId = embeddedWindow.mDisplayId;
             if (grantFocus) {
-                t.setFocusedWindow(inputToken, embeddedWindow.getName(), displayId).apply();
+                t.setFocusedWindow(inputToken, embeddedWindow.toString(), displayId).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
-                        "Focus request " + embeddedWindow.getName(),
-                        "reason=grantEmbeddedWindowFocus(true)");
+                        "Focus request " + embeddedWindow, "reason=grantEmbeddedWindowFocus(true)");
             } else {
                 // Search for a new focus target
                 DisplayContent displayContent = mRoot.getDisplayContent(displayId);
@@ -8441,18 +8589,18 @@
                 if (newFocusTarget == null) {
                     ProtoLog.v(WM_DEBUG_FOCUS, "grantEmbeddedWindowFocus remove request for "
                                     + "win=%s dropped since no candidate was found",
-                            embeddedWindow.getName());
+                            embeddedWindow);
                     return;
                 }
                 t.requestFocusTransfer(newFocusTarget.mInputChannelToken, newFocusTarget.getName(),
-                        inputToken, embeddedWindow.getName(),
+                        inputToken, embeddedWindow.toString(),
                         displayId).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
                         "Transfer focus request " + newFocusTarget,
                         "reason=grantEmbeddedWindowFocus(false)");
             }
             ProtoLog.v(WM_DEBUG_FOCUS, "grantEmbeddedWindowFocus win=%s grantFocus=%s",
-                    embeddedWindow.getName(), grantFocus);
+                    embeddedWindow, grantFocus);
         }
     }
 
@@ -8481,24 +8629,24 @@
             }
             SurfaceControl.Transaction t = mTransactionFactory.get();
             if (grantFocus) {
-                t.requestFocusTransfer(targetInputToken, embeddedWindow.getName(),
+                t.requestFocusTransfer(targetInputToken, embeddedWindow.toString(),
                         hostWindow.mInputChannel.getToken(),
                         hostWindow.getName(),
                         hostWindow.getDisplayId()).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
-                        "Transfer focus request " + embeddedWindow.getName(),
+                        "Transfer focus request " + embeddedWindow,
                         "reason=grantEmbeddedWindowFocus(true)");
             } else {
                 t.requestFocusTransfer(hostWindow.mInputChannel.getToken(), hostWindow.getName(),
                         targetInputToken,
-                        embeddedWindow.getName(),
+                        embeddedWindow.toString(),
                         hostWindow.getDisplayId()).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
                         "Transfer focus request " + hostWindow,
                         "reason=grantEmbeddedWindowFocus(false)");
             }
             ProtoLog.v(WM_DEBUG_FOCUS, "grantEmbeddedWindowFocus win=%s grantFocus=%s",
-                    embeddedWindow.getName(), grantFocus);
+                    embeddedWindow, grantFocus);
         }
     }
 
@@ -8550,7 +8698,7 @@
             }
 
             if (win.mActivityRecord == null || !win.mActivityRecord.isState(
-                    Task.ActivityState.RESUMED)) {
+                    ActivityRecord.State.RESUMED)) {
                 mDisplayHashController.sendDisplayHashError(callback,
                         DISPLAY_HASH_ERROR_MISSING_WINDOW);
                 return;
@@ -8606,4 +8754,23 @@
             return snapshot != null && snapshot.hasImeSurface();
         }
     }
+
+    @Override
+    public int getImeDisplayId() {
+        // TODO(b/189805422): Add a toast to notify users that IMS may get extra
+        //  onConfigurationChanged callback when perDisplayFocus is enabled.
+        //  Enabling perDisplayFocus means that we track focus on each display, so we don't have
+        //  the "top focus" display and getTopFocusedDisplayContent returns the default display
+        //  as the fallback. It leads to InputMethodService receives an extra onConfiguration
+        //  callback when InputMethodService move from a secondary display to another display
+        //  with the same display metrics because InputMethodService will always associate with
+        //  the ImeContainer on the default display in onCreate and receive a configuration update
+        //  to match default display ImeContainer and then receive another configuration update
+        //  from attachToWindowToken.
+        synchronized (mGlobalLock) {
+            final DisplayContent dc = mRoot.getTopFocusedDisplayContent();
+            return dc.getImePolicy() == DISPLAY_IME_POLICY_LOCAL ? dc.getDisplayId()
+                    : DEFAULT_DISPLAY;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index a94fd07..0f8587c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -19,6 +19,16 @@
 import static android.os.Build.IS_USER;
 import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED;
 
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_RIGHT;
+
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.ParcelFileDescriptor;
@@ -36,6 +46,8 @@
 import com.android.internal.protolog.ProtoLogImpl;
 import com.android.server.LocalServices;
 import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
+import com.android.server.wm.LetterboxConfiguration.LetterboxReachabilityPosition;
 
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -58,10 +70,12 @@
 
     // Internal service impl -- must perform security checks before touching.
     private final WindowManagerService mInternal;
+    private final LetterboxConfiguration mLetterboxConfiguration;
 
     public WindowManagerShellCommand(WindowManagerService service) {
         mInterface = service;
         mInternal = service;
+        mLetterboxConfiguration = service.mLetterboxConfiguration;
     }
 
     @Override
@@ -113,6 +127,14 @@
                     return runGetIgnoreOrientationRequest(pw);
                 case "dump-visible-window-views":
                     return runDumpVisibleWindowViews(pw);
+                case "set-letterbox-style":
+                    return runSetLetterboxStyle(pw);
+                case "get-letterbox-style":
+                    return runGetLetterboxStyle(pw);
+                case "reset-letterbox-style":
+                    return runResetLetterboxStyle(pw);
+                case "set-sandbox-display-apis":
+                    return runSandboxDisplayApis(pw);
                 case "set-multi-window-config":
                     return runSetMultiWindowConfig();
                 case "get-multi-window-config":
@@ -331,6 +353,37 @@
         return 0;
     }
 
+    /**
+     * Override display size and metrics to reflect the DisplayArea of the calling activity.
+     */
+    private int runSandboxDisplayApis(PrintWriter pw) throws RemoteException {
+        int displayId = Display.DEFAULT_DISPLAY;
+        String arg = getNextArgRequired();
+        if ("-d".equals(arg)) {
+            displayId = Integer.parseInt(getNextArgRequired());
+            arg = getNextArgRequired();
+        }
+
+        final boolean sandboxDisplayApis;
+        switch (arg) {
+            case "true":
+            case "1":
+                sandboxDisplayApis = true;
+                break;
+            case "false":
+            case "0":
+                sandboxDisplayApis = false;
+                break;
+            default:
+                getErrPrintWriter().println("Error: expecting true, 1, false, 0, but we "
+                        + "get " + arg);
+                return -1;
+        }
+
+        mInternal.setSandboxDisplayApis(displayId, sandboxDisplayApis);
+        return 0;
+    }
+
     private int runDismissKeyguard(PrintWriter pw) throws RemoteException {
         mInterface.dismissKeyguard(null /* callback */, null /* message */);
         return 0;
@@ -548,6 +601,318 @@
         return 0;
     }
 
+    private int runSetFixedOrientationLetterboxAspectRatio(PrintWriter pw) throws RemoteException {
+        final float aspectRatio;
+        try {
+            String arg = getNextArgRequired();
+            aspectRatio = Float.parseFloat(arg);
+        } catch (NumberFormatException  e) {
+            getErrPrintWriter().println("Error: bad aspect ratio format " + e);
+            return -1;
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: aspect ratio should be provided as an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxActivityCornersRadius(PrintWriter pw) throws RemoteException {
+        final int cornersRadius;
+        try {
+            String arg = getNextArgRequired();
+            cornersRadius = Integer.parseInt(arg);
+        } catch (NumberFormatException  e) {
+            getErrPrintWriter().println("Error: bad corners radius format " + e);
+            return -1;
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: corners radius should be provided as an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setLetterboxActivityCornersRadius(cornersRadius);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxBackgroundType(PrintWriter pw) throws RemoteException {
+        @LetterboxBackgroundType final int backgroundType;
+        try {
+            String arg = getNextArgRequired();
+            switch (arg) {
+                case "solid_color":
+                    backgroundType = LETTERBOX_BACKGROUND_SOLID_COLOR;
+                    break;
+                case "app_color_background":
+                    backgroundType = LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+                    break;
+                case "app_color_background_floating":
+                    backgroundType = LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+                    break;
+                case "wallpaper":
+                    backgroundType = LETTERBOX_BACKGROUND_WALLPAPER;
+                    break;
+                default:
+                    getErrPrintWriter().println(
+                            "Error: 'solid_color', 'app_color_background' or "
+                            + "'wallpaper' should be provided as an argument");
+                    return -1;
+            }
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: 'solid_color', 'app_color_background' or "
+                        + "'wallpaper' should be provided as an argument" + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setLetterboxBackgroundType(backgroundType);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxBackgroundColorResource(PrintWriter pw) throws RemoteException {
+        final int colorId;
+        try {
+            String arg = getNextArgRequired();
+            colorId = mInternal.mContext.getResources()
+                    .getIdentifier(arg, "color", "com.android.internal");
+        } catch (NotFoundException e) {
+            getErrPrintWriter().println(
+                    "Error: color in '@android:color/resource_name' format should be provided as "
+                            + "an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setLetterboxBackgroundColorResourceId(colorId);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxBackgroundColor(PrintWriter pw) throws RemoteException {
+        final Color color;
+        try {
+            String arg = getNextArgRequired();
+            color = Color.valueOf(Color.parseColor(arg));
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: color in #RRGGBB format should be provided as "
+                            + "an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setLetterboxBackgroundColor(color);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxBackgroundWallpaperBlurRadius(PrintWriter pw)
+            throws RemoteException {
+        final int radius;
+        try {
+            String arg = getNextArgRequired();
+            radius = Integer.parseInt(arg);
+        } catch (NumberFormatException  e) {
+            getErrPrintWriter().println("Error: blur radius format " + e);
+            return -1;
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: blur radius should be provided as an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setLetterboxBackgroundWallpaperBlurRadius(radius);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxBackgroundWallpaperDarkScrimAlpha(PrintWriter pw)
+            throws RemoteException {
+        final float alpha;
+        try {
+            String arg = getNextArgRequired();
+            alpha = Float.parseFloat(arg);
+        } catch (NumberFormatException  e) {
+            getErrPrintWriter().println("Error: bad alpha format " + e);
+            return -1;
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: alpha should be provided as an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxHorizontalPositionMultiplier(PrintWriter pw) throws RemoteException {
+        final float multiplier;
+        try {
+            String arg = getNextArgRequired();
+            multiplier = Float.parseFloat(arg);
+        } catch (NumberFormatException  e) {
+            getErrPrintWriter().println("Error: bad multiplier format " + e);
+            return -1;
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: multiplier should be provided as an argument " + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxIsReachabilityEnabled(PrintWriter pw) throws RemoteException {
+        String arg = getNextArg();
+        final boolean enabled;
+        switch (arg) {
+            case "true":
+            case "1":
+                enabled = true;
+                break;
+            case "false":
+            case "0":
+                enabled = false;
+                break;
+            default:
+                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
+                return -1;
+        }
+
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setIsReachabilityEnabled(enabled);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxDefaultPositionForReachability(PrintWriter pw)
+            throws RemoteException {
+        @LetterboxReachabilityPosition final int position;
+        try {
+            String arg = getNextArgRequired();
+            switch (arg) {
+                case "left":
+                    position = LETTERBOX_REACHABILITY_POSITION_LEFT;
+                    break;
+                case "center":
+                    position = LETTERBOX_REACHABILITY_POSITION_CENTER;
+                    break;
+                case "right":
+                    position = LETTERBOX_REACHABILITY_POSITION_RIGHT;
+                    break;
+                default:
+                    getErrPrintWriter().println(
+                            "Error: 'left', 'center' or 'right' are expected as an argument");
+                    return -1;
+            }
+        } catch (IllegalArgumentException  e) {
+            getErrPrintWriter().println(
+                    "Error: 'left', 'center' or 'right' are expected as an argument" + e);
+            return -1;
+        }
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setDefaultPositionForReachability(position);
+        }
+        return 0;
+    }
+
+    private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException {
+        if (peekNextArg() == null) {
+            getErrPrintWriter().println("Error: No arguments provided.");
+        }
+        while (peekNextArg() != null) {
+            String arg = getNextArg();
+            switch (arg) {
+                case "--aspectRatio":
+                    runSetFixedOrientationLetterboxAspectRatio(pw);
+                    break;
+                case "--cornerRadius":
+                    runSetLetterboxActivityCornersRadius(pw);
+                    break;
+                case "--backgroundType":
+                    runSetLetterboxBackgroundType(pw);
+                    break;
+                case "--backgroundColor":
+                    runSetLetterboxBackgroundColor(pw);
+                    break;
+                case "--backgroundColorResource":
+                    runSetLetterboxBackgroundColorResource(pw);
+                    break;
+                case "--wallpaperBlurRadius":
+                    runSetLetterboxBackgroundWallpaperBlurRadius(pw);
+                    break;
+                case "--wallpaperDarkScrimAlpha":
+                    runSetLetterboxBackgroundWallpaperDarkScrimAlpha(pw);
+                    break;
+                case "--horizontalPositionMultiplier":
+                    runSetLetterboxHorizontalPositionMultiplier(pw);
+                    break;
+                case "--isReachabilityEnabled":
+                    runSetLetterboxIsReachabilityEnabled(pw);
+                    break;
+                case "--defaultPositionForReachability":
+                    runSetLetterboxDefaultPositionForReachability(pw);
+                    break;
+                default:
+                    getErrPrintWriter().println(
+                            "Error: Unrecognized letterbox style option: " + arg);
+                    return -1;
+            }
+        }
+        return 0;
+    }
+
+    private int runResetLetterboxStyle(PrintWriter pw) throws RemoteException {
+        if (peekNextArg() == null) {
+            resetLetterboxStyle();
+        }
+        synchronized (mInternal.mGlobalLock) {
+            while (peekNextArg() != null) {
+                String arg = getNextArg();
+                switch (arg) {
+                    case "aspectRatio":
+                        mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
+                        break;
+                    case "cornerRadius":
+                        mLetterboxConfiguration.resetLetterboxActivityCornersRadius();
+                        break;
+                    case "backgroundType":
+                        mLetterboxConfiguration.resetLetterboxBackgroundType();
+                        break;
+                    case "backgroundColor":
+                        mLetterboxConfiguration.resetLetterboxBackgroundColor();
+                        break;
+                    case "wallpaperBlurRadius":
+                        mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadius();
+                        break;
+                    case "wallpaperDarkScrimAlpha":
+                        mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
+                        break;
+                    case "horizontalPositionMultiplier":
+                        mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
+                        break;
+                    case "isReachabilityEnabled":
+                        mLetterboxConfiguration.getIsReachabilityEnabled();
+                        break;
+                    case "defaultPositionForReachability":
+                        mLetterboxConfiguration.getDefaultPositionForReachability();
+                        break;
+                    default:
+                        getErrPrintWriter().println(
+                                "Error: Unrecognized letterbox style option: " + arg);
+                        return -1;
+                }
+            }
+        }
+        return 0;
+    }
+
     private int runSetMultiWindowConfig() {
         if (peekNextArg() == null) {
             getErrPrintWriter().println("Error: No arguments provided.");
@@ -622,6 +987,47 @@
         return 0;
     }
 
+    private void resetLetterboxStyle() {
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
+            mLetterboxConfiguration.resetLetterboxActivityCornersRadius();
+            mLetterboxConfiguration.resetLetterboxBackgroundType();
+            mLetterboxConfiguration.resetLetterboxBackgroundColor();
+            mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadius();
+            mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
+            mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
+            mLetterboxConfiguration.resetIsReachabilityEnabled();
+            mLetterboxConfiguration.resetDefaultPositionForReachability();
+        }
+    }
+
+    private int runGetLetterboxStyle(PrintWriter pw) throws RemoteException {
+        synchronized (mInternal.mGlobalLock) {
+            pw.println("Corner radius: "
+                    + mLetterboxConfiguration.getLetterboxActivityCornersRadius());
+            pw.println("Horizontal position multiplier: "
+                    + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier());
+            pw.println("Aspect ratio: "
+                    + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
+            pw.println("Is reachability enabled: "
+                    + mLetterboxConfiguration.getIsReachabilityEnabled());
+            pw.println("Default position for reachability: "
+                    + LetterboxConfiguration.letterboxReachabilityPositionToString(
+                            mLetterboxConfiguration.getDefaultPositionForReachability()));
+
+            pw.println("Background type: "
+                    + LetterboxConfiguration.letterboxBackgroundTypeToString(
+                            mLetterboxConfiguration.getLetterboxBackgroundType()));
+            pw.println("    Background color: " + Integer.toHexString(
+                    mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb()));
+            pw.println("    Wallpaper blur radius: "
+                    + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius());
+            pw.println("    Wallpaper dark scrim alpha: "
+                    + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha());
+        }
+        return 0;
+    }
+
     private int runReset(PrintWriter pw) throws RemoteException {
         int displayId = getDisplayId(getNextArg());
 
@@ -646,6 +1052,12 @@
         // set-ignore-orientation-request
         mInterface.setIgnoreOrientationRequest(displayId, false /* ignoreOrientationRequest */);
 
+        // set-letterbox-style
+        resetLetterboxStyle();
+
+        // set-sandbox-display-apis
+        mInternal.setSandboxDisplayApis(displayId, /* sandboxDisplayApis= */ true);
+
         // set-multi-window-config
         runResetMultiWindowConfig();
 
@@ -680,7 +1092,12 @@
         pw.println("  set-ignore-orientation-request [-d DISPLAY_ID] [true|1|false|0]");
         pw.println("  get-ignore-orientation-request [-d DISPLAY_ID] ");
         pw.println("    If app requested orientation should be ignored.");
+        pw.println("  set-sandbox-display-apis [true|1|false|0]");
+        pw.println("    Sets override of Display APIs getRealSize / getRealMetrics to reflect ");
+        pw.println("    DisplayArea of the activity, or the window bounds if in letterbox or");
+        pw.println("    Size Compat Mode.");
 
+        printLetterboxHelp(pw);
         printMultiWindowConfigHelp(pw);
 
         pw.println("  reset [-d DISPLAY_ID]");
@@ -693,6 +1110,61 @@
         }
     }
 
+    private void printLetterboxHelp(PrintWriter pw) {
+        pw.println("  set-letterbox-style");
+        pw.println("    Sets letterbox style using the following options:");
+        pw.println("      --aspectRatio aspectRatio");
+        pw.println("        Aspect ratio of letterbox for fixed orientation. If aspectRatio <= "
+                + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+        pw.println("        both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will");
+        pw.println("        be ignored and framework implementation will determine aspect ratio.");
+        pw.println("      --cornerRadius radius");
+        pw.println("        Corners radius for activities in the letterbox mode. If radius < 0,");
+        pw.println("        both it and R.integer.config_letterboxActivityCornersRadius will be");
+        pw.println("        ignored and corners of the activity won't be rounded.");
+        pw.println("      --backgroundType [reset|solid_color|app_color_background");
+        pw.println("          |app_color_background_floating|wallpaper]");
+        pw.println("        Type of background used in the letterbox mode.");
+        pw.println("      --backgroundColor color");
+        pw.println("        Color of letterbox which is be used when letterbox background type");
+        pw.println("        is 'solid-color'. Use (set)get-letterbox-style to check and control");
+        pw.println("        letterbox background type. See Color#parseColor for allowed color");
+        pw.println("        formats (#RRGGBB and some colors by name, e.g. magenta or olive).");
+        pw.println("      --backgroundColorResource resource_name");
+        pw.println("        Color resource name of letterbox background which is used when");
+        pw.println("        background type is 'solid-color'. Use (set)get-letterbox-style to");
+        pw.println("        check and control background type. Parameter is a color resource");
+        pw.println("        name, for example, @android:color/system_accent2_50.");
+        pw.println("      --wallpaperBlurRadius radius");
+        pw.println("        Blur radius for 'wallpaper' letterbox background. If radius <= 0");
+        pw.println("        both it and R.dimen.config_letterboxBackgroundWallpaperBlurRadius");
+        pw.println("        are ignored and 0 is used.");
+        pw.println("      --wallpaperDarkScrimAlpha alpha");
+        pw.println("        Alpha of a black translucent scrim shown over 'wallpaper'");
+        pw.println("        letterbox background. If alpha < 0 or >= 1 both it and");
+        pw.println("        R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha are ignored");
+        pw.println("        and 0.0 (transparent) is used instead.");
+        pw.println("      --horizontalPositionMultiplier multiplier");
+        pw.println("        Horizontal position of app window center. If multiplier < 0 or > 1,");
+        pw.println("        both it and R.dimen.config_letterboxHorizontalPositionMultiplier");
+        pw.println("        are ignored and central position (0.5) is used.");
+        pw.println("      --isReachabilityEnabled [true|1|false|0]");
+        pw.println("        Whether reachability repositioning is allowed for letterboxed");
+        pw.println("        fullscreen apps in landscape device orientation.");
+        pw.println("      --defaultPositionForReachability [left|center|right]");
+        pw.println("        Default horizontal position of app window  when reachability is.");
+        pw.println("        enabled.");
+        pw.println("  reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
+        pw.println("      |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
+        pw.println("      |horizontalPositionMultiplier|isReachabilityEnabled");
+        pw.println("      |defaultPositionMultiplierForReachability]");
+        pw.println("    Resets overrides to default values for specified properties separated");
+        pw.println("    by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'.");
+        pw.println("    If no arguments provided, all values will be reset.");
+        pw.println("  get-letterbox-style");
+        pw.println("    Prints letterbox style configuration.");
+    }
+
     private void printMultiWindowConfigHelp(PrintWriter pw) {
         pw.println("  set-multi-window-config");
         pw.println("    Sets options to determine if activity should be shown in multi window:");
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index a82a478..6e706e9 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -16,13 +16,23 @@
 
 package com.android.server.wm;
 
+import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
+import static android.app.ActivityManager.isStartResultSuccessful;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_LAUNCH_TASK;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
 import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED;
@@ -33,7 +43,11 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.WindowConfiguration;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -42,14 +56,21 @@
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.RemoteException;
+import android.util.AndroidRuntimeException;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.view.RemoteAnimationAdapter;
 import android.view.SurfaceControl;
 import android.window.IDisplayAreaOrganizerController;
+import android.window.ITaskFragmentOrganizer;
+import android.window.ITaskFragmentOrganizerController;
 import android.window.ITaskOrganizerController;
+import android.window.ITransitionMetricsReporter;
 import android.window.ITransitionPlayer;
 import android.window.IWindowContainerTransactionCallback;
 import android.window.IWindowOrganizerController;
+import android.window.TaskFragmentCreationParams;
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -63,7 +84,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * Server side implementation for the interface for organizing windows
@@ -95,15 +116,26 @@
 
     final TaskOrganizerController mTaskOrganizerController;
     final DisplayAreaOrganizerController mDisplayAreaOrganizerController;
+    final TaskFragmentOrganizerController mTaskFragmentOrganizerController;
 
-    final TransitionController mTransitionController;
+    TransitionController mTransitionController;
+    /**
+     * A Map which manages the relationship between
+     * {@link TaskFragmentCreationParams#getFragmentToken()} and {@link TaskFragment}
+     */
+    @VisibleForTesting
+    final ArrayMap<IBinder, TaskFragment> mLaunchTaskFragments = new ArrayMap<>();
 
     WindowOrganizerController(ActivityTaskManagerService atm) {
         mService = atm;
         mGlobalLock = atm.mGlobalLock;
         mTaskOrganizerController = new TaskOrganizerController(mService);
         mDisplayAreaOrganizerController = new DisplayAreaOrganizerController(mService);
-        mTransitionController = new TransitionController(atm);
+        mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm);
+    }
+
+    void setWindowManager(WindowManagerService wms) {
+        mTransitionController = new TransitionController(mService, wms.mTaskSnapshotController);
     }
 
     TransitionController getTransitionController() {
@@ -122,14 +154,15 @@
 
     @Override
     public void applyTransaction(WindowContainerTransaction t) {
-        enforceTaskPermission("applyTransaction()");
         if (t == null) {
-            throw new IllegalArgumentException("Null transaction passed to applySyncTransaction");
+            throw new IllegalArgumentException("Null transaction passed to applyTransaction");
         }
+        enforceTaskPermission("applyTransaction()", t);
+        final CallerInfo caller = new CallerInfo();
         final long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
-                applyTransaction(t, -1 /*syncId*/, null /*transition*/);
+                applyTransaction(t, -1 /*syncId*/, null /*transition*/, caller);
             }
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -139,10 +172,11 @@
     @Override
     public int applySyncTransaction(WindowContainerTransaction t,
             IWindowContainerTransactionCallback callback) {
-        enforceTaskPermission("applySyncTransaction()");
         if (t == null) {
             throw new IllegalArgumentException("Null transaction passed to applySyncTransaction");
         }
+        enforceTaskPermission("applySyncTransaction()", t);
+        final CallerInfo caller = new CallerInfo();
         final long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
@@ -162,7 +196,7 @@
                 if (callback != null) {
                     syncId = startSyncWithOrganizer(callback);
                 }
-                applyTransaction(t, syncId, null /*transition*/);
+                applyTransaction(t, syncId, null /*transition*/, caller);
                 if (syncId >= 0) {
                     setSyncReady(syncId);
                 }
@@ -177,6 +211,7 @@
     public IBinder startTransition(int type, @Nullable IBinder transitionToken,
             @Nullable WindowContainerTransaction t) {
         enforceTaskPermission("startTransition()");
+        final CallerInfo caller = new CallerInfo();
         final long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
@@ -196,7 +231,7 @@
                             throw new IllegalArgumentException("Can't use legacy transitions in"
                                     + " compatibility mode with no WCT.");
                         }
-                        applyTransaction(t, -1 /* syncId */, null);
+                        applyTransaction(t, -1 /* syncId */, null, caller);
                         return null;
                     }
                     transition = mTransitionController.createTransition(type);
@@ -205,9 +240,9 @@
                 if (t == null) {
                     t = new WindowContainerTransaction();
                 }
-                applyTransaction(t, -1 /*syncId*/, transition);
+                applyTransaction(t, -1 /*syncId*/, transition, caller);
                 if (needsSetReady) {
-                    transition.setReady();
+                    transition.setAllReady();
                 }
                 return transition;
             }
@@ -217,10 +252,47 @@
     }
 
     @Override
+    public int startLegacyTransition(int type, @NonNull RemoteAnimationAdapter adapter,
+            @NonNull IWindowContainerTransactionCallback callback,
+            @NonNull WindowContainerTransaction t) {
+        enforceTaskPermission("startLegacyTransition()");
+        final CallerInfo caller = new CallerInfo();
+        final long ident = Binder.clearCallingIdentity();
+        int syncId;
+        try {
+            synchronized (mGlobalLock) {
+                if (type < 0) {
+                    throw new IllegalArgumentException("Can't create transition with no type");
+                }
+                if (mTransitionController.getTransitionPlayer() != null) {
+                    throw new IllegalArgumentException("Can't use legacy transitions in"
+                            + " when shell transitions are enabled.");
+                }
+                final DisplayContent dc =
+                        mService.mRootWindowContainer.getDisplayContent(DEFAULT_DISPLAY);
+                if (dc.mAppTransition.isTransitionSet()) {
+                    // a transition already exists, so the callback probably won't be called.
+                    return -1;
+                }
+                adapter.setCallingPidUid(caller.mPid, caller.mUid);
+                dc.prepareAppTransition(type);
+                dc.mAppTransition.overridePendingAppTransitionRemote(adapter, true /* sync */);
+                syncId = startSyncWithOrganizer(callback);
+                applyTransaction(t, syncId, null /* transition */, caller);
+                setSyncReady(syncId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+        return syncId;
+    }
+
+    @Override
     public int finishTransition(@NonNull IBinder transitionToken,
             @Nullable WindowContainerTransaction t,
             @Nullable IWindowContainerTransactionCallback callback) {
         enforceTaskPermission("finishTransition()");
+        final CallerInfo caller = new CallerInfo();
         final long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
@@ -231,7 +303,7 @@
                 // apply the incoming transaction before finish in case it alters the visibility
                 // of the participants.
                 if (t != null) {
-                    applyTransaction(t, syncId, null /*transition*/);
+                    applyTransaction(t, syncId, null /*transition*/, caller);
                 }
                 getTransitionController().finishTransition(transitionToken);
                 if (syncId >= 0) {
@@ -247,17 +319,19 @@
     /**
      * @param syncId If non-null, this will be a sync-transaction.
      * @param transition A transition to collect changes into.
+     * @param caller Info about the calling process.
      */
     private void applyTransaction(@NonNull WindowContainerTransaction t, int syncId,
-            @Nullable Transition transition) {
+            @Nullable Transition transition, @NonNull CallerInfo caller) {
         int effects = 0;
         ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Apply window transaction, syncId=%d", syncId);
         mService.deferWindowLayout();
+        mService.mTaskSupervisor.setDeferRootVisibilityUpdate(true /* deferUpdate */);
         try {
             if (transition != null) {
                 // First check if we have a display rotation transition and if so, update it.
                 final DisplayContent dc = DisplayRotation.getDisplayFromTransition(transition);
-                if (dc != null && transition.mChanges.get(dc).mRotation != dc.getRotation()) {
+                if (dc != null && transition.mChanges.get(dc).hasChanged(dc)) {
                     // Go through all tasks and collect them before the rotation
                     // TODO(shell-transitions): move collect() to onConfigurationChange once
                     //       wallpaper handling is synchronized.
@@ -303,7 +377,8 @@
                 final boolean isInLockTaskMode = mService.isInLockTaskMode();
                 for (int i = 0; i < hopSize; ++i) {
                     effects |= applyHierarchyOp(hops.get(i), effects, syncId, transition,
-                            isInLockTaskMode);
+                            isInLockTaskMode, caller, t.getErrorCallbackToken(),
+                            t.getTaskFragmentOrganizer());
                 }
             }
             // Queue-up bounds-change transactions for tasks which are now organized. Do
@@ -341,6 +416,7 @@
                 task.setMainWindowSizeChangeTransaction(sft);
             }
             if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) {
+                mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */);
                 // Already calls ensureActivityConfig
                 mService.mRootWindowContainer.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS);
                 mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
@@ -362,6 +438,7 @@
                 mService.addWindowLayoutReasons(LAYOUT_REASON_CONFIG_CHANGED);
             }
         } finally {
+            mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */);
             mService.continueWindowLayout();
         }
     }
@@ -389,6 +466,10 @@
                 container.onRequestedOverrideConfigurationChanged(c);
             }
             effects |= TRANSACT_EFFECTS_CLIENT_CONFIG;
+            if (windowMask != 0 && container.isEmbedded()) {
+                // Changing bounds of the embedded TaskFragments may result in lifecycle changes.
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
+            }
         }
         if ((change.getChangeMask() & WindowContainerTransaction.Change.CHANGE_FOCUSABLE) != 0) {
             if (container.setFocusable(change.getFocusable())) {
@@ -402,7 +483,15 @@
                 throw new UnsupportedOperationException("Not supported to set multi-window"
                         + " windowing mode during locked task mode.");
             }
+
+            final int prevMode = container.getWindowingMode();
             container.setWindowingMode(windowingMode);
+            if (prevMode != container.getWindowingMode()) {
+                // The activity in the container may become focusable or non-focusable due to
+                // windowing modes changes (such as entering or leaving pinned windowing mode),
+                // so also apply the lifecycle effects to this transaction.
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
+            }
         }
         return effects;
     }
@@ -458,7 +547,9 @@
     }
 
     private int applyHierarchyOp(WindowContainerTransaction.HierarchyOp hop, int effects,
-            int syncId, @Nullable Transition transition, boolean isInLockTaskMode) {
+            int syncId, @Nullable Transition transition, boolean isInLockTaskMode,
+            @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken,
+            @Nullable ITaskFragmentOrganizer organizer) {
         final int type = hop.getType();
         switch (type) {
             case HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT: {
@@ -480,7 +571,7 @@
                 } else if (!task.mCreatedByOrganizer) {
                     throw new UnsupportedOperationException(
                             "Cannot set non-organized task as adjacent flag root: " + wc);
-                } else if (task.mAdjacentTask == null) {
+                } else if (task.getAdjacentTaskFragment() == null) {
                     throw new UnsupportedOperationException(
                             "Cannot set non-adjacent task as adjacent flag root: " + wc);
                 }
@@ -501,13 +592,15 @@
             return effects;
         }
 
+        final WindowContainer wc;
+        final IBinder fragmentToken;
         switch (type) {
             case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT:
                 effects |= reparentChildrenTasksHierarchyOp(hop, transition, syncId);
                 break;
             case HIERARCHY_OP_TYPE_REORDER:
             case HIERARCHY_OP_TYPE_REPARENT:
-                final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer());
+                wc = WindowContainer.fromBinder(hop.getContainer());
                 if (wc == null || !wc.isAttached()) {
                     Slog.e(TAG, "Attempt to operate on detached container: " + wc);
                     break;
@@ -537,11 +630,151 @@
                 effects |= sanitizeAndApplyHierarchyOp(wc, hop);
                 break;
             case HIERARCHY_OP_TYPE_LAUNCH_TASK:
+                mService.mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS,
+                        "launchTask HierarchyOp");
                 final Bundle launchOpts = hop.getLaunchOptions();
                 final int taskId = launchOpts.getInt(
                         WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID);
                 launchOpts.remove(WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID);
-                mService.startActivityFromRecents(taskId, launchOpts);
+                final SafeActivityOptions safeOptions =
+                        SafeActivityOptions.fromBundle(launchOpts, caller.mPid, caller.mUid);
+                final Integer[] starterResult = { null };
+                // startActivityFromRecents should not be called in lock.
+                mService.mH.post(() -> {
+                    try {
+                        starterResult[0] = mService.mTaskSupervisor.startActivityFromRecents(
+                                caller.mPid, caller.mUid, taskId, safeOptions);
+                    } catch (Throwable t) {
+                        starterResult[0] = ActivityManager.START_CANCELED;
+                        Slog.w(TAG, t);
+                    }
+                    synchronized (mGlobalLock) {
+                        mGlobalLock.notifyAll();
+                    }
+                });
+                while (starterResult[0] == null) {
+                    try {
+                        mGlobalLock.wait();
+                    } catch (InterruptedException ignored) {
+                    }
+                }
+                break;
+            case HIERARCHY_OP_TYPE_PENDING_INTENT:
+                String resolvedType = hop.getActivityIntent() != null
+                        ? hop.getActivityIntent().resolveTypeIfNeeded(
+                                mService.mContext.getContentResolver())
+                        : null;
+
+                Bundle options = null;
+                if (hop.getPendingIntent().isActivity()) {
+                    // Set the context display id as preferred for this activity launches, so that
+                    // it can land on caller's display. Or just brought the task to front at the
+                    // display where it was on since it has higher preference.
+                    ActivityOptions activityOptions = hop.getLaunchOptions() != null
+                            ? new ActivityOptions(hop.getLaunchOptions())
+                            : ActivityOptions.makeBasic();
+                    activityOptions.setCallerDisplayId(DEFAULT_DISPLAY);
+                    options = activityOptions.toBundle();
+                }
+
+                mService.mAmInternal.sendIntentSender(hop.getPendingIntent().getTarget(),
+                        hop.getPendingIntent().getWhitelistToken(), 0 /* code */,
+                        hop.getActivityIntent(), resolvedType, null /* finishReceiver */,
+                        null /* requiredPermission */, options);
+                break;
+            case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT:
+                final TaskFragmentCreationParams taskFragmentCreationOptions =
+                        hop.getTaskFragmentCreationOptions();
+                createTaskFragment(taskFragmentCreationOptions, errorCallbackToken);
+                break;
+            case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT:
+                wc = WindowContainer.fromBinder(hop.getContainer());
+                if (wc == null || !wc.isAttached()) {
+                    Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc);
+                    break;
+                }
+                final TaskFragment taskFragment = wc.asTaskFragment();
+                if (taskFragment == null || taskFragment.asTask() != null) {
+                    throw new IllegalArgumentException(
+                            "Can only delete organized TaskFragment, but not Task.");
+                }
+                effects |= deleteTaskFragment(taskFragment, errorCallbackToken);
+                break;
+            case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
+                fragmentToken = hop.getContainer();
+                if (!mLaunchTaskFragments.containsKey(fragmentToken)) {
+                    final Throwable exception = new IllegalArgumentException(
+                            "Not allowed to operate with invalid fragment token");
+                    sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+                    break;
+                }
+                final Intent activityIntent = hop.getActivityIntent();
+                final Bundle activityOptions = hop.getLaunchOptions();
+                final TaskFragment tf = mLaunchTaskFragments.get(fragmentToken);
+                final int result = mService.getActivityStartController()
+                        .startActivityInTaskFragment(tf, activityIntent, activityOptions,
+                                hop.getCallingActivity());
+                if (!isStartResultSuccessful(result)) {
+                    sendTaskFragmentOperationFailure(tf.getTaskFragmentOrganizer(),
+                            errorCallbackToken,
+                            convertStartFailureToThrowable(result, activityIntent));
+                }
+                break;
+            case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT:
+                fragmentToken = hop.getNewParent();
+                final ActivityRecord activity = ActivityRecord.forTokenLocked(hop.getContainer());
+                if (!mLaunchTaskFragments.containsKey(fragmentToken) || activity == null) {
+                    final Throwable exception = new IllegalArgumentException(
+                            "Not allowed to operate with invalid fragment token or activity.");
+                    sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+                    break;
+                }
+                activity.reparent(mLaunchTaskFragments.get(fragmentToken), POSITION_TOP);
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
+                break;
+            case HIERARCHY_OP_TYPE_REPARENT_CHILDREN:
+                final WindowContainer oldParent = WindowContainer.fromBinder(hop.getContainer());
+                final WindowContainer newParent = hop.getNewParent() != null
+                        ? WindowContainer.fromBinder(hop.getNewParent())
+                        : null;
+                if (oldParent == null || !oldParent.isAttached()) {
+                    Slog.e(TAG, "Attempt to operate on unknown or detached container: "
+                            + oldParent);
+                    break;
+                }
+                reparentTaskFragment(oldParent, newParent, errorCallbackToken);
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
+                break;
+            case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS:
+                fragmentToken = hop.getContainer();
+                final IBinder adjacentFragmentToken = hop.getAdjacentRoot();
+                final TaskFragment tf1 = mLaunchTaskFragments.get(fragmentToken);
+                final TaskFragment tf2 = adjacentFragmentToken != null
+                        ? mLaunchTaskFragments.get(adjacentFragmentToken)
+                        : null;
+                if (tf1 == null || (adjacentFragmentToken != null && tf2 == null)) {
+                    final Throwable exception = new IllegalArgumentException(
+                            "Not allowed to set adjacent on invalid fragment tokens");
+                    sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+                    break;
+                }
+                tf1.setAdjacentTaskFragment(tf2);
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
+
+                final Bundle bundle = hop.getLaunchOptions();
+                final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams =
+                        bundle != null ? new WindowContainerTransaction.TaskFragmentAdjacentParams(
+                                bundle) : null;
+                if (adjacentParams == null) {
+                    break;
+                }
+
+                tf1.setDelayLastActivityRemoval(
+                        adjacentParams.shouldDelayPrimaryLastActivityRemoval());
+                if (tf2 != null) {
+                    tf2.setDelayLastActivityRemoval(
+                            adjacentParams.shouldDelaySecondaryLastActivityRemoval());
+                }
                 break;
         }
         return effects;
@@ -661,24 +894,31 @@
         // We want to collect the tasks first before re-parenting to avoid array shifting on us.
         final ArrayList<Task> tasksToReparent = new ArrayList<>();
 
-        currentParent.forAllTasks((Consumer<Task>) (task) -> {
+        currentParent.forAllTasks((Function<Task, Boolean>) task -> {
             Slog.i(TAG, " Processing task=" + task);
-            if (task.mCreatedByOrganizer
-                    || task.getParent() != finalCurrentParent) {
+            final boolean reparent;
+            if (task.mCreatedByOrganizer || task.getParent() != finalCurrentParent) {
                 // We only care about non-organized task that are direct children of the thing we
                 // are reparenting from.
-                return;
+                return false;
             }
             if (newParentInMultiWindow && !task.supportsMultiWindowInDisplayArea(newParentTda)) {
                 Slog.e(TAG, "reparentChildrenTasksHierarchyOp non-resizeable task to multi window,"
                         + " task=" + task);
-                return;
+                return false;
             }
-            if (!ArrayUtils.contains(hop.getActivityTypes(), task.getActivityType())) return;
-            if (!ArrayUtils.contains(hop.getWindowingModes(), task.getWindowingMode())) return;
+            if (!ArrayUtils.contains(hop.getActivityTypes(), task.getActivityType())
+                    || !ArrayUtils.contains(hop.getWindowingModes(), task.getWindowingMode())) {
+                return false;
+            }
 
-            tasksToReparent.add(task);
-        }, !hop.getToTop());
+            if (hop.getToTop()) {
+                tasksToReparent.add(0, task);
+            } else {
+                tasksToReparent.add(task);
+            }
+            return hop.getReparentTopOnly() && tasksToReparent.size() == 1;
+        });
 
         final int count = tasksToReparent.size();
         for (int i = 0; i < count; ++i) {
@@ -704,19 +944,20 @@
     }
 
     private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) {
-        final Task root1 = WindowContainer.fromBinder(hop.getContainer()).asTask();
-        final Task root2 = WindowContainer.fromBinder(hop.getAdjacentRoot()).asTask();
+        final TaskFragment root1 = WindowContainer.fromBinder(hop.getContainer()).asTaskFragment();
+        final TaskFragment root2 =
+                WindowContainer.fromBinder(hop.getAdjacentRoot()).asTaskFragment();
         if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) {
             throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by"
                     + " organizer root1=" + root1 + " root2=" + root2);
         }
-        root1.setAdjacentTask(root2);
+        root1.setAdjacentTaskFragment(root2);
         return TRANSACT_EFFECTS_LIFECYCLE;
     }
 
     private void sanitizeWindowContainer(WindowContainer wc) {
-        if (!(wc instanceof Task) && !(wc instanceof DisplayArea)) {
-            throw new RuntimeException("Invalid token in task or displayArea transaction");
+        if (!(wc instanceof TaskFragment) && !(wc instanceof DisplayArea)) {
+            throw new RuntimeException("Invalid token in task fragment or displayArea transaction");
         }
     }
 
@@ -747,6 +988,11 @@
         return mDisplayAreaOrganizerController;
     }
 
+    @Override
+    public ITaskFragmentOrganizerController getTaskFragmentOrganizerController() {
+        return mTaskFragmentOrganizerController;
+    }
+
     @VisibleForTesting
     int startSyncWithOrganizer(IWindowContainerTransactionCallback callback) {
         int id = mService.mWindowManager.mSyncEngine.startSyncSet(this);
@@ -795,7 +1041,235 @@
         }
     }
 
+    @Override
+    public ITransitionMetricsReporter getTransitionMetricsReporter() {
+        return mTransitionController.mTransitionMetricsReporter;
+    }
+
+    /** Whether the configuration changes are important to report back to an organizer. */
+    static boolean configurationsAreEqualForOrganizer(
+            Configuration newConfig, @Nullable Configuration oldConfig) {
+        if (oldConfig == null) {
+            return false;
+        }
+        int cfgChanges = newConfig.diff(oldConfig);
+        final int winCfgChanges = (cfgChanges & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0
+                ? (int) newConfig.windowConfiguration.diff(oldConfig.windowConfiguration,
+                true /* compareUndefined */) : 0;
+        if ((winCfgChanges & CONTROLLABLE_WINDOW_CONFIGS) == 0) {
+            cfgChanges &= ~ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+        }
+        return (cfgChanges & CONTROLLABLE_CONFIGS) == 0;
+    }
+
     private void enforceTaskPermission(String func) {
         mService.enforceTaskPermission(func);
     }
+
+    private void enforceTaskPermission(String func, WindowContainerTransaction t) {
+        if (t == null || t.getTaskFragmentOrganizer() == null) {
+            enforceTaskPermission(func);
+            return;
+        }
+
+        // Apps may not have the permission to manage Tasks, but we are allowing apps to manage
+        // TaskFragments belonging to their own Task.
+        enforceOperationsAllowedForTaskFragmentOrganizer(func, t);
+    }
+
+    /**
+     * Makes sure that the transaction only contains operations that are allowed for the
+     * {@link WindowContainerTransaction#getTaskFragmentOrganizer()}.
+     */
+    private void enforceOperationsAllowedForTaskFragmentOrganizer(
+            String func, WindowContainerTransaction t) {
+        final ITaskFragmentOrganizer organizer = t.getTaskFragmentOrganizer();
+
+        // Configuration changes
+        final Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries =
+                t.getChanges().entrySet().iterator();
+        while (entries.hasNext()) {
+            final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = entries.next();
+            // Only allow to apply changes to TaskFragment that is created by this organizer.
+            enforceTaskFragmentOrganized(func, WindowContainer.fromBinder(entry.getKey()),
+                    organizer);
+        }
+
+        // Hierarchy changes
+        final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps();
+        for (int i = hops.size() - 1; i >= 0; i--) {
+            final WindowContainerTransaction.HierarchyOp hop = hops.get(i);
+            final int type = hop.getType();
+            // Check for each type of the operations that are allowed for TaskFragmentOrganizer.
+            switch (type) {
+                case HIERARCHY_OP_TYPE_REORDER:
+                case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT:
+                    enforceTaskFragmentOrganized(func,
+                            WindowContainer.fromBinder(hop.getContainer()), organizer);
+                    break;
+                case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS:
+                    enforceTaskFragmentOrganized(func,
+                            WindowContainer.fromBinder(hop.getContainer()), organizer);
+                    enforceTaskFragmentOrganized(func,
+                            WindowContainer.fromBinder(hop.getAdjacentRoot()),
+                            organizer);
+                    break;
+                case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT:
+                    // We are allowing organizer to create TaskFragment. We will check the
+                    // ownerToken in #createTaskFragment, and trigger error callback if that is not
+                    // valid.
+                case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
+                case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT:
+                case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS:
+                    // We are allowing organizer to start/reparent activity to a TaskFragment it
+                    // created, or set two TaskFragments adjacent to each other. Nothing to check
+                    // here because the TaskFragment may not be created yet, but will be created in
+                    // the same transaction.
+                    break;
+                case HIERARCHY_OP_TYPE_REPARENT_CHILDREN:
+                    enforceTaskFragmentOrganized(func,
+                            WindowContainer.fromBinder(hop.getContainer()), organizer);
+                    if (hop.getNewParent() != null) {
+                        enforceTaskFragmentOrganized(func,
+                                WindowContainer.fromBinder(hop.getNewParent()),
+                                organizer);
+                    }
+                    break;
+                default:
+                    // Other types of hierarchy changes are not allowed.
+                    String msg = "Permission Denial: " + func + " from pid="
+                            + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                            + " trying to apply a hierarchy change that is not allowed for"
+                            + " TaskFragmentOrganizer=" + organizer;
+                    Slog.w(TAG, msg);
+                    throw new SecurityException(msg);
+            }
+        }
+    }
+
+    private void enforceTaskFragmentOrganized(String func, @Nullable WindowContainer wc,
+            ITaskFragmentOrganizer organizer) {
+        if (wc == null) {
+            Slog.e(TAG, "Attempt to operate on window that no longer exists");
+            return;
+        }
+
+        final TaskFragment tf = wc.asTaskFragment();
+        if (tf == null || !tf.hasTaskFragmentOrganizer(organizer)) {
+            String msg = "Permission Denial: " + func + " from pid=" + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid() + " trying to modify window container not"
+                    + " belonging to the TaskFragmentOrganizer=" + organizer;
+            Slog.w(TAG, msg);
+            throw new SecurityException(msg);
+        }
+    }
+
+    void createTaskFragment(@NonNull TaskFragmentCreationParams creationParams,
+            @Nullable IBinder errorCallbackToken) {
+        final ActivityRecord ownerActivity =
+                ActivityRecord.forTokenLocked(creationParams.getOwnerToken());
+        final ITaskFragmentOrganizer organizer = ITaskFragmentOrganizer.Stub.asInterface(
+                creationParams.getOrganizer().asBinder());
+
+        if (ownerActivity == null || ownerActivity.getTask() == null) {
+            final Throwable exception =
+                    new IllegalArgumentException("Not allowed to operate with invalid ownerToken");
+            sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+            return;
+        }
+        // The ownerActivity has to belong to the same app as the root Activity of the target Task.
+        final ActivityRecord rootActivity = ownerActivity.getTask().getRootActivity();
+        if (rootActivity.getUid() != ownerActivity.getUid()) {
+            final Throwable exception =
+                    new IllegalArgumentException("Not allowed to operate with the ownerToken while "
+                            + "the root activity of the target task belong to the different app");
+            sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+            return;
+        }
+        final TaskFragment taskFragment = new TaskFragment(mService,
+                creationParams.getFragmentToken(), true /* createdByOrganizer */);
+        // Set task fragment organizer immediately, since it might have to be notified about further
+        // actions.
+        taskFragment.setTaskFragmentOrganizer(
+                creationParams.getOrganizer(), ownerActivity.getPid());
+        ownerActivity.getTask().addChild(taskFragment, POSITION_TOP);
+        taskFragment.setWindowingMode(creationParams.getWindowingMode());
+        taskFragment.setBounds(creationParams.getInitialBounds());
+        mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment);
+    }
+
+    void reparentTaskFragment(@NonNull WindowContainer oldParent,
+            @Nullable WindowContainer newParent,  @Nullable IBinder errorCallbackToken) {
+        WindowContainer parent = newParent;
+        if (parent == null && oldParent.asTaskFragment() != null) {
+            parent = oldParent.asTaskFragment().getTask();
+        }
+        if (parent == null) {
+            final Throwable exception =
+                    new IllegalArgumentException("Not allowed to operate with invalid container");
+            sendTaskFragmentOperationFailure(oldParent.asTaskFragment().getTaskFragmentOrganizer(),
+                    errorCallbackToken, exception);
+            return;
+        }
+        while (oldParent.hasChild()) {
+            oldParent.getChildAt(0).reparent(parent, POSITION_TOP);
+        }
+    }
+
+    private int deleteTaskFragment(@NonNull TaskFragment taskFragment,
+            @Nullable IBinder errorCallbackToken) {
+        final int index = mLaunchTaskFragments.indexOfValue(taskFragment);
+        if (index < 0) {
+            final Throwable exception =
+                    new IllegalArgumentException("Not allowed to operate with invalid "
+                            + "taskFragment");
+            sendTaskFragmentOperationFailure(taskFragment.getTaskFragmentOrganizer(),
+                    errorCallbackToken, exception);
+            return 0;
+        }
+        mLaunchTaskFragments.removeAt(index);
+        taskFragment.remove(true /* withTransition */, "deleteTaskFragment");
+        return TRANSACT_EFFECTS_LIFECYCLE;
+    }
+
+    @Nullable
+    TaskFragment getTaskFragment(IBinder tfToken) {
+        return mLaunchTaskFragments.get(tfToken);
+    }
+
+    static class CallerInfo {
+        final int mPid;
+        final int mUid;
+
+        CallerInfo() {
+            mPid = Binder.getCallingPid();
+            mUid = Binder.getCallingUid();
+        }
+    }
+
+    void sendTaskFragmentOperationFailure(@NonNull ITaskFragmentOrganizer organizer,
+            @Nullable IBinder errorCallbackToken, @NonNull Throwable exception) {
+        if (organizer == null) {
+            throw new IllegalArgumentException("Not allowed to operate with invalid organizer");
+        }
+        mService.mTaskFragmentOrganizerController
+                .onTaskFragmentError(organizer, errorCallbackToken, exception);
+    }
+
+    private Throwable convertStartFailureToThrowable(int result, Intent intent) {
+        switch (result) {
+            case ActivityManager.START_INTENT_NOT_RESOLVED:
+            case ActivityManager.START_CLASS_NOT_FOUND:
+                return new ActivityNotFoundException("No Activity found to handle " + intent);
+            case ActivityManager.START_PERMISSION_DENIED:
+                return new SecurityException("Permission denied and not allowed to start activity "
+                        + intent);
+            case ActivityManager.START_CANCELED:
+                return new AndroidRuntimeException("Activity could not be started for " + intent
+                        + " with error code : " + result);
+            default:
+                return new AndroidRuntimeException("Start activity failed with error code : "
+                        + result + " when starting " + intent);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 5540cc5..af38641 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -25,6 +25,13 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.server.am.ActivityManagerService.MY_PID;
+import static com.android.server.wm.ActivityRecord.State.DESTROYED;
+import static com.android.server.wm.ActivityRecord.State.DESTROYING;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STARTED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RELEASE;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CONFIGURATION;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RELEASE;
@@ -32,13 +39,6 @@
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.ActivityTaskManagerService.INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT_MILLIS;
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
-import static com.android.server.wm.Task.ActivityState.DESTROYED;
-import static com.android.server.wm.Task.ActivityState.DESTROYING;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
-import static com.android.server.wm.Task.ActivityState.STARTED;
-import static com.android.server.wm.Task.ActivityState.STOPPING;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -57,6 +57,7 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.LocaleList;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
@@ -75,6 +76,7 @@
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -219,6 +221,10 @@
     /** Whether our process is currently running a {@link IRemoteAnimationRunner} */
     private boolean mRunningRemoteAnimation;
 
+    /** List of "chained" processes that are running remote animations for this process */
+    private final ArrayList<WeakReference<WindowProcessController>> mRemoteAnimationDelegates =
+            new ArrayList<>();
+
     // The bits used for mActivityStateFlags.
     private static final int ACTIVITY_STATE_FLAG_IS_VISIBLE = 1 << 16;
     private static final int ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED = 1 << 17;
@@ -258,7 +264,7 @@
         }
 
         onConfigurationChanged(atm.getGlobalConfiguration());
-        mAtm.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, mName);
+        mAtm.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, mInfo.packageName);
     }
 
     public void setPid(int pid) {
@@ -725,20 +731,23 @@
             canUpdate = true;
         }
 
-        // Compare the z-order of ActivityStacks if both activities landed on same display.
-        if (display == topDisplay
-                && mPreQTopResumedActivity.getRootTask().compareTo(
-                        activity.getRootTask()) <= 0) {
-            canUpdate = true;
+        // Update the topmost activity if the activity has higher z-order than the current
+        // top-resumed activity.
+        if (!canUpdate) {
+            final ActivityRecord ar = topDisplay.getActivity(r -> r == activity,
+                    true /* traverseTopToBottom */, mPreQTopResumedActivity);
+            if (ar != null && ar != mPreQTopResumedActivity) {
+                canUpdate = true;
+            }
         }
 
         if (canUpdate) {
             // Make sure the previous top activity in the process no longer be resumed.
             if (mPreQTopResumedActivity != null && mPreQTopResumedActivity.isState(RESUMED)) {
-                final Task task = mPreQTopResumedActivity.getTask();
-                if (task != null) {
-                    boolean userLeaving = task.shouldBeVisible(null);
-                    task.startPausingLocked(userLeaving, false /* uiSleeping */,
+                final TaskFragment taskFrag = mPreQTopResumedActivity.getTaskFragment();
+                if (taskFrag != null) {
+                    boolean userLeaving = taskFrag.shouldBeVisible(null);
+                    taskFrag.startPausing(userLeaving, false /* uiSleeping */,
                             activity, "top-resumed-changed");
                 }
             }
@@ -809,10 +818,13 @@
         return false;
     }
 
-    void updateNightModeForAllActivities(int nightMode) {
+    // TODO(b/199277065): Re-assess how app-specific locales are applied based on UXR
+    // TODO(b/199277729): Consider whether we need to add special casing for edge cases like
+    //  activity-embeddings etc.
+    void updateAppSpecificSettingsForAllActivities(Integer nightMode, LocaleList localesOverride) {
         for (int i = mActivities.size() - 1; i >= 0; --i) {
             final ActivityRecord r = mActivities.get(i);
-            if (r.setOverrideNightMode(nightMode) && r.mVisibleRequested) {
+            if (r.applyAppSpecificConfig(nightMode, localesOverride) && r.mVisibleRequested) {
                 r.ensureActivityConfiguration(0 /* globalChanges */, true /* preserveWindow */);
             }
         }
@@ -991,7 +1003,7 @@
         // Since there could be more than one activities in a process record, we don't need to
         // compute the OomAdj with each of them, just need to find out the activity with the
         // "best" state, the order would be visible, pausing, stopping...
-        Task.ActivityState bestInvisibleState = DESTROYED;
+        ActivityRecord.State bestInvisibleState = DESTROYED;
         boolean allStoppingFinishing = true;
         boolean visible = false;
         int minTaskLayer = Integer.MAX_VALUE;
@@ -1215,12 +1227,12 @@
                 hasVisibleActivities = true;
             }
 
-            final Task task = r.getTask();
-            if (task != null) {
+            final TaskFragment taskFragment = r.getTaskFragment();
+            if (taskFragment != null) {
                 // There may be a pausing activity that hasn't shown any window and was requested
                 // to be hidden. But pausing is also a visible state, it should be regarded as
                 // visible, so the caller can know the next activity should be resumed.
-                hasVisibleActivities |= task.handleAppDied(this);
+                hasVisibleActivities |= taskFragment.handleAppDied(this);
             }
             r.handleAppDied();
         }
@@ -1596,11 +1608,38 @@
         updateRunningRemoteOrRecentsAnimation();
     }
 
+    /**
+     * Marks another process as a "delegate" animator. This means that process is doing some part
+     * of a remote animation on behalf of this process.
+     */
+    void addRemoteAnimationDelegate(WindowProcessController delegate) {
+        if (!isRunningRemoteTransition()) {
+            throw new IllegalStateException("Can't add a delegate to a process which isn't itself"
+                    + " running a remote animation");
+        }
+        mRemoteAnimationDelegates.add(new WeakReference<>(delegate));
+    }
+
     void updateRunningRemoteOrRecentsAnimation() {
+        if (!isRunningRemoteTransition()) {
+            // Clean-up any delegates
+            for (int i = 0; i < mRemoteAnimationDelegates.size(); ++i) {
+                final WindowProcessController delegate = mRemoteAnimationDelegates.get(i).get();
+                if (delegate == null) continue;
+                delegate.setRunningRemoteAnimation(false);
+                delegate.setRunningRecentsAnimation(false);
+            }
+            mRemoteAnimationDelegates.clear();
+        }
+
         // Posting on handler so WM lock isn't held when we call into AM.
         mAtm.mH.sendMessage(PooledLambda.obtainMessage(
                 WindowProcessListener::setRunningRemoteAnimation, mListener,
-                mRunningRecentsAnimation || mRunningRemoteAnimation));
+                isRunningRemoteTransition()));
+    }
+
+    boolean isRunningRemoteTransition() {
+        return mRunningRecentsAnimation || mRunningRemoteAnimation;
     }
 
     /** Adjusts scheduling group for animation. This method MUST NOT be called inside WM lock. */
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0af6a29..2410d7ec 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -33,6 +33,7 @@
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.SurfaceControl.Transaction;
 import static android.view.SurfaceControl.getGlobalTransaction;
+import static android.view.ViewRootImpl.INSETS_LAYOUT_GENERALIZATION;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
@@ -105,15 +106,17 @@
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_DRAG_RESIZING_DOCKED;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_DRAG_RESIZING_FREEFORM;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME;
+import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER;
+import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT;
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IME;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RESIZE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS;
 import static com.android.server.am.ActivityManagerService.MY_PID;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER;
@@ -151,8 +154,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.WindowManagerService.H.WINDOW_STATE_BLAST_SYNC_TIMEOUT;
 import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION;
-import static com.android.server.wm.WindowManagerService.TYPE_LAYER_MULTIPLIER;
-import static com.android.server.wm.WindowManagerService.TYPE_LAYER_OFFSET;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_REMOVING_FOCUS;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
@@ -191,6 +192,7 @@
 import android.annotation.CallSuper;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityTaskManager;
 import android.app.AppOpsManager;
 import android.app.admin.DevicePolicyCache;
 import android.content.Context;
@@ -200,6 +202,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.gui.TouchOcclusionMode;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Debug;
@@ -209,7 +212,6 @@
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.os.TouchOcclusionMode;
 import android.os.Trace;
 import android.os.WorkSource;
 import android.provider.Settings;
@@ -235,6 +237,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.Surface.Rotation;
 import android.view.SurfaceControl;
@@ -270,7 +273,7 @@
 
 /** A window in the window manager. */
 class WindowState extends WindowContainer<WindowState> implements WindowManagerPolicy.WindowState,
-        InsetsControlTarget {
+        InsetsControlTarget, InputTarget {
     static final String TAG = TAG_WITH_CLASS_NAME ? "WindowState" : TAG_WM;
 
     // The minimal size of a window within the usable area of the freeform root task.
@@ -305,6 +308,8 @@
     @NonNull WindowToken mToken;
     // The same object as mToken if this is an app window and null for non-app windows.
     ActivityRecord mActivityRecord;
+    /** Non-null if this is a starting window. */
+    StartingData mStartingData;
 
     // mAttrs.flags is tested in animation without being locked. If the bits tested are ever
     // modified they will need to be locked.
@@ -367,6 +372,14 @@
     private boolean mRedrawForSyncReported;
 
     /**
+     * {@code true} when the client was still drawing for sync when the sync-set was finished or
+     * cancelled. This can happen if the window goes away during a sync. In this situation we need
+     * to make sure to still apply the postDrawTransaction when it finishes to prevent the client
+     * from getting stuck in a bad state.
+     */
+    boolean mClientWasDrawingForSync = false;
+
+    /**
      * Special mode that is intended only for the rounded corner overlay: during rotation
      * transition, we un-rotate the window token such that the window appears as it did before the
      * rotation.
@@ -745,7 +758,7 @@
     private boolean mIsDimming = false;
 
     private @Nullable InsetsSourceProvider mControllableInsetProvider;
-    private final InsetsState mRequestedInsetsState = new InsetsState();
+    private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
 
     /**
      * Freeze the insets state in some cases that not necessarily keeps up-to-date to the client.
@@ -868,18 +881,23 @@
      */
     @Override
     public boolean getRequestedVisibility(@InternalInsetsType int type) {
-        return mRequestedInsetsState.getSourceOrDefaultVisibility(type);
+        return mRequestedVisibilities.getVisibility(type);
+    }
+
+    /**
+     * Returns all the requested visibilities.
+     *
+     * @return an {@link InsetsVisibilities} as the requested visibilities.
+     */
+    InsetsVisibilities getRequestedVisibilities() {
+        return mRequestedVisibilities;
     }
 
     /**
      * @see #getRequestedVisibility(int)
      */
-    void updateRequestedVisibility(InsetsState state) {
-        for (int i = 0; i < InsetsState.SIZE; i++) {
-            final InsetsSource source = state.peekSource(i);
-            if (source == null) continue;
-            mRequestedInsetsState.addSource(source);
-        }
+    void setRequestedVisibilities(InsetsVisibilities visibilities) {
+        mRequestedVisibilities.set(visibilities);
     }
 
     /**
@@ -1157,7 +1175,7 @@
         if (WindowManager.LayoutParams.isSystemAlertWindowType(mAttrs.type)) {
             return TouchOcclusionMode.USE_OPACITY;
         }
-        if (isAnimating(PARENTS | TRANSITION, ANIMATION_TYPE_ALL)) {
+        if (isAnimating(PARENTS | TRANSITION, ANIMATION_TYPE_ALL) || inTransition()) {
             return TouchOcclusionMode.USE_OPACITY;
         }
         return TouchOcclusionMode.BLOCK_UNTRUSTED;
@@ -1254,8 +1272,8 @@
         frame.inset(left, top, right, bottom);
     }
 
-    void computeFrameAndUpdateSourceFrame() {
-        computeFrame();
+    void computeFrameAndUpdateSourceFrame(DisplayFrames displayFrames) {
+        computeFrame(displayFrames);
         // Update the source frame to provide insets to other windows during layout. If the
         // simulated frames exist, then this is not computing a stable result so just skip.
         if (mControllableInsetProvider != null && mSimulatedWindowFrames == null) {
@@ -1266,7 +1284,7 @@
     /**
      * Perform standard frame computation. The result can be obtained with getFrame() if so desired.
      */
-    void computeFrame() {
+    void computeFrame(DisplayFrames displayFrames) {
         if (mWillReplaceWindow && (mAnimatingExit || !mReplacingRemoveRequested)) {
             // This window is being replaced and either already got information that it's being
             // removed or we are still waiting for some information. Because of this we don't
@@ -1379,7 +1397,8 @@
         final int fw = windowFrames.mFrame.width();
         final int fh = windowFrames.mFrame.height();
 
-        applyGravityAndUpdateFrame(windowFrames, layoutContainingFrame, layoutDisplayFrame);
+        applyGravityAndUpdateFrame(windowFrames, layoutContainingFrame, layoutDisplayFrame,
+                displayFrames);
 
         if (mAttrs.type == TYPE_DOCK_DIVIDER) {
             if (!windowFrames.mFrame.equals(windowFrames.mLastFrame)) {
@@ -1427,14 +1446,12 @@
         }
     }
 
-    // TODO: Look into whether this override is still necessary.
     @Override
     public Rect getBounds() {
-        if (mActivityRecord != null) {
-            return mActivityRecord.getBounds();
-        } else {
-            return super.getBounds();
-        }
+        // The window bounds are used for layout in screen coordinates. If the token has bounds for
+        // size compatibility mode, its configuration bounds are app based coordinates which should
+        // not be used for layout.
+        return mToken.hasSizeCompatBounds() ? mToken.getBounds() : super.getBounds();
     }
 
     /** Retrieves the current frame of the window that the application sees. */
@@ -1472,6 +1489,18 @@
         return mAttrs;
     }
 
+    WindowManager.LayoutParams getLayoutingAttrs(int rotation) {
+        if (!INSETS_LAYOUT_GENERALIZATION) {
+            return mAttrs;
+        }
+        final WindowManager.LayoutParams[] paramsForRotation = mAttrs.paramsForRotation;
+        if (paramsForRotation == null || paramsForRotation.length != 4
+                || paramsForRotation[rotation] == null) {
+            return mAttrs;
+        }
+        return paramsForRotation[rotation];
+    }
+
     /** Retrieves the flags used to disable system UI functions. */
     int getDisableFlags() {
         return mDisableFlags;
@@ -1698,7 +1727,8 @@
         return state;
     }
 
-    int getDisplayId() {
+    @Override
+    public int getDisplayId() {
         final DisplayContent displayContent = getDisplayContent();
         if (displayContent == null) {
             return Display.INVALID_DISPLAY;
@@ -1706,10 +1736,29 @@
         return displayContent.getDisplayId();
     }
 
+    @Override
+    public WindowState getWindowState() {
+        return this;
+    }
+
+    @Override
+    public IWindow getIWindow() {
+        return mClient;
+    }
+
+    @Override
+    public int getPid() {
+        return mSession.mPid;
+    }
+
     Task getTask() {
         return mActivityRecord != null ? mActivityRecord.getTask() : null;
     }
 
+    @Nullable TaskFragment getTaskFragment() {
+        return mActivityRecord != null ? mActivityRecord.getTaskFragment() : null;
+    }
+
     @Nullable Task getRootTask() {
         final Task task = getTask();
         if (task != null) {
@@ -1837,9 +1886,8 @@
         return super.hasContentToDisplay();
     }
 
-    @Override
-    boolean isVisible() {
-        return wouldBeVisibleIfPolicyIgnored() && isVisibleByPolicy()
+    private boolean isVisibleByPolicyOrInsets() {
+        return isVisibleByPolicy()
                 // If we don't have a provider, this window isn't used as a window generating
                 // insets, so nobody can hide it over the inset APIs.
                 && (mControllableInsetProvider == null
@@ -1847,11 +1895,18 @@
     }
 
     @Override
+    boolean isVisible() {
+        return wouldBeVisibleIfPolicyIgnored() && isVisibleByPolicyOrInsets();
+    }
+
+    @Override
     boolean isVisibleRequested() {
-        if (shouldCheckTokenVisibleRequested()) {
-            return isVisible() && mToken.isVisibleRequested();
+        final boolean localVisibleRequested =
+                wouldBeVisibleRequestedIfPolicyIgnored() && isVisibleByPolicyOrInsets();
+        if (localVisibleRequested && shouldCheckTokenVisibleRequested()) {
+            return mToken.isVisibleRequested();
         }
-        return isVisible();
+        return localVisibleRequested;
     }
 
     /**
@@ -1898,6 +1953,16 @@
         return !isWallpaper || mToken.isVisible();
     }
 
+    private boolean wouldBeVisibleRequestedIfPolicyIgnored() {
+        final WindowState parent = getParentWindow();
+        final boolean isParentHiddenRequested = parent != null && !parent.isVisibleRequested();
+        if (isParentHiddenRequested || mAnimatingExit || mDestroying) {
+            return false;
+        }
+        final boolean isWallpaper = mToken.asWallpaperToken() != null;
+        return !isWallpaper || mToken.isVisibleRequested();
+    }
+
     /**
      * Is this window visible, ignoring its app token? It is not visible if there is no surface,
      * or we are in the process of running an exit animation that will remove the surface.
@@ -1935,7 +2000,7 @@
         final ActivityRecord atoken = mActivityRecord;
         return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE))
                 && isVisibleByPolicy() && !isParentWindowHidden()
-                && (atoken == null || atoken.mVisibleRequested)
+                && (atoken == null || atoken.isVisible())
                 && !mAnimatingExit && !mDestroying;
     }
 
@@ -2167,7 +2232,7 @@
                         mWmService.mAccessibilityController;
                 final int winTransit = TRANSIT_EXIT;
                 mWinAnimator.applyAnimationLocked(winTransit, false /* isEntrance */);
-                if (accessibilityController != null) {
+                if (accessibilityController.hasCallbacks()) {
                     accessibilityController.onWindowTransition(this, winTransit);
                 }
             }
@@ -2188,7 +2253,7 @@
         }
         if (isVisibleNow() && animateExit) {
             mWinAnimator.applyAnimationLocked(TRANSIT_EXIT, false);
-            if (mWmService.mAccessibilityController != null) {
+            if (mWmService.mAccessibilityController.hasCallbacks()) {
                 mWmService.mAccessibilityController.onWindowTransition(this, TRANSIT_EXIT);
             }
             changed = true;
@@ -2238,7 +2303,7 @@
             startMoveAnimation(left, top);
         }
 
-        if (mWmService.mAccessibilityController != null) {
+        if (mWmService.mAccessibilityController.hasCallbacks()) {
             mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(getDisplayId());
         }
         updateLocationInParentDisplayIfNeeded();
@@ -2276,7 +2341,7 @@
                 && (mWindowFrames.mRelFrame.top != mWindowFrames.mLastRelFrame.top
                     || mWindowFrames.mRelFrame.left != mWindowFrames.mLastRelFrame.left)
                 && (!mIsChildWindow || !getParentWindow().hasMoved())
-                && !mWmService.mAtmService.getTransitionController().isCollecting();
+                && !mTransitionController.isCollecting();
     }
 
     boolean isObscuringDisplay() {
@@ -2362,6 +2427,12 @@
 
     @Override
     void removeImmediately() {
+        if (!mRemoved) {
+            // Destroy surface before super call. The general pattern is that the children need
+            // to be removed before the parent (so that the sync-engine tracking works). Since
+            // WindowStateAnimator is a "virtual" child, we have to do it manually here.
+            mWinAnimator.destroySurfaceLocked(getSyncTransaction());
+        }
         super.removeImmediately();
 
         if (mRemoved) {
@@ -2403,8 +2474,6 @@
 
         disposeInputChannel();
 
-        mWinAnimator.destroySurfaceLocked(mTmpTransaction);
-        mTmpTransaction.apply();
         mSession.windowRemovedLocked();
         try {
             mClient.asBinder().unlinkToDeath(mDeathRecipient, 0);
@@ -2520,13 +2589,12 @@
                         setDisplayLayoutNeeded();
                         mWmService.requestTraversal();
                     }
-                    if (mWmService.mAccessibilityController != null) {
+                    if (mWmService.mAccessibilityController.hasCallbacks()) {
                         mWmService.mAccessibilityController.onWindowTransition(this, transit);
                     }
                 }
                 final boolean isAnimating = mAnimatingExit
-                        || isAnimating(TRANSITION | PARENTS, EXIT_ANIMATING_TYPES)
-                        && (mActivityRecord == null || !mActivityRecord.isWaitingForTransitionStart());
+                        || isAnimating(TRANSITION | PARENTS, EXIT_ANIMATING_TYPES);
                 final boolean lastWindowIsStartingWindow = startingWindow && mActivityRecord != null
                         && mActivityRecord.isLastWindow(this);
                 // We delay the removal of a window if it has a showing surface that can be used to run
@@ -2657,6 +2725,12 @@
             }
         }
 
+        // Don't allow transient-launch activities to take IME.
+        if (rootTask != null && mActivityRecord != null
+                && mTransitionController.isTransientLaunch(mActivityRecord)) {
+            return false;
+        }
+
         if (DEBUG_INPUT_METHOD) {
             Slog.i(TAG_WM, "isVisibleOrAdding " + this + ": " + isVisibleOrAdding());
             if (!isVisibleOrAdding()) {
@@ -2880,9 +2954,14 @@
                 // means we need to intercept touches outside of that window. The dim layer
                 // user associated with the window (task or root task) will give us the good
                 // bounds, as they would be used to display the dim layer.
-                final Task task = getTask();
-                if (task != null) {
-                    task.getDimBounds(mTmpRect);
+                final TaskFragment taskFragment = getTaskFragment();
+                if (taskFragment != null) {
+                    final Task task = taskFragment.asTask();
+                    if (task != null) {
+                        task.getDimBounds(mTmpRect);
+                    } else {
+                        mTmpRect.set(taskFragment.getBounds());
+                    }
                 } else if (getRootTask() != null) {
                     getRootTask().getDimBounds(mTmpRect);
                 }
@@ -3535,7 +3614,7 @@
         if (mAttrs.type >= FIRST_SYSTEM_WINDOW && mAttrs.type != TYPE_TOAST) {
             mWmService.mAtmService.mActiveUids.onNonAppSurfaceVisibilityChanged(mOwnerUid, shown);
         }
-        if (mIsImWindow && mWmService.mAccessibilityController != null) {
+        if (mIsImWindow && mWmService.mAccessibilityController.hasCallbacks()) {
             mWmService.mAccessibilityController.onImeSurfaceShownChanged(this, shown);
         }
     }
@@ -3865,7 +3944,7 @@
         final boolean forceRelayout = syncRedraw || reportOrientation || isDragResizeChanged();
         final DisplayContent displayContent = getDisplayContent();
         final boolean alwaysConsumeSystemBars =
-                displayContent.getDisplayPolicy().areSystemBarsForcedShownLw(this);
+                displayContent.getDisplayPolicy().areSystemBarsForcedShownLw();
         final int displayId = displayContent.getDisplayId();
 
         markRedrawForSyncReported();
@@ -3879,7 +3958,7 @@
                         "Requested redraw for orientation change: %s", this);
             }
 
-            if (mWmService.mAccessibilityController != null) {
+            if (mWmService.mAccessibilityController.hasCallbacks()) {
                 mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(displayId);
             }
             updateLocationInParentDisplayIfNeeded();
@@ -3931,7 +4010,7 @@
      * Called when the insets state changed.
      */
     void notifyInsetsChanged() {
-        ProtoLog.d(WM_DEBUG_IME, "notifyInsetsChanged for %s ", this);
+        ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "notifyInsetsChanged for %s ", this);
         try {
             mClient.insetsChanged(getCompatInsetsState(),
                     hasMoved(),
@@ -3943,7 +4022,7 @@
 
     @Override
     public void notifyInsetsControlChanged() {
-        ProtoLog.d(WM_DEBUG_IME, "notifyInsetsControlChanged for %s ", this);
+        ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "notifyInsetsControlChanged for %s ", this);
         if (mAppDied || mRemoved) {
             return;
         }
@@ -4364,9 +4443,9 @@
             pw.println(prefix + "mEmbeddedDisplayContents=" + mEmbeddedDisplayContents);
         }
         if (dumpAll) {
-            final String visibilityString = mRequestedInsetsState.toSourceVisibilityString();
+            final String visibilityString = mRequestedVisibilities.toString();
             if (!visibilityString.isEmpty()) {
-                pw.println(prefix + "Requested visibility: " + visibilityString);
+                pw.println(prefix + "Requested visibilities: " + visibilityString);
             }
         }
     }
@@ -4399,12 +4478,13 @@
     }
 
     private void applyGravityAndUpdateFrame(WindowFrames windowFrames, Rect containingFrame,
-            Rect displayFrame) {
+            Rect displayFrame, DisplayFrames displayFrames) {
         final int pw = containingFrame.width();
         final int ph = containingFrame.height();
         final Task task = getTask();
         final boolean inNonFullscreenContainer = !inAppWindowThatMatchesParentBounds();
-        final boolean noLimits = (mAttrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
+        final WindowManager.LayoutParams attrs = getLayoutingAttrs(displayFrames.mRotation);
+        final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
 
         // We need to fit it to the display if either
         // a) The window is in a fullscreen container, or we don't have a task (we assume fullscreen
@@ -4414,49 +4494,54 @@
         // screen, but SurfaceViews want to be always at a specific location so we don't fit it to
         // the display.
         final boolean fitToDisplay = (task == null || !inNonFullscreenContainer)
-                || ((mAttrs.type != TYPE_BASE_APPLICATION) && !noLimits);
+                || ((attrs.type != TYPE_BASE_APPLICATION) && !noLimits);
         float x, y;
         int w,h;
 
         final boolean hasCompatScale = hasCompatScale();
-        if ((mAttrs.flags & FLAG_SCALED) != 0) {
-            if (mAttrs.width < 0) {
+        if ((attrs.flags & FLAG_SCALED) != 0 || mAttrs != attrs) {
+            // For the window with different layout attrs for different rotations, we need to avoid
+            // using requested size. Otherwise, when finishing a simulated rotation, the information
+            // coming from WindowManagerServices to the ViewRootImpl may not contain the correct
+            // value for the new rotation, and there will be a quick flash of wrong layout when the
+            // simulated activity faded out.
+            if (attrs.width < 0) {
                 w = pw;
             } else if (hasCompatScale) {
-                w = (int)(mAttrs.width * mGlobalScale + .5f);
+                w = (int) (attrs.width * mGlobalScale + .5f);
             } else {
-                w = mAttrs.width;
+                w = attrs.width;
             }
-            if (mAttrs.height < 0) {
+            if (attrs.height < 0) {
                 h = ph;
             } else if (hasCompatScale) {
-                h = (int)(mAttrs.height * mGlobalScale + .5f);
+                h = (int) (attrs.height * mGlobalScale + .5f);
             } else {
-                h = mAttrs.height;
+                h = attrs.height;
             }
         } else {
-            if (mAttrs.width == MATCH_PARENT) {
+            if (attrs.width == MATCH_PARENT) {
                 w = pw;
             } else if (hasCompatScale) {
-                w = (int)(mRequestedWidth * mGlobalScale + .5f);
+                w = (int) (mRequestedWidth * mGlobalScale + .5f);
             } else {
                 w = mRequestedWidth;
             }
-            if (mAttrs.height == MATCH_PARENT) {
+            if (attrs.height == MATCH_PARENT) {
                 h = ph;
             } else if (hasCompatScale) {
-                h = (int)(mRequestedHeight * mGlobalScale + .5f);
+                h = (int) (mRequestedHeight * mGlobalScale + .5f);
             } else {
                 h = mRequestedHeight;
             }
         }
 
         if (hasCompatScale) {
-            x = mAttrs.x * mGlobalScale;
-            y = mAttrs.y * mGlobalScale;
+            x = attrs.x * mGlobalScale;
+            y = attrs.y * mGlobalScale;
         } else {
-            x = mAttrs.x;
-            y = mAttrs.y;
+            x = attrs.x;
+            y = attrs.y;
         }
 
         if (inNonFullscreenContainer && !layoutInParentFrame()) {
@@ -4483,13 +4568,12 @@
         }
 
         // Set mFrame
-        Gravity.apply(mAttrs.gravity, w, h, containingFrame,
-                (int) (x + mAttrs.horizontalMargin * pw),
-                (int) (y + mAttrs.verticalMargin * ph), windowFrames.mFrame);
-
+        Gravity.apply(attrs.gravity, w, h, containingFrame,
+                (int) (x + attrs.horizontalMargin * pw),
+                (int) (y + attrs.verticalMargin * ph), windowFrames.mFrame);
         // Now make sure the window fits in the overall display frame.
         if (fitToDisplay) {
-            Gravity.applyDisplay(mAttrs.gravity, displayFrame, windowFrames.mFrame);
+            Gravity.applyDisplay(attrs.gravity, displayFrame, windowFrames.mFrame);
         }
 
         // We need to make sure we update the CompatFrame as it is used for
@@ -4685,7 +4769,7 @@
         final int drawState = mWinAnimator.mDrawState;
         if ((drawState == HAS_DRAWN || drawState == READY_TO_SHOW) && mActivityRecord != null) {
             if (mAttrs.type != TYPE_APPLICATION_STARTING) {
-                mActivityRecord.onFirstWindowDrawn(this, mWinAnimator);
+                mActivityRecord.onFirstWindowDrawn(this);
             } else {
                 mActivityRecord.onStartingWindowDrawn();
             }
@@ -4773,6 +4857,7 @@
         windowInfo.focused = isFocused();
         Task task = getTask();
         windowInfo.inPictureInPicture = (task != null) && task.inPinnedWindowingMode();
+        windowInfo.taskId = task == null ? ActivityTaskManager.INVALID_TASK_ID : task.mTaskId;
         windowInfo.hasFlagWatchOutsideTouch =
                 (mAttrs.flags & WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH) != 0;
 
@@ -4999,7 +5084,7 @@
         if (isAnimating()) {
             return;
         }
-        if (mWmService.mAccessibilityController != null) {
+        if (mWmService.mAccessibilityController.hasCallbacks()) {
             mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(getDisplayId());
         }
 
@@ -5416,6 +5501,10 @@
         return mWillReplaceWindow;
     }
 
+    private boolean isStartingWindowAssociatedToTask() {
+        return mStartingData != null && mStartingData.mAssociatedTask != null;
+    }
+
     private void applyDims() {
         if (!mAnimatingExit && mAppDied) {
             mIsDimming = true;
@@ -5565,7 +5654,9 @@
             outPoint.offset(-parent.mWindowFrames.mFrame.left + mTmpPoint.x,
                     -parent.mWindowFrames.mFrame.top + mTmpPoint.y);
         } else if (parentWindowContainer != null) {
-            final Rect parentBounds = parentWindowContainer.getBounds();
+            final Rect parentBounds = isStartingWindowAssociatedToTask()
+                    ? mStartingData.mAssociatedTask.getBounds()
+                    : parentWindowContainer.getBounds();
             outPoint.offset(-parentBounds.left, -parentBounds.top);
         }
 
@@ -5602,9 +5693,11 @@
     }
 
     boolean needsRelativeLayeringToIme() {
-        // We only use the relative layering mode in split screen, as part of elevating the IME
-        // and windows above it's target above the docked divider.
-        if (!inSplitScreenWindowingMode()) {
+        // We use the relative layering when IME isn't attached to the app. Such as part of
+        // elevating the IME and windows above it's target above the docked divider in
+        // split-screen, or make the popupMenu to be above the IME when the parent window is the
+        // IME layering target in bubble/freeform mode.
+        if (mDisplayContent.shouldImeAttachedToApp()) {
             return false;
         }
 
@@ -5648,6 +5741,11 @@
 
     @Override
     void assignLayer(Transaction t, int layer) {
+        if (isStartingWindowAssociatedToTask()) {
+            // The starting window should cover the task.
+            t.setLayer(mSurfaceControl, Integer.MAX_VALUE);
+            return;
+        }
         // See comment in assignRelativeLayerForImeTargetChild
         if (needsRelativeLayeringToIme()) {
             getDisplayContent().assignRelativeLayerForImeTargetChild(t, this);
@@ -5660,6 +5758,24 @@
         return mIsDimming;
     }
 
+    @Override
+    protected void reparentSurfaceControl(Transaction t, SurfaceControl newParent) {
+        if (isStartingWindowAssociatedToTask()) {
+            // Its surface is already put in task. Don't reparent when transferring starting window
+            // across activities.
+            return;
+        }
+        super.reparentSurfaceControl(t, newParent);
+    }
+
+    @Override
+    public SurfaceControl getAnimationLeashParent() {
+        if (isStartingWindowAssociatedToTask()) {
+            return mStartingData.mAssociatedTask.mSurfaceControl;
+        }
+        return super.getAnimationLeashParent();
+    }
+
     // TODO(b/70040778): We should aim to eliminate the last user of TYPE_APPLICATION_MEDIA
     // then we can drop all negative layering on the windowing side and simply inherit
     // the default implementation here.
@@ -5899,9 +6015,9 @@
         outSurfaceInsets.set(getAttrs().surfaceInsets);
         final InsetsState state = getInsetsStateWithVisibilityOverride();
         outInsets.set(state.calculateInsets(outFrame, systemBars(),
-                false /* ignoreVisibility */));
+                false /* ignoreVisibility */).toRect());
         outStableInsets.set(state.calculateInsets(outFrame, systemBars(),
-                true /* ignoreVisibility */));
+                true /* ignoreVisibility */).toRect());
     }
 
     void setViewVisibility(int viewVisibility) {
@@ -5921,7 +6037,7 @@
         // since a generic WindowContainer only needs to wait for its
         // children to finish and is immediately ready from its own
         // perspective but at the WindowState level we need to wait for ourselves
-        // to draw even if the children draw first our don't need to sync, so we start
+        // to draw even if the children draw first or don't need to sync, so we start
         // in WAITING state rather than READY.
         mSyncState = SYNC_STATE_WAITING_FOR_DRAW;
         requestRedrawForSync();
@@ -5932,6 +6048,25 @@
         return true;
     }
 
+    @Override
+    boolean isSyncFinished() {
+        if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mViewVisibility == View.GONE
+                && !isVisibleRequested()) {
+            // Don't wait for GONE windows. However, we don't alter the state in case the window
+            // becomes un-gone while the syncset is still active.
+            return true;
+        }
+        return super.isSyncFinished();
+    }
+
+    @Override
+    void finishSync(Transaction outMergedTransaction, boolean cancel) {
+        if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) {
+            mClientWasDrawingForSync = true;
+        }
+        super.finishSync(outMergedTransaction, cancel);
+    }
+
     boolean finishDrawing(SurfaceControl.Transaction postDrawTransaction) {
         if (mOrientationChangeRedrawRequestTime > 0) {
             final long duration =
@@ -5947,15 +6082,25 @@
         }
 
         executeDrawHandlers(postDrawTransaction);
+
+        final boolean applyPostDrawNow = mClientWasDrawingForSync && postDrawTransaction != null;
+        mClientWasDrawingForSync = false;
         if (!onSyncFinishedDrawing()) {
-            return mWinAnimator.finishDrawingLocked(postDrawTransaction);
+            return mWinAnimator.finishDrawingLocked(postDrawTransaction, applyPostDrawNow);
+        }
+
+        if (mActivityRecord != null
+                && mTransitionController.isShellTransitionsEnabled()
+                && mAttrs.type == TYPE_APPLICATION_STARTING) {
+            mWmService.mAtmService.mTaskSupervisor.getActivityMetricsLogger()
+                    .notifyStartingWindowDrawn(mActivityRecord);
         }
 
         if (postDrawTransaction != null) {
             mSyncTransaction.merge(postDrawTransaction);
         }
 
-        mWinAnimator.finishDrawingLocked(null);
+        mWinAnimator.finishDrawingLocked(null, false /* forceApplyNow */);
         // We always want to force a traversal after a finish draw for blast sync.
         return true;
     }
@@ -5993,8 +6138,11 @@
     }
 
     boolean hasWallpaper() {
-        return (mAttrs.flags & FLAG_SHOW_WALLPAPER) != 0
-                || (mActivityRecord != null && mActivityRecord.hasWallpaperBackgroudForLetterbox());
+        return (mAttrs.flags & FLAG_SHOW_WALLPAPER) != 0 || hasWallpaperForLetterboxBackground();
+    }
+
+    boolean hasWallpaperForLetterboxBackground() {
+        return mActivityRecord != null && mActivityRecord.hasWallpaperBackgroudForLetterbox();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 8094196..423b3a0 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -41,7 +41,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-import static com.android.server.wm.WindowManagerService.TYPE_LAYER_MULTIPLIER;
 import static com.android.server.wm.WindowManagerService.logWithStack;
 import static com.android.server.wm.WindowStateAnimatorProto.DRAW_STATE;
 import static com.android.server.wm.WindowStateAnimatorProto.SURFACE;
@@ -71,7 +70,6 @@
  **/
 class WindowStateAnimator {
     static final String TAG = TAG_WITH_CLASS_NAME ? "WindowStateAnimator" : TAG_WM;
-    static final int WINDOW_FREEZE_LAYER = TYPE_LAYER_MULTIPLIER * 200;
     static final int PRESERVED_SURFACE_LAYER = 1;
 
     /**
@@ -82,16 +80,10 @@
     static final int ROOT_TASK_CLIP_AFTER_ANIM = 0;
 
     /**
-     * Mode how the window gets clipped by the root task bounds: The clipping should be applied
-     * before applying the animation transformation, i.e. the root task bounds move with the window.
-     */
-    static final int ROOT_TASK_CLIP_BEFORE_ANIM = 1;
-
-    /**
      * Mode how window gets clipped by the root task bounds during an animation: Don't clip the
      * window by the root task bounds.
      */
-    static final int ROOT_TASK_CLIP_NONE = 2;
+    static final int ROOT_TASK_CLIP_NONE = 1;
 
     // Unchanging local convenience fields.
     final WindowManagerService mService;
@@ -230,7 +222,8 @@
         }
     }
 
-    boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction) {
+    boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction,
+            boolean forceApplyNow) {
         final boolean startingWindow =
                 mWin.mAttrs.type == WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
         if (startingWindow) {
@@ -255,12 +248,12 @@
             // If there is no surface, the last draw was for the previous surface. We don't want to
             // wait until the new surface is shown and instead just apply the transaction right
             // away.
-            if (mLastHidden && mDrawState != NO_SURFACE) {
+            if (mLastHidden && mDrawState != NO_SURFACE && !forceApplyNow) {
                 mPostDrawTransaction.merge(postDrawTransaction);
-                layoutNeeded = true;
             } else {
-                postDrawTransaction.apply();
+                mWin.getSyncTransaction().merge(postDrawTransaction);
             }
+            layoutNeeded = true;
         }
 
         return layoutNeeded;
@@ -684,7 +677,7 @@
             applyAnimationLocked(transit, true);
         }
 
-        if (mService.mAccessibilityController != null) {
+        if (mService.mAccessibilityController.hasCallbacks()) {
             mService.mAccessibilityController.onWindowTransition(mWin, transit);
         }
     }
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 3cbc67c..ad351f0 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -16,9 +16,11 @@
 
 package com.android.server.wm;
 
+import static android.view.ViewRootImpl.INSETS_LAYOUT_GENERALIZATION;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
@@ -230,6 +232,11 @@
             ProtoLog.w(WM_DEBUG_WINDOW_MOVEMENT,
                     "removeAllWindowsIfPossible: removing win=%s", win);
             win.removeIfPossible();
+            if (i > mChildren.size()) {
+                // It's possible for removeIfPossible to delete siblings (for example if it is a
+                // starting window, it will perform operations on the ActivityRecord).
+                i = mChildren.size();
+            }
         }
     }
 
@@ -453,9 +460,24 @@
     }
 
     Rect getFixedRotationBarContentFrame(int windowType) {
-        return isFixedRotationTransforming()
-                ? mFixedRotationTransformState.mBarContentFrames.get(windowType)
-                : null;
+        if (!isFixedRotationTransforming()) {
+            return null;
+        }
+        if (!INSETS_LAYOUT_GENERALIZATION) {
+            return mFixedRotationTransformState.mBarContentFrames.get(windowType);
+        }
+        final DisplayFrames displayFrames = mFixedRotationTransformState.mDisplayFrames;
+        final Rect tmpRect = new Rect();
+        if (windowType == TYPE_NAVIGATION_BAR) {
+            tmpRect.set(displayFrames.mInsetsState.getSource(InsetsState.ITYPE_NAVIGATION_BAR)
+                    .getFrame());
+        }
+        if (windowType == TYPE_STATUS_BAR) {
+            tmpRect.set(displayFrames.mInsetsState.getSource(InsetsState.ITYPE_STATUS_BAR)
+                    .getFrame());
+        }
+        tmpRect.intersect(displayFrames.mDisplayCutoutSafe);
+        return tmpRect;
     }
 
     InsetsState getFixedRotationTransformInsetsState() {
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index 0bb97f5..6204824 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -54,7 +54,8 @@
     private static final int BUFFER_CAPACITY_CRITICAL = 512 * 1024;
     private static final int BUFFER_CAPACITY_TRIM = 2048 * 1024;
     private static final int BUFFER_CAPACITY_ALL = 4096 * 1024;
-    private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace.pb";
+    static final String WINSCOPE_EXT = ".winscope";
+    private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT;
     private static final String TAG = "WindowTracing";
     private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
 
diff --git a/services/core/xsd/Android.bp b/services/core/xsd/Android.bp
index d43cf3f..6a50d38 100644
--- a/services/core/xsd/Android.bp
+++ b/services/core/xsd/Android.bp
@@ -14,7 +14,6 @@
     package_name: "com.android.server.pm.permission.configfile",
 }
 
-
 xsd_config {
     name: "platform-compat-config",
     srcs: ["platform-compat/config/platform-compat-config.xsd"],
@@ -42,6 +41,7 @@
     srcs: ["display-layout-config/display-layout-config.xsd"],
     api_dir: "display-layout-config/schema",
     package_name: "com.android.server.display.config.layout",
+    boolean_getter: true,
 }
 
 xsd_config {
diff --git a/services/core/xsd/device-state-config/device-state-config.xsd b/services/core/xsd/device-state-config/device-state-config.xsd
index 94a398f..86f4176 100644
--- a/services/core/xsd/device-state-config/device-state-config.xsd
+++ b/services/core/xsd/device-state-config/device-state-config.xsd
@@ -40,10 +40,19 @@
             <xs:element name="name" type="xs:string" minOccurs="0">
                 <xs:annotation name="nullable" />
             </xs:element>
+            <xs:element name="flags" type="flags" />
             <xs:element name="conditions" type="conditions" />
         </xs:sequence>
     </xs:complexType>
 
+    <xs:complexType name="flags">
+        <xs:sequence>
+            <xs:element name="flag" type="xs:string" minOccurs="0" maxOccurs="unbounded">
+                <xs:annotation name="nullable" />
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+
     <xs:complexType name="conditions">
         <xs:sequence>
             <xs:element name="lid-switch" type="lidSwitchCondition" minOccurs="0">
diff --git a/services/core/xsd/device-state-config/schema/current.txt b/services/core/xsd/device-state-config/schema/current.txt
index 08fccf8..a98d4e5 100644
--- a/services/core/xsd/device-state-config/schema/current.txt
+++ b/services/core/xsd/device-state-config/schema/current.txt
@@ -11,9 +11,11 @@
   public class DeviceState {
     ctor public DeviceState();
     method public com.android.server.policy.devicestate.config.Conditions getConditions();
+    method public com.android.server.policy.devicestate.config.Flags getFlags();
     method public java.math.BigInteger getIdentifier();
     method @Nullable public String getName();
     method public void setConditions(com.android.server.policy.devicestate.config.Conditions);
+    method public void setFlags(com.android.server.policy.devicestate.config.Flags);
     method public void setIdentifier(java.math.BigInteger);
     method public void setName(@Nullable String);
   }
@@ -23,6 +25,11 @@
     method public java.util.List<com.android.server.policy.devicestate.config.DeviceState> getDeviceState();
   }
 
+  public class Flags {
+    ctor public Flags();
+    method @Nullable public java.util.List<java.lang.String> getFlag();
+  }
+
   public class LidSwitchCondition {
     ctor public LidSwitchCondition();
     method public boolean getOpen();
diff --git a/services/core/xsd/display-layout-config/display-layout-config.xsd b/services/core/xsd/display-layout-config/display-layout-config.xsd
index c542c0d..e14139a 100644
--- a/services/core/xsd/display-layout-config/display-layout-config.xsd
+++ b/services/core/xsd/display-layout-config/display-layout-config.xsd
@@ -52,6 +52,6 @@
             <xs:element name="address" type="xs:nonNegativeInteger"/>
         </xs:sequence>
         <xs:attribute name="enabled" type="xs:boolean" use="optional" />
-        <xs:attribute name="isDefault" type="xs:boolean" use="optional" />
+        <xs:attribute name="defaultDisplay" type="xs:boolean" use="optional" />
     </xs:complexType>
 </xs:schema>
diff --git a/services/core/xsd/display-layout-config/schema/current.txt b/services/core/xsd/display-layout-config/schema/current.txt
index 8171885..f391575 100644
--- a/services/core/xsd/display-layout-config/schema/current.txt
+++ b/services/core/xsd/display-layout-config/schema/current.txt
@@ -4,11 +4,11 @@
   public class Display {
     ctor public Display();
     method public java.math.BigInteger getAddress();
-    method public boolean getEnabled();
-    method public boolean getIsDefault();
+    method public boolean isDefaultDisplay();
+    method public boolean isEnabled();
     method public void setAddress(java.math.BigInteger);
+    method public void setDefaultDisplay(boolean);
     method public void setEnabled(boolean);
-    method public void setIsDefault(boolean);
   }
 
   public class Layout {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
index 8ea21ec..a301799 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
@@ -46,6 +46,12 @@
     @GuardedBy("mLock")
     private final SparseIntArray mPermissionPolicy = new SparseIntArray();
 
+    /** Maps to {@code ActiveAdmin.mAdminCanGrantSensorsPermissions}.
+     *
+     * <p>For users affiliated with the device, they inherit the policy from {@code DO} so
+     * it will map to the {@code DO}'s policy. Otherwise it will map to the admin of the requesting
+     * user.
+     */
     @GuardedBy("mLock")
     private final SparseBooleanArray mCanGrantSensorsPermissions = new SparseBooleanArray();
 
@@ -102,17 +108,16 @@
     }
 
     @Override
-    public boolean canAdminGrantSensorsPermissionsForUser(@UserIdInt int userHandle) {
+    public boolean canAdminGrantSensorsPermissionsForUser(@UserIdInt int userId) {
         synchronized (mLock) {
-            return mCanGrantSensorsPermissions.get(userHandle, false);
+            return mCanGrantSensorsPermissions.get(userId, false);
         }
     }
 
     /** Sets ahmin control over permission grants for user. */
-    public void setAdminCanGrantSensorsPermissions(@UserIdInt int userHandle,
-            boolean canGrant) {
+    public void setAdminCanGrantSensorsPermissions(@UserIdInt int userId, boolean canGrant) {
         synchronized (mLock) {
-            mCanGrantSensorsPermissions.put(userHandle, canGrant);
+            mCanGrantSensorsPermissions.put(userId, canGrant);
         }
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 9ceabce..d0d0cdf 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -327,7 +327,6 @@
 import com.android.server.SystemServerInitThreadPool;
 import com.android.server.SystemService;
 import com.android.server.devicepolicy.ActiveAdmin.TrustAgentInfo;
-import com.android.server.devicepolicy.Owners.OwnerDto;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.net.NetworkPolicyManagerInternal;
 import com.android.server.pm.RestrictionsSet;
@@ -1259,17 +1258,37 @@
     }
 
     // Used by DevicePolicyManagerServiceShellCommand
-    List<OwnerDto> listAllOwners() {
+    List<OwnerShellData> listAllOwners() {
         Preconditions.checkCallAuthorization(
                 hasCallingOrSelfPermission(permission.MANAGE_DEVICE_ADMINS));
         return mInjector.binderWithCleanCallingIdentity(() -> {
-            List<OwnerDto> owners = mOwners.listAllOwners();
+            SparseArray<DevicePolicyData> userData;
+
+            // Gets the owners of "full users" first (device owner and profile owners)
+            List<OwnerShellData> owners = mOwners.listAllOwners();
             synchronized (getLockObject()) {
                 for (int i = 0; i < owners.size(); i++) {
-                    OwnerDto owner = owners.get(i);
+                    OwnerShellData owner = owners.get(i);
                     owner.isAffiliated = isUserAffiliatedWithDeviceLocked(owner.userId);
                 }
+                userData = mUserData;
             }
+
+            // Then the owners of profile users (managed profiles)
+            for (int i = 0; i < userData.size(); i++) {
+                DevicePolicyData policyData = mUserData.valueAt(i);
+                int userId = userData.keyAt(i);
+                int parentUserId = mUserManagerInternal.getProfileParentId(userId);
+                boolean isProfile = parentUserId != userId;
+                if (!isProfile) continue;
+                for (int j = 0; j < policyData.mAdminList.size(); j++) {
+                    ActiveAdmin admin = policyData.mAdminList.get(j);
+                    OwnerShellData owner = OwnerShellData.forManagedProfileOwner(userId,
+                            parentUserId, admin.info.getComponent());
+                    owners.add(owner);
+                }
+            }
+
             return owners;
         });
     }
@@ -8337,7 +8356,8 @@
     }
 
     @Override
-    public boolean setDeviceOwner(ComponentName admin, String ownerName, int userId) {
+    public boolean setDeviceOwner(ComponentName admin, String ownerName, int userId,
+            boolean setProfileOwnerOnCurrentUserIfNecessary) {
         if (!mHasFeature) {
             logMissingFeatureAction("Cannot set " + ComponentName.flattenToShortString(admin)
                     + " as device owner for user " + userId);
@@ -8400,7 +8420,8 @@
 
             Slogf.i(LOG_TAG, "Device owner set: " + admin + " on user " + userId);
 
-            if (mInjector.userManagerIsHeadlessSystemUserMode()) {
+            if (setProfileOwnerOnCurrentUserIfNecessary
+                    && mInjector.userManagerIsHeadlessSystemUserMode()) {
                 int currentForegroundUser = getCurrentForegroundUserId();
                 Slogf.i(LOG_TAG, "setDeviceOwner(): setting " + admin
                         + " as profile owner on user " + currentForegroundUser);
@@ -9128,9 +9149,7 @@
     }
 
     /**
-     * Returns the ActiveAdmin associated wit the PO or DO on the given user.
-     * @param userHandle
-     * @return
+     * Returns the ActiveAdmin associated with the PO or DO on the given user.
      */
     private @Nullable ActiveAdmin getDeviceOrProfileOwnerAdminLocked(int userHandle) {
         ActiveAdmin admin = getProfileOwnerAdminLocked(userHandle);
@@ -10614,19 +10633,21 @@
             }
         }
         final String adminPkg = admin.getPackageName();
-        try {
-            // Install the profile owner if not present.
-            if (!mIPackageManager.isPackageAvailable(adminPkg, userId)) {
-                mIPackageManager.installExistingPackageAsUser(adminPkg, userId,
-                        PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS,
-                        PackageManager.INSTALL_REASON_POLICY,
-                        /* allowlistedRestrictedPermissions= */ null);
+        mInjector.binderWithCleanCallingIdentity(() -> {
+            try {
+                // Install the profile owner if not present.
+                if (!mIPackageManager.isPackageAvailable(adminPkg, userId)) {
+                    mIPackageManager.installExistingPackageAsUser(adminPkg, userId,
+                            PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS,
+                            PackageManager.INSTALL_REASON_POLICY,
+                            /* allowlistedRestrictedPermissions= */ null);
+                }
+            } catch (RemoteException e) {
+                // Does not happen, same process
+                Slogf.wtf(LOG_TAG, e, "Failed to install admin package %s for user %d",
+                        adminPkg, userId);
             }
-        } catch (RemoteException e) {
-            // Does not happen, same process
-            Slogf.wtf(LOG_TAG, e, "Failed to install admin package %s for user %d",
-                    adminPkg, userId);
-        }
+        });
 
         // Set admin.
         setActiveAdmin(profileOwner, /* refreshing= */ true, userId);
@@ -10665,7 +10686,10 @@
             }
         }
 
-        if (!mOwners.hasDeviceOwner() || !user.isFull() || user.isManagedProfile()) return;
+        if (!mOwners.hasDeviceOwner() || !user.isFull() || user.isManagedProfile()
+                || user.isGuest()) {
+            return;
+        }
 
         if (mInjector.userManagerIsHeadlessSystemUserMode()) {
             ComponentName admin = mOwners.getDeviceOwnerComponent();
@@ -14338,6 +14362,7 @@
             maybePauseDeviceWideLoggingLocked();
             maybeResumeDeviceWideLoggingLocked();
             maybeClearLockTaskPolicyLocked();
+            updateAdminCanGrantSensorsPermissionCache(callingUserId);
         }
     }
 
@@ -17001,6 +17026,19 @@
     }
 
     @Override
+    public void clearOrganizationIdForUser(int userHandle) {
+        Preconditions.checkCallAuthorization(
+                hasCallingOrSelfPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS));
+
+        synchronized (getLockObject()) {
+            final ActiveAdmin owner = getDeviceOrProfileOwnerAdminLocked(userHandle);
+            owner.mOrganizationId = null;
+            owner.mEnrollmentSpecificId = null;
+            saveSettingsLocked(userHandle);
+        }
+    }
+
+    @Override
     public UserHandle createAndProvisionManagedProfile(
             @NonNull ManagedProfileProvisioningParams provisioningParams,
             @NonNull String callerPackage) {
@@ -17447,7 +17485,8 @@
         // TODO(b/178187130): Directly set DO and remove the check once silent provisioning is no
         //  longer used.
         if (getDeviceOwnerComponent(/* callingUserOnly= */ true) == null) {
-            return setDeviceOwner(adminComponent, name, userId);
+            return setDeviceOwner(adminComponent, name, userId,
+                    /* setProfileOwnerOnCurrentUserIfNecessary= */ true);
         }
         return true;
     }
@@ -17502,7 +17541,10 @@
         });
     }
 
-    private void setAdminCanGrantSensorsPermissionForUserUnchecked(int userId, boolean canGrant) {
+    private void setAdminCanGrantSensorsPermissionForUserUnchecked(@UserIdInt int userId,
+            boolean canGrant) {
+        Slogf.d(LOG_TAG, "setAdminCanGrantSensorsPermissionForUserUnchecked(%d, %b)",
+                userId, canGrant);
         synchronized (getLockObject()) {
             ActiveAdmin owner = getDeviceOrProfileOwnerAdminLocked(userId);
 
@@ -17516,10 +17558,18 @@
         }
     }
 
-    private void updateAdminCanGrantSensorsPermissionCache(int userId) {
+    private void updateAdminCanGrantSensorsPermissionCache(@UserIdInt int userId) {
         synchronized (getLockObject()) {
-            ActiveAdmin owner = getDeviceOrProfileOwnerAdminLocked(userId);
-            final boolean canGrant = owner != null ? owner.mAdminCanGrantSensorsPermissions : false;
+
+            ActiveAdmin owner;
+            // If the user is affiliated the device (either a DO itself, or an affiliated PO),
+            // use mAdminCanGrantSensorsPermissions from the DO
+            if (isUserAffiliatedWithDeviceLocked(userId)) {
+                owner = getDeviceOwnerAdminLocked();
+            } else {
+                owner = getDeviceOrProfileOwnerAdminLocked(userId);
+            }
+            boolean canGrant = owner != null ? owner.mAdminCanGrantSensorsPermissions : false;
             mPolicyCache.setAdminCanGrantSensorsPermissions(userId, canGrant);
         }
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
index a2db6aac..e1d720c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
@@ -22,8 +22,6 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 
-import com.android.server.devicepolicy.Owners.OwnerDto;
-
 import java.io.PrintWriter;
 import java.util.Collection;
 import java.util.List;
@@ -48,11 +46,13 @@
 
     private static final String USER_OPTION = "--user";
     private static final String NAME_OPTION = "--name";
+    private static final String DO_ONLY_OPTION = "--device-owner-only";
 
     private final DevicePolicyManagerService mService;
     private int mUserId = UserHandle.USER_SYSTEM;
     private String mName = "";
     private ComponentName mComponent;
+    private boolean mSetDoOnly;
 
     DevicePolicyManagerServiceShellCommand(DevicePolicyManagerService service) {
         mService = Objects.requireNonNull(service);
@@ -132,8 +132,8 @@
         pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
                 CMD_SET_ACTIVE_ADMIN, USER_OPTION);
         pw.printf("    Sets the given component as active admin for an existing user.\n\n");
-        pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s <NAME> ] "
-                + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, NAME_OPTION);
+        pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s <NAME> ] [ %s ]"
+                + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, NAME_OPTION, DO_ONLY_OPTION);
         pw.printf("    Sets the given component as active admin, and its package as device owner."
                 + "\n\n");
         pw.printf("  %s [ %s <USER_ID> | current ] [ %s <NAME> ] <COMPONENT>\n",
@@ -205,12 +205,12 @@
     }
 
     private int runListOwners(PrintWriter pw) {
-        List<OwnerDto> owners = mService.listAllOwners();
+        List<OwnerShellData> owners = mService.listAllOwners();
         int size = printAndGetSize(pw, owners, "owner");
         if (size == 0) return 0;
 
         for (int i = 0; i < size; i++) {
-            OwnerDto owner = owners.get(i);
+            OwnerShellData owner = owners.get(i);
             pw.printf("User %2d: admin=%s", owner.userId, owner.admin.flattenToShortString());
             if (owner.isDeviceOwner) {
                 pw.print(",DeviceOwner");
@@ -218,6 +218,9 @@
             if (owner.isProfileOwner) {
                 pw.print(",ProfileOwner");
             }
+            if (owner.isManagedProfileOwner) {
+                pw.printf(",ManagedProfileOwner(parentUserId=%d)", owner.parentUserId);
+            }
             if (owner.isAffiliated) {
                 pw.print(",Affiliated");
             }
@@ -253,7 +256,8 @@
         mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId);
 
         try {
-            if (!mService.setDeviceOwner(mComponent, mName, mUserId)) {
+            if (!mService.setDeviceOwner(mComponent, mName, mUserId,
+                    /* setProfileOwnerOnCurrentUserIfNecessary= */ !mSetDoOnly)) {
                 throw new RuntimeException(
                         "Can't set package " + mComponent + " as device owner.");
             }
@@ -350,6 +354,8 @@
                 if (mUserId == UserHandle.USER_CURRENT) {
                     mUserId = ActivityManager.getCurrentUser();
                 }
+            } else if (DO_ONLY_OPTION.equals(opt)) {
+                mSetDoOnly = true;
             } else if (canHaveName && NAME_OPTION.equals(opt)) {
                 mName = getNextArgRequired();
             } else {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnerShellData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnerShellData.java
new file mode 100644
index 0000000..b98c3dc
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnerShellData.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.devicepolicy;
+
+import static android.os.UserHandle.USER_NULL;
+
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Data-transfer object used by {@link DevicePolicyManagerServiceShellCommand}.
+ */
+final class OwnerShellData {
+
+    public final @UserIdInt int userId;
+    public final @UserIdInt int parentUserId;
+    public final ComponentName admin;
+    public final boolean isDeviceOwner;
+    public final boolean isProfileOwner;
+    public final boolean isManagedProfileOwner;
+    public boolean isAffiliated;
+
+    // NOTE: class is too simple to require a Builder (not to mention isAffiliated is mutable)
+    private OwnerShellData(@UserIdInt int userId, @UserIdInt int parentUserId, ComponentName admin,
+            boolean isDeviceOwner, boolean isProfileOwner, boolean isManagedProfileOwner) {
+        Preconditions.checkArgument(userId != USER_NULL, "userId cannot be USER_NULL");
+        this.userId = userId;
+        this.parentUserId = parentUserId;
+        this.admin = Objects.requireNonNull(admin, "admin must not be null");
+        this.isDeviceOwner = isDeviceOwner;
+        this.isProfileOwner = isProfileOwner;
+        this.isManagedProfileOwner = isManagedProfileOwner;
+        if (isManagedProfileOwner) {
+            Preconditions.checkArgument(parentUserId != USER_NULL,
+                    "parentUserId cannot be USER_NULL for managed profile owner");
+            Preconditions.checkArgument(parentUserId != userId,
+                    "cannot be parent of itself (%d)", userId);
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder(getClass().getSimpleName())
+                .append("[userId=").append(userId)
+                .append(",admin=").append(admin.flattenToShortString());
+        if (isDeviceOwner) {
+            sb.append(",deviceOwner");
+        }
+        if (isProfileOwner) {
+            sb.append(",isProfileOwner");
+        }
+        if (isManagedProfileOwner) {
+            sb.append(",isManagedProfileOwner");
+        }
+        if (parentUserId != USER_NULL) {
+            sb.append(",parentUserId=").append(parentUserId);
+        }
+        if (isAffiliated) {
+            sb.append(",isAffiliated");
+        }
+        return sb.append(']').toString();
+    }
+
+    static OwnerShellData forDeviceOwner(@UserIdInt int userId, ComponentName admin) {
+        return new OwnerShellData(userId, /* parentUserId= */ USER_NULL, admin,
+                /* isDeviceOwner= */ true, /* isProfileOwner= */ false,
+                /* isManagedProfileOwner= */ false);
+    }
+
+    static OwnerShellData forUserProfileOwner(@UserIdInt int userId, ComponentName admin) {
+        return new OwnerShellData(userId, /* parentUserId= */ USER_NULL, admin,
+                /* isDeviceOwner= */ false, /* isProfileOwner= */ true,
+                /* isManagedProfileOwner= */ false);
+    }
+
+    static OwnerShellData forManagedProfileOwner(@UserIdInt int userId, @UserIdInt int parentUserId,
+            ComponentName admin) {
+        return new OwnerShellData(userId, parentUserId, admin, /* isDeviceOwner= */ false,
+                /* isProfileOwner= */ false, /* isManagedProfileOwner= */ true);
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index fd09e3f..3584728 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -19,7 +19,6 @@
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT;
 
 import android.annotation.Nullable;
-import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManagerInternal;
 import android.app.admin.DevicePolicyManager.DeviceOwnerType;
@@ -476,17 +475,16 @@
         }
     }
 
-    List<OwnerDto> listAllOwners() {
-        List<OwnerDto> owners = new ArrayList<>();
+    List<OwnerShellData> listAllOwners() {
+        List<OwnerShellData> owners = new ArrayList<>();
         synchronized (mLock) {
             if (mDeviceOwner != null) {
-                owners.add(new OwnerDto(mDeviceOwnerUserId, mDeviceOwner.admin,
-                        /* isDeviceOwner= */ true));
+                owners.add(OwnerShellData.forDeviceOwner(mDeviceOwnerUserId, mDeviceOwner.admin));
             }
             for (int i = 0; i < mProfileOwners.size(); i++) {
                 int userId = mProfileOwners.keyAt(i);
                 OwnerInfo info = mProfileOwners.valueAt(i);
-                owners.add(new OwnerDto(userId, info.admin, /* isDeviceOwner= */ false));
+                owners.add(OwnerShellData.forUserProfileOwner(userId, info.admin));
             }
         }
         return owners;
@@ -1236,24 +1234,6 @@
         }
     }
 
-    /**
-     * Data-transfer object used by {@link DevicePolicyManagerServiceShellCommand}.
-     */
-    static final class OwnerDto {
-        public final @UserIdInt int userId;
-        public final ComponentName admin;
-        public final boolean isDeviceOwner;
-        public final boolean isProfileOwner;
-        public boolean isAffiliated;
-
-        private OwnerDto(@UserIdInt int userId, ComponentName admin, boolean isDeviceOwner) {
-            this.userId = userId;
-            this.admin = Objects.requireNonNull(admin, "admin must not be null");
-            this.isDeviceOwner = isDeviceOwner;
-            this.isProfileOwner = !isDeviceOwner;
-        }
-    }
-
     public void dump(IndentingPrintWriter pw) {
         boolean needBlank = false;
         if (mDeviceOwner != null) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e012dd2..0df3863 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -378,6 +378,8 @@
             "com.android.server.connectivity.IpConnectivityMetrics";
     private static final String MEDIA_COMMUNICATION_SERVICE_CLASS =
             "com.android.server.media.MediaCommunicationService";
+    private static final String APP_COMPAT_OVERRIDES_SERVICE_CLASS =
+            "com.android.server.compat.overrides.AppCompatOverridesService$Lifecycle";
 
     private static final String ROLE_SERVICE_CLASS = "com.android.role.RoleService";
     private static final String GAME_MANAGER_SERVICE_CLASS =
@@ -1584,6 +1586,9 @@
             // all listeners have the chance to react with special handling.
             Settings.Global.putInt(context.getContentResolver(),
                     Settings.Global.AIRPLANE_MODE_ON, 1);
+        } else if (context.getResources().getBoolean(R.bool.config_autoResetAirplaneMode)) {
+            Settings.Global.putInt(context.getContentResolver(),
+                    Settings.Global.AIRPLANE_MODE_ON, 0);
         }
 
         StatusBarManagerService statusBar = null;
@@ -2651,6 +2656,10 @@
         mSystemServiceManager.startService(MEDIA_COMMUNICATION_SERVICE_CLASS);
         t.traceEnd();
 
+        t.traceBegin("AppCompatOverridesService");
+        mSystemServiceManager.startService(APP_COMPAT_OVERRIDES_SERVICE_CLASS);
+        t.traceEnd();
+
         // These are needed to propagate to the runnable below.
         final NetworkManagementService networkManagementF = networkManagement;
         final NetworkStatsService networkStatsF = networkStats;
diff --git a/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesParserTest.java b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesParserTest.java
new file mode 100644
index 0000000..df19be4
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesParserTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.compat.overrides;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import static java.util.Collections.emptySet;
+
+import android.app.compat.PackageOverride;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Test class for {@link AppCompatOverridesParser}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksMockingServicesTests:AppCompatOverridesParserTest
+ */
+@RunWith(MockitoJUnitRunner.class)
+@SmallTest
+@Presubmit
+public class AppCompatOverridesParserTest {
+    private static final String PACKAGE_1 = "com.android.test1";
+    private static final String PACKAGE_2 = "com.android.test2";
+    private static final String PACKAGE_3 = "com.android.test3";
+    private static final String PACKAGE_4 = "com.android.test4";
+
+    private AppCompatOverridesParser mParser;
+
+    @Mock
+    private PackageManager mPackageManager;
+
+    @Before
+    public void setUp() throws Exception {
+        mParser = new AppCompatOverridesParser(mPackageManager);
+    }
+
+    @Test
+    public void parseRemoveOverrides_emptyConfig_returnsEmpty() {
+        Set<Long> ownedChangeIds = new ArraySet<>(Arrays.asList(123L, 456L));
+
+        assertThat(mParser.parseRemoveOverrides("", ownedChangeIds)).isEmpty();
+    }
+
+    @Test
+    public void parseRemoveOverrides_configHasWildcardNoOwnedChangeIds_returnsEmpty() {
+        when(mPackageManager.getInstalledApplications(anyInt()))
+                .thenReturn(Arrays.asList(createAppInfo(PACKAGE_1), createAppInfo(PACKAGE_2)));
+
+        assertThat(mParser.parseRemoveOverrides("*", /* ownedChangeIds= */ emptySet())).isEmpty();
+    }
+
+    @Test
+    public void parseRemoveOverrides_configHasWildcard_returnsAllInstalledPackagesToAllOwnedIds() {
+        Set<Long> ownedChangeIds = new ArraySet<>(Arrays.asList(123L, 456L));
+        when(mPackageManager.getInstalledApplications(anyInt()))
+                .thenReturn(Arrays.asList(createAppInfo(PACKAGE_1), createAppInfo(PACKAGE_2),
+                        createAppInfo(PACKAGE_3)));
+
+        Map<String, Set<Long>> result = mParser.parseRemoveOverrides("*", ownedChangeIds);
+
+        assertThat(result).hasSize(3);
+        assertThat(result.get(PACKAGE_1)).containsExactly(123L, 456L);
+        assertThat(result.get(PACKAGE_2)).containsExactly(123L, 456L);
+        assertThat(result.get(PACKAGE_3)).containsExactly(123L, 456L);
+    }
+
+    @Test
+    public void parseRemoveOverrides_configHasInvalidWildcardSymbol_returnsEmpty() {
+        Set<Long> ownedChangeIds = new ArraySet<>(Arrays.asList(123L, 456L));
+        when(mPackageManager.getInstalledApplications(anyInt())).thenReturn(
+                Arrays.asList(createAppInfo(PACKAGE_1), createAppInfo(PACKAGE_2)));
+
+        assertThat(mParser.parseRemoveOverrides("**", ownedChangeIds)).isEmpty();
+    }
+
+    @Test
+    public void parseRemoveOverrides_configHasSingleEntry_returnsPackageToChangeIds() {
+        Map<String, Set<Long>> result = mParser.parseRemoveOverrides(
+                PACKAGE_1 + "=12:34", /* ownedChangeIds= */ emptySet());
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(PACKAGE_1)).containsExactly(12L, 34L);
+    }
+
+    @Test
+    public void parseRemoveOverrides_configHasMultipleEntries_returnsPackagesToChangeIds() {
+        Set<Long> ownedChangeIds = new ArraySet<>(Arrays.asList(12L, 34L, 56L, 78L));
+
+        Map<String, Set<Long>> result = mParser.parseRemoveOverrides(
+                PACKAGE_1 + "=12," + PACKAGE_2 + "=*," + PACKAGE_3 + "=12:56:78," + PACKAGE_4
+                        + "=", ownedChangeIds);
+
+        assertThat(result).hasSize(3);
+        assertThat(result.get(PACKAGE_1)).containsExactly(12L);
+        assertThat(result.get(PACKAGE_2)).containsExactly(12L, 34L, 56L, 78L);
+        assertThat(result.get(PACKAGE_3)).containsExactly(12L, 56L, 78L);
+    }
+
+    @Test
+    public void parseRemoveOverrides_configHasPackageWithWildcardNoOwnedId_returnsWithoutPackage() {
+        Map<String, Set<Long>> result = mParser.parseRemoveOverrides(
+                PACKAGE_1 + "=*," + PACKAGE_2 + "=12", /* ownedChangeIds= */ emptySet());
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(PACKAGE_2)).containsExactly(12L);
+    }
+
+    @Test
+    public void parseRemoveOverrides_configHasInvalidKeyValueListFormat_returnsEmpty() {
+        Set<Long> ownedChangeIds = new ArraySet<>(Arrays.asList(12L, 34L));
+
+        assertThat(mParser.parseRemoveOverrides(
+                PACKAGE_1 + "=12," + PACKAGE_2 + ">34", ownedChangeIds)).isEmpty();
+    }
+
+
+    @Test
+    public void parseRemoveOverrides_configHasInvalidChangeIds_returnsWithoutInvalidChangeIds() {
+        Map<String, Set<Long>> result = mParser.parseRemoveOverrides(
+                PACKAGE_1 + "=12," + PACKAGE_2 + "=12:56L:78," + PACKAGE_3
+                        + "=34L", /* ownedChangeIds= */ emptySet());
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(PACKAGE_1)).containsExactly(12L);
+        assertThat(result.get(PACKAGE_2)).containsExactly(12L, 78L);
+    }
+
+    @Test
+    public void parseOwnedChangeIds_emptyConfig_returnsEmpty() {
+        assertThat(AppCompatOverridesParser.parseOwnedChangeIds("")).isEmpty();
+    }
+
+    @Test
+    public void parseOwnedChangeIds_configHasSingleChangeId_returnsChangeId() {
+        assertThat(AppCompatOverridesParser.parseOwnedChangeIds("123")).containsExactly(123L);
+    }
+
+    @Test
+    public void parseOwnedChangeIds_configHasMultipleChangeIds_returnsChangeIds() {
+        assertThat(AppCompatOverridesParser.parseOwnedChangeIds("12,34,56")).containsExactly(12L,
+                34L, 56L);
+    }
+
+    @Test
+    public void parseOwnedChangeIds_configHasInvalidChangeIds_returnsWithoutInvalidChangeIds() {
+        // We add a valid entry before and after the invalid ones to make sure they are applied.
+        assertThat(AppCompatOverridesParser.parseOwnedChangeIds("12,C34,56")).containsExactly(12L,
+                56L);
+    }
+
+    @Test
+    public void parsePackageOverrides_emptyConfigNoOwnedChangeIds_returnsEmpty() {
+        Map<Long, PackageOverride> result = AppCompatOverridesParser.parsePackageOverrides(
+                /* configStr= */ "", /* versionCode= */ 0, /* changeIdsToSkip= */ emptySet());
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void parsePackageOverrides_configWithSingleOverride_returnsOverride() {
+        Map<Long, PackageOverride> result = AppCompatOverridesParser.parsePackageOverrides(
+                /* configStr= */ "123:::true", /* versionCode= */ 5, /* changeIdsToSkip= */
+                emptySet());
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(123L)).isEqualTo(
+                new PackageOverride.Builder().setEnabled(true).build());
+    }
+
+    @Test
+    public void parsePackageOverrides_configWithMultipleOverrides_returnsOverrides() {
+        Map<Long, PackageOverride> result = AppCompatOverridesParser.parsePackageOverrides(
+                /* configStr= */ "910:3:4:false,78:10::false,12:::false,34:1:2:true,34:10::true,"
+                        + "56::2:true,56:3:4:false,34:4:8:true,78:6:7:true,910:5::true,"
+                        + "1112::5:true,56:6::true,1112:6:7:false", /* versionCode= */
+                5, /* changeIdsToSkip= */ emptySet());
+
+        assertThat(result).hasSize(6);
+        assertThat(result.get(12L)).isEqualTo(
+                new PackageOverride.Builder().setEnabled(false).build());
+        assertThat(result.get(34L)).isEqualTo(
+                new PackageOverride.Builder().setMinVersionCode(4).setMaxVersionCode(8).setEnabled(
+                        true).build());
+        assertThat(result.get(56L)).isEqualTo(
+                new PackageOverride.Builder().setMinVersionCode(3).setMaxVersionCode(4).setEnabled(
+                        false).build());
+        assertThat(result.get(78L)).isEqualTo(
+                new PackageOverride.Builder().setMinVersionCode(6).setMaxVersionCode(7).setEnabled(
+                        true).build());
+        assertThat(result.get(910L)).isEqualTo(
+                new PackageOverride.Builder().setMinVersionCode(5).setEnabled(true).build());
+        assertThat(result.get(1112L)).isEqualTo(
+                new PackageOverride.Builder().setMaxVersionCode(5).setEnabled(true).build());
+    }
+
+    @Test
+    public void parsePackageOverrides_changeIdsToSkipSpecified_returnsWithoutChangeIdsToSkip() {
+        ArraySet<Long> changeIdsToSkip = new ArraySet<>(Arrays.asList(34L, 56L));
+        Map<Long, PackageOverride> result = AppCompatOverridesParser.parsePackageOverrides(
+                /* configStr= */ "12:::true,56:3:7:true", /* versionCode= */ 5, changeIdsToSkip);
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(12L)).isEqualTo(
+                new PackageOverride.Builder().setEnabled(true).build());
+    }
+
+    @Test
+    public void parsePackageOverrides_changeIdsToSkipContainsAllIds_returnsEmpty() {
+        ArraySet<Long> changeIdsToSkip = new ArraySet<>(Arrays.asList(12L, 34L));
+        Map<Long, PackageOverride> result = AppCompatOverridesParser.parsePackageOverrides(
+                /* configStr= */ "12:::true", /* versionCode= */ 5, changeIdsToSkip);
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void parsePackageOverrides_someOverridesAreInvalid_returnsWithoutInvalidOverrides() {
+        // We add a valid entry before and after the invalid ones to make sure they are applied.
+        Map<Long, PackageOverride> result = AppCompatOverridesParser.parsePackageOverrides(
+                /* configStr= */ "12:::True,56:1:2:FALSE,56:3:true,78:4:8:true:,C1:::true,910:::no,"
+                        + "1112:1:ten:true,1112:one:10:true,,1314:7:3:false,34:::",
+                /* versionCode= */ 5, /* changeIdsToSkip= */ emptySet());
+
+        assertThat(result).hasSize(2);
+        assertThat(result.get(12L)).isEqualTo(
+                new PackageOverride.Builder().setEnabled(true).build());
+        assertThat(result.get(56L)).isEqualTo(
+                new PackageOverride.Builder().setMinVersionCode(1).setMaxVersionCode(2).setEnabled(
+                        false).build());
+    }
+
+    private static ApplicationInfo createAppInfo(String packageName) {
+        ApplicationInfo appInfo = new ApplicationInfo();
+        appInfo.packageName = packageName;
+        return appInfo;
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesServiceTest.java
new file mode 100644
index 0000000..007191f
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/compat/overrides/AppCompatOverridesServiceTest.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.compat.overrides;
+
+import static android.content.Intent.ACTION_PACKAGE_ADDED;
+import static android.content.Intent.ACTION_PACKAGE_CHANGED;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+import static android.content.Intent.ACTION_USER_SWITCHED;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_OWNED_CHANGE_IDS;
+import static com.android.server.compat.overrides.AppCompatOverridesParser.FLAG_REMOVE_OVERRIDES;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.compat.PackageOverride;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.compat.CompatibilityOverrideConfig;
+import com.android.internal.compat.CompatibilityOverridesToRemoveConfig;
+import com.android.internal.compat.IPlatformCompat;
+import com.android.server.testables.TestableDeviceConfig.TestableDeviceConfigRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Test class for {@link AppCompatOverridesService}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksMockingServicesTests:AppCompatOverridesServiceTest
+ */
+@RunWith(MockitoJUnitRunner.class)
+@SmallTest
+@Presubmit
+public class AppCompatOverridesServiceTest {
+    private static final String NAMESPACE_1 = "namespace_1";
+    private static final String NAMESPACE_2 = "namespace_2";
+    private static final String NAMESPACE_3 = "namespace_3";
+    private static final List<String> SUPPORTED_NAMESPACES = Arrays.asList(NAMESPACE_1,
+            NAMESPACE_2, NAMESPACE_3);
+
+    private static final String PACKAGE_1 = "com.android.test1";
+    private static final String PACKAGE_2 = "com.android.test2";
+    private static final String PACKAGE_3 = "com.android.test3";
+    private static final String PACKAGE_4 = "com.android.test4";
+
+    private MockContext mMockContext;
+    private BroadcastReceiver mPackageReceiver;
+    private AppCompatOverridesService mService;
+
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private IPlatformCompat mPlatformCompat;
+
+    @Captor
+    private ArgumentCaptor<CompatibilityOverrideConfig> mOverridesToAddConfigCaptor;
+    @Captor
+    private ArgumentCaptor<CompatibilityOverridesToRemoveConfig> mOverridesToRemoveConfigCaptor;
+
+    @Rule
+    public TestableDeviceConfigRule mDeviceConfigRule = new TestableDeviceConfigRule();
+
+    class MockContext extends ContextWrapper {
+        MockContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mPackageManager;
+        }
+
+        @Override
+        public Executor getMainExecutor() {
+            // Run on current thread
+            return Runnable::run;
+        }
+
+        @Override
+        @Nullable
+        public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver,
+                @NonNull IntentFilter filter, @Nullable String broadcastPermission,
+                @Nullable Handler scheduler) {
+            mPackageReceiver = receiver;
+            return null;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mMockContext = new MockContext(
+                InstrumentationRegistry.getInstrumentation().getTargetContext());
+        mService = new AppCompatOverridesService(mMockContext, mPlatformCompat,
+                SUPPORTED_NAMESPACES);
+        mService.registerPackageReceiver();
+        assertThat(mPackageReceiver).isNotNull();
+    }
+
+    @Test
+    public void onPropertiesChanged_removeOverridesFlagNotSet_appliesPackageOverrides()
+            throws Exception {
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 3);
+        mockGetApplicationInfoNotInstalled(PACKAGE_2);
+        mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 10);
+        mockGetApplicationInfo(PACKAGE_4, /* versionCode= */ 1);
+
+        mService.registerDeviceConfigListeners();
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "123,456,789")
+                .setString(PACKAGE_1, "123:::true,456::1:false,456:2::true,789:::false")
+                .setString(PACKAGE_2, "123:::true")
+                .setString(PACKAGE_3, "123:1:9:true,123:10:11:false,123:11::true")
+                .setString(PACKAGE_4, "").build());
+
+        Map<Long, PackageOverride> addedOverrides;
+        // Package 1
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+        addedOverrides = mOverridesToAddConfigCaptor.getValue().overrides;
+        assertThat(addedOverrides).hasSize(3);
+        assertThat(addedOverrides.get(123L)).isEqualTo(
+                new PackageOverride.Builder().setEnabled(true).build());
+        assertThat(addedOverrides.get(456L)).isEqualTo(
+                new PackageOverride.Builder().setMinVersionCode(2).setEnabled(true).build());
+        assertThat(addedOverrides.get(789L)).isEqualTo(
+                new PackageOverride.Builder().setEnabled(false).build());
+        // Package 2
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_2));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_2));
+        // Package 3
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_3));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_3));
+        addedOverrides = mOverridesToAddConfigCaptor.getValue().overrides;
+        assertThat(addedOverrides).hasSize(1);
+        assertThat(addedOverrides.get(123L)).isEqualTo(
+                new PackageOverride.Builder().setMinVersionCode(10).setMaxVersionCode(
+                        11).setEnabled(false).build());
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(456L, 789L);
+        // Package 4
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_4));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_4));
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(123L, 456L,
+                789L);
+    }
+
+    @Test
+    public void onPropertiesChanged_ownedChangeIdsFlagNotSet_onlyAddsOverrides()
+            throws Exception {
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mService.registerDeviceConfigListeners();
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "123:::true").build());
+
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+        assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(123L);
+    }
+
+    @Test
+    public void onPropertiesChanged_removeOverridesFlagSetBefore_skipsOverridesToRemove()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "123,456,789")
+                .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=123:456," + PACKAGE_2 + "=123")
+                .setString(PACKAGE_1, "123:::true")
+                .setString(PACKAGE_4, "123:::true").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0);
+
+        mService.registerDeviceConfigListeners();
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "123:::true,789:::false")
+                .setString(PACKAGE_2, "123:::true")
+                .setString(PACKAGE_3, "456:::true").build());
+
+        // Package 1
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+        assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(789L);
+        // Package 2
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_2));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_2));
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(456L, 789L);
+        // Package 3
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_3));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_3));
+        assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(456L);
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(123L, 789L);
+        // Package 4 (not applied because it hasn't changed after the listener was added)
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_4));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_4));
+    }
+
+    @Test
+    public void onPropertiesChanged_removeOverridesFlagChangedNoPackageOverridesFlags_removesOnly()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "123,456,789")
+                .setString(PACKAGE_1, "")
+                .setString(PACKAGE_2, "").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0);
+
+        mService.registerDeviceConfigListeners();
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_REMOVE_OVERRIDES,
+                        PACKAGE_1 + "=123:456," + PACKAGE_2 + "=*").build());
+
+        // Package 1
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, times(2)).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1));
+        List<CompatibilityOverridesToRemoveConfig> configs =
+                mOverridesToRemoveConfigCaptor.getAllValues();
+        assertThat(configs.size()).isAtLeast(2);
+        assertThat(configs.get(configs.size() - 2).changeIds).containsExactly(123L, 456L);
+        assertThat(configs.get(configs.size() - 1).changeIds).containsExactly(789L);
+        // Package 2
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_2));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_2));
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(123L, 456L,
+                789L);
+    }
+
+    @Test
+    public void onPropertiesChanged_removeOverridesFlagAndSomePackageOverrideFlagsChanged_ok()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "123,456,789")
+                .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=123:456")
+                .setString(PACKAGE_1, "123:::true,789:::false")
+                .setString(PACKAGE_3, "456:::false,789:::true").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0);
+
+        mService.registerDeviceConfigListeners();
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_2 + "=123," + PACKAGE_3 + "=789")
+                .setString(PACKAGE_2, "123:::true").build());
+
+        // Package 1
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_1));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1));
+        assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(123L,
+                789L);
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(456L);
+        // Package 2
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_2));
+        verify(mPlatformCompat, times(2)).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_2));
+        List<CompatibilityOverridesToRemoveConfig> configs =
+                mOverridesToRemoveConfigCaptor.getAllValues();
+        assertThat(configs.size()).isAtLeast(2);
+        assertThat(configs.get(configs.size() - 2).changeIds).containsExactly(123L);
+        assertThat(configs.get(configs.size() - 1).changeIds).containsExactly(456L, 789L);
+        // Package 3
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_3));
+        verify(mPlatformCompat, times(2)).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_3));
+        assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(456L);
+        configs = mOverridesToRemoveConfigCaptor.getAllValues();
+        assertThat(configs.size()).isAtLeast(2);
+        assertThat(configs.get(configs.size() - 2).changeIds).containsExactly(789L);
+        assertThat(configs.get(configs.size() - 1).changeIds).containsExactly(123L);
+    }
+
+    @Test
+    public void onPropertiesChanged_ownedChangeIdsFlagAndSomePackageOverrideFlagsChanged_ok()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=*")
+                .setString(FLAG_OWNED_CHANGE_IDS, "123,456")
+                .setString(PACKAGE_1, "123:::true")
+                .setString(PACKAGE_3, "456:::false").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0);
+
+        mService.registerDeviceConfigListeners();
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "123,456,789")
+                .setString(PACKAGE_2, "123:::true").build());
+
+        // Package 1
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1));
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(123L, 456L,
+                789L);
+        // Package 2
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(mOverridesToAddConfigCaptor.capture(),
+                eq(PACKAGE_2));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_2));
+        assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(123L);
+        assertThat(mOverridesToRemoveConfigCaptor.getValue().changeIds).containsExactly(456L, 789L);
+        // Package 3
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_3));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_3));
+    }
+
+    @Test
+    public void onPropertiesChanged_platformCompatThrowsExceptionForSomeCalls_skipsFailedCalls()
+            throws Exception {
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_2, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_3, /* versionCode= */ 0);
+        mockGetApplicationInfo(PACKAGE_4, /* versionCode= */ 0);
+        doThrow(new RemoteException()).when(mPlatformCompat).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_2));
+        doThrow(new RemoteException()).when(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_3));
+
+        mService.registerDeviceConfigListeners();
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "123,456")
+                .setString(PACKAGE_1, "123:::true")
+                .setString(PACKAGE_2, "123:::true")
+                .setString(PACKAGE_3, "123:::true")
+                .setString(PACKAGE_4, "123:::true").build());
+
+        // Package 1
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class),
+                eq(PACKAGE_1));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+        // Package 2
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class),
+                eq(PACKAGE_2));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_2));
+        // Package 3
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class),
+                eq(PACKAGE_3));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_3));
+        // Package 4
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(any(CompatibilityOverrideConfig.class),
+                eq(PACKAGE_1));
+        verify(mPlatformCompat).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_4));
+    }
+
+    @Test
+    public void packageReceiver_packageAddedIntentDataIsNull_doesNothing() throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_1, "201:::true").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext, new Intent(ACTION_PACKAGE_ADDED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    @Test
+    public void packageReceiver_actionIsNull_doesNothing() throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_1, "201:::true").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, /* action= */ null));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    @Test
+    public void packageReceiver_unsupportedAction_doesNothing() throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_1, "201:::true").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_USER_SWITCHED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    @Test
+    public void packageReceiver_packageAddedIntentPackageNotInstalled_doesNothing()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_1, "201:::true").build());
+        mockGetApplicationInfoNotInstalled(PACKAGE_1);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_ADDED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    @Test
+    public void packageReceiver_packageAddedIntentNoOverridesForPackage_doesNothing()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_2, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_3, "201:::true").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_ADDED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    @Test
+    public void packageReceiver_packageAddedIntent_appliesOverridesFromAllNamespaces()
+            throws Exception {
+        // We're adding the owned_change_ids flag to make sure it's ignored.
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "101,102,103")
+                .setString(PACKAGE_1, "101:::true")
+                .setString(PACKAGE_2, "102:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(FLAG_OWNED_CHANGE_IDS, "201,202,203")
+                .setString(PACKAGE_3, "201:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_3)
+                .setString(FLAG_OWNED_CHANGE_IDS, "301,302")
+                .setString(PACKAGE_1, "301:::true,302:::false")
+                .setString(PACKAGE_2, "302:::false").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_ADDED));
+
+        verify(mPlatformCompat, times(2)).putOverridesOnReleaseBuilds(
+                mOverridesToAddConfigCaptor.capture(), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+        List<CompatibilityOverrideConfig> configs = mOverridesToAddConfigCaptor.getAllValues();
+        assertThat(configs.get(0).overrides.keySet()).containsExactly(101L);
+        assertThat(configs.get(1).overrides.keySet()).containsExactly(301L, 302L);
+    }
+
+    @Test
+    public void packageReceiver_packageChangedIntent_appliesOverrides()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "101:::true,103:::false").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_CHANGED));
+
+        verify(mPlatformCompat).putOverridesOnReleaseBuilds(
+                mOverridesToAddConfigCaptor.capture(), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+        assertThat(mOverridesToAddConfigCaptor.getValue().overrides.keySet()).containsExactly(101L,
+                103L);
+    }
+
+    @Test
+    public void packageReceiver_packageAddedIntentRemoveOverridesSetForSomeNamespaces_skipsIds()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=103," + PACKAGE_2 + "=101")
+                .setString(PACKAGE_1, "101:::true,103:::false")
+                .setString(PACKAGE_2, "102:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_1, "201:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_3)
+                .setString(FLAG_REMOVE_OVERRIDES, PACKAGE_1 + "=301," + PACKAGE_3 + "=302")
+                .setString(PACKAGE_1, "301:::true,302:::false,303:::true")
+                .setString(PACKAGE_3, "302:::false").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_ADDED));
+
+        verify(mPlatformCompat, times(3)).putOverridesOnReleaseBuilds(
+                mOverridesToAddConfigCaptor.capture(), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+        List<CompatibilityOverrideConfig> configs = mOverridesToAddConfigCaptor.getAllValues();
+        assertThat(configs.get(0).overrides.keySet()).containsExactly(101L);
+        assertThat(configs.get(1).overrides.keySet()).containsExactly(201L);
+        assertThat(configs.get(2).overrides.keySet()).containsExactly(302L, 303L);
+    }
+
+    @Test
+    public void packageReceiver_packageRemovedIntentNoOverridesForPackage_doesNothing()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "101,102")
+                .setString(PACKAGE_2, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(FLAG_OWNED_CHANGE_IDS, "201,202")
+                .setString(PACKAGE_3, "201:::true").build());
+        mockGetApplicationInfoNotInstalled(PACKAGE_1);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_REMOVED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    @Test
+    public void packageReceiver_packageRemovedIntentPackageInstalledForAnotherUser_doesNothing()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "101,102,103")
+                .setString(PACKAGE_1, "101:::true,103:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(FLAG_OWNED_CHANGE_IDS, "201,202")
+                .setString(PACKAGE_1, "202:::false").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_REMOVED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    @Test
+    public void packageReceiver_packageRemovedIntent_removesOwnedOverridesForNamespacesWithPackage()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "101,102,103")
+                .setString(PACKAGE_1, "101:::true,103:::false")
+                .setString(PACKAGE_2, "102:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(FLAG_OWNED_CHANGE_IDS, "201")
+                .setString(PACKAGE_3, "201:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_3)
+                .setString(FLAG_OWNED_CHANGE_IDS, "301,302")
+                .setString(PACKAGE_1, "302:::false")
+                .setString(PACKAGE_2, "301:::true").build());
+        mockGetApplicationInfoNotInstalled(PACKAGE_1);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_REMOVED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, times(2)).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1));
+        List<CompatibilityOverridesToRemoveConfig> configs =
+                mOverridesToRemoveConfigCaptor.getAllValues();
+        assertThat(configs.get(0).changeIds).containsExactly(101L, 102L, 103L);
+        assertThat(configs.get(1).changeIds).containsExactly(301L, 302L);
+    }
+
+    @Test
+    public void packageReceiver_packageRemovedIntentNoOwnedIdsForSomeNamespace_skipsNamespace()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(FLAG_OWNED_CHANGE_IDS, "101,102")
+                .setString(PACKAGE_1, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_1, "201:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_3)
+                .setString(FLAG_OWNED_CHANGE_IDS, "301")
+                .setString(PACKAGE_1, "301:::true").build());
+        mockGetApplicationInfoNotInstalled(PACKAGE_1);
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_REMOVED));
+
+        verify(mPlatformCompat, never()).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, times(2)).removeOverridesOnReleaseBuilds(
+                mOverridesToRemoveConfigCaptor.capture(), eq(PACKAGE_1));
+        List<CompatibilityOverridesToRemoveConfig> configs =
+                mOverridesToRemoveConfigCaptor.getAllValues();
+        assertThat(configs.get(0).changeIds).containsExactly(101L, 102L);
+        assertThat(configs.get(1).changeIds).containsExactly(301L);
+    }
+
+    @Test
+    public void packageReceiver_platformCompatThrowsExceptionForSomeNamespace_skipsFailedCall()
+            throws Exception {
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_1)
+                .setString(PACKAGE_1, "101:::true").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_2)
+                .setString(PACKAGE_1, "201:::false").build());
+        DeviceConfig.setProperties(new Properties.Builder(NAMESPACE_3)
+                .setString(PACKAGE_1, "301:::true").build());
+        mockGetApplicationInfo(PACKAGE_1, /* versionCode= */ 0);
+        doThrow(new RemoteException()).when(mPlatformCompat).putOverridesOnReleaseBuilds(
+                argThat(config -> config.overrides.containsKey(201L)), eq(PACKAGE_1));
+
+        mPackageReceiver.onReceive(mMockContext,
+                createPackageIntent(PACKAGE_1, ACTION_PACKAGE_ADDED));
+
+        verify(mPlatformCompat, times(3)).putOverridesOnReleaseBuilds(
+                any(CompatibilityOverrideConfig.class), eq(PACKAGE_1));
+        verify(mPlatformCompat, never()).removeOverridesOnReleaseBuilds(
+                any(CompatibilityOverridesToRemoveConfig.class), eq(PACKAGE_1));
+    }
+
+    private void mockGetApplicationInfo(String packageName, long versionCode)
+            throws Exception {
+        when(mPackageManager.getApplicationInfo(eq(packageName), anyInt())).thenReturn(
+                createAppInfo(versionCode));
+    }
+
+    private void mockGetApplicationInfoNotInstalled(String packageName) throws Exception {
+        when(mPackageManager.getApplicationInfo(eq(packageName), anyInt()))
+                .thenThrow(new PackageManager.NameNotFoundException());
+    }
+
+    private static ApplicationInfo createAppInfo(long versionCode) {
+        ApplicationInfo appInfo = new ApplicationInfo();
+        appInfo.longVersionCode = versionCode;
+        return appInfo;
+    }
+
+    private Intent createPackageIntent(String packageName, @Nullable String action) {
+        return new Intent(action, Uri.parse("package:" + packageName));
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java
index 589a349..457c8db 100644
--- a/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java
@@ -48,7 +48,7 @@
 import org.mockito.quality.Strictness;
 
 /**
- * Run it as {@code atest FrameworksMockingCoreTests:FactoryResetterTest}
+ * Run it as {@code atest FrameworksMockingServicesTests:FactoryResetterTest}
  */
 @Presubmit
 public final class FactoryResetterTest {
diff --git a/services/tests/mockingservicestests/src/com/android/server/devicepolicy/OwnerShellDataTest.java b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/OwnerShellDataTest.java
new file mode 100644
index 0000000..dd67d72
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/OwnerShellDataTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.devicepolicy;
+
+import static android.os.UserHandle.USER_NULL;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.expectThrows;
+
+import android.content.ComponentName;
+
+import org.junit.Test;
+
+/**
+ * Run it as {@code atest FrameworksMockingServicesTests:OwnerShellDataTest}
+ */
+public final class OwnerShellDataTest {
+
+    private static final int USER_ID = 007;
+    private static final int PARENT_USER_ID = 'M' + 'I' + 6;
+    private static final ComponentName ADMIN = new ComponentName("Bond", "James");
+
+    @Test
+    public void testForDeviceOwner_noAdmin() {
+        expectThrows(NullPointerException.class,
+                () -> OwnerShellData.forDeviceOwner(USER_ID, /* admin= */ null));
+    }
+
+    @Test
+    public void testForDeviceOwner_invalidUser() {
+        expectThrows(IllegalArgumentException.class,
+                () -> OwnerShellData.forDeviceOwner(USER_NULL, ADMIN));
+    }
+
+    @Test
+    public void testForDeviceOwner() {
+        OwnerShellData dto = OwnerShellData.forDeviceOwner(USER_ID, ADMIN);
+
+        assertWithMessage("dto(%s).userId", dto).that(dto.userId).isEqualTo(USER_ID);
+        assertWithMessage("dto(%s).parentUserId", dto).that(dto.parentUserId)
+                .isEqualTo(USER_NULL);
+        assertWithMessage("dto(%s).admin", dto).that(dto.admin).isSameInstanceAs(ADMIN);
+        assertWithMessage("dto(%s).isDeviceOwner", dto).that(dto.isDeviceOwner).isTrue();
+        assertWithMessage("dto(%s).isProfileOwner", dto).that(dto.isProfileOwner).isFalse();
+        assertWithMessage("dto(%s).isManagedProfileOwner", dto).that(dto.isManagedProfileOwner)
+                .isFalse();
+        assertWithMessage("dto(%s).isAffiliated", dto).that(dto.isAffiliated).isFalse();
+    }
+
+    @Test
+    public void testForUserProfileOwner_noAdmin() {
+        expectThrows(NullPointerException.class,
+                () -> OwnerShellData.forUserProfileOwner(USER_ID, /* admin= */ null));
+    }
+
+    @Test
+    public void testForUserProfileOwner_invalidUser() {
+        expectThrows(IllegalArgumentException.class,
+                () -> OwnerShellData.forUserProfileOwner(USER_NULL, ADMIN));
+    }
+
+    @Test
+    public void testForUserProfileOwner() {
+        OwnerShellData dto = OwnerShellData.forUserProfileOwner(USER_ID, ADMIN);
+
+        assertWithMessage("dto(%s).userId", dto).that(dto.userId).isEqualTo(USER_ID);
+        assertWithMessage("dto(%s).parentUserId", dto).that(dto.parentUserId)
+                .isEqualTo(USER_NULL);
+        assertWithMessage("dto(%s).admin", dto).that(dto.admin).isSameInstanceAs(ADMIN);
+        assertWithMessage("dto(%s).isDeviceOwner", dto).that(dto.isDeviceOwner).isFalse();
+        assertWithMessage("dto(%s).isProfileOwner", dto).that(dto.isProfileOwner).isTrue();
+        assertWithMessage("dto(%s).isManagedProfileOwner", dto).that(dto.isManagedProfileOwner)
+                .isFalse();
+        assertWithMessage("dto(%s).isAffiliated", dto).that(dto.isAffiliated).isFalse();
+    }
+
+    @Test
+    public void testForManagedProfileOwner_noAdmin() {
+        expectThrows(NullPointerException.class,
+                () -> OwnerShellData.forManagedProfileOwner(USER_ID, PARENT_USER_ID, null));
+    }
+
+    @Test
+    public void testForManagedProfileOwner_invalidUser() {
+        expectThrows(IllegalArgumentException.class,
+                () -> OwnerShellData.forManagedProfileOwner(USER_NULL, PARENT_USER_ID, ADMIN));
+    }
+
+    @Test
+    public void testForManagedProfileOwner_invalidParent() {
+        expectThrows(IllegalArgumentException.class,
+                () -> OwnerShellData.forManagedProfileOwner(USER_ID, USER_NULL, ADMIN));
+    }
+
+    @Test
+    public void testForManagedProfileOwner_parentOfItself() {
+        expectThrows(IllegalArgumentException.class,
+                () -> OwnerShellData.forManagedProfileOwner(USER_ID, USER_ID, ADMIN));
+    }
+
+    @Test
+    public void testForManagedProfileOwner() {
+        OwnerShellData dto = OwnerShellData.forManagedProfileOwner(USER_ID, PARENT_USER_ID, ADMIN);
+
+        assertWithMessage("dto(%s).userId", dto).that(dto.userId).isEqualTo(USER_ID);
+        assertWithMessage("dto(%s).parentUserId", dto).that(dto.parentUserId)
+                .isEqualTo(PARENT_USER_ID);
+        assertWithMessage("dto(%s).admin", dto).that(dto.admin).isSameInstanceAs(ADMIN);
+        assertWithMessage("dto(%s).isDeviceOwner", dto).that(dto.isDeviceOwner).isFalse();
+        assertWithMessage("dto(%s).isProfileOwner", dto).that(dto.isProfileOwner).isFalse();
+        assertWithMessage("dto(%s).isManagedProfileOwner", dto).that(dto.isManagedProfileOwner)
+                .isTrue();
+        assertWithMessage("dto(%s).isAffiliated", dto).that(dto.isAffiliated).isFalse();
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 0efcc57..28cdd63 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -134,6 +134,20 @@
         when(mMockedResources.getFloat(com.android.internal.R.dimen
                 .config_screenBrightnessSettingMaximumFloat))
                 .thenReturn(BACKLIGHT_RANGE_ZERO_TO_ONE[1]);
+        when(mMockedResources.getStringArray(R.array.config_displayUniqueIdArray))
+                .thenReturn(new String[]{});
+        TypedArray mockArray = mock(TypedArray.class);
+        when(mockArray.length()).thenReturn(0);
+        when(mMockedResources.obtainTypedArray(R.array.config_maskBuiltInDisplayCutoutArray))
+                .thenReturn(mockArray);
+        when(mMockedResources.obtainTypedArray(R.array.config_waterfallCutoutArray))
+                .thenReturn(mockArray);
+        when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerRadiusArray))
+                .thenReturn(mockArray);
+        when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerTopRadiusArray))
+                .thenReturn(mockArray);
+        when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerBottomRadiusArray))
+                .thenReturn(mockArray);
     }
 
     @After
diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java
index 64b24c1..43188f6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java
+++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java
@@ -95,18 +95,25 @@
                     String name = invocationOnMock.getArgument(1);
                     String value = invocationOnMock.getArgument(2);
                     mKeyValueMap.put(getKey(namespace, name), value);
-                    for (DeviceConfig.OnPropertiesChangedListener listener :
-                            mOnPropertiesChangedListenerMap.keySet()) {
-                        if (namespace.equals(mOnPropertiesChangedListenerMap.get(listener).first)) {
-                            mOnPropertiesChangedListenerMap.get(listener).second.execute(
-                                    () -> listener.onPropertiesChanged(
-                                            getProperties(namespace, name, value)));
-                        }
-                    }
+                    invokeListeners(namespace, getProperties(namespace, name, value));
                     return true;
                 }
         ).when(() -> DeviceConfig.setProperty(anyString(), anyString(), anyString(), anyBoolean()));
 
+        doAnswer((Answer<Boolean>) invocationOnMock -> {
+                    Properties properties = invocationOnMock.getArgument(0);
+                    String namespace = properties.getNamespace();
+                    Map<String, String> keyValues = new ArrayMap<>();
+                    for (String name : properties.getKeyset()) {
+                        String value = properties.getString(name, /* defaultValue= */ "");
+                        mKeyValueMap.put(getKey(namespace, name), value);
+                        keyValues.put(name.toLowerCase(), value);
+                    }
+                    invokeListeners(namespace, getProperties(namespace, keyValues));
+                    return true;
+                }
+        ).when(() -> DeviceConfig.setProperties(any(Properties.class)));
+
         doAnswer((Answer<String>) invocationOnMock -> {
             String namespace = invocationOnMock.getArgument(0);
             String name = invocationOnMock.getArgument(1);
@@ -153,6 +160,16 @@
         return Pair.create(values[0], values[1]);
     }
 
+    private void invokeListeners(String namespace, Properties properties) {
+        for (DeviceConfig.OnPropertiesChangedListener listener :
+                mOnPropertiesChangedListenerMap.keySet()) {
+            if (namespace.equals(mOnPropertiesChangedListenerMap.get(listener).first)) {
+                mOnPropertiesChangedListenerMap.get(listener).second.execute(
+                        () -> listener.onPropertiesChanged(properties));
+            }
+        }
+    }
+
     private Properties getProperties(String namespace, String name, String value) {
         return getProperties(namespace, Collections.singletonMap(name.toLowerCase(), value));
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java
index 0e40669..d68b814 100644
--- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java
@@ -23,6 +23,7 @@
 import android.app.ActivityThread;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.BadConfigException;
 import android.provider.DeviceConfig.Properties;
 
 import androidx.test.filters.SmallTest;
@@ -92,6 +93,16 @@
     }
 
     @Test
+    public void setProperties() throws BadConfigException {
+        String newKey = "key2";
+        String newValue = "value2";
+        DeviceConfig.setProperties(new Properties.Builder(sNamespace).setString(sKey,
+                sValue).setString(newKey, newValue).build());
+        assertThat(DeviceConfig.getProperty(sNamespace, sKey)).isEqualTo(sValue);
+        assertThat(DeviceConfig.getProperty(sNamespace, newKey)).isEqualTo(newValue);
+    }
+
+    @Test
     public void getProperties_empty() {
         String newKey = "key2";
         String newValue = "value2";
@@ -131,13 +142,12 @@
     }
 
     @Test
-    public void testListener() throws InterruptedException {
+    public void testListener_setProperty() throws InterruptedException {
         CountDownLatch countDownLatch = new CountDownLatch(1);
 
         OnPropertiesChangedListener changeListener = (properties) -> {
             assertThat(properties.getNamespace()).isEqualTo(sNamespace);
-            assertThat(properties.getKeyset().size()).isEqualTo(1);
-            assertThat(properties.getKeyset()).contains(sKey);
+            assertThat(properties.getKeyset()).containsExactly(sKey);
             assertThat(properties.getString(sKey, "bogus_value")).isEqualTo(sValue);
             assertThat(properties.getString("bogus_key", "bogus_value")).isEqualTo("bogus_value");
             countDownLatch.countDown();
@@ -153,6 +163,32 @@
         }
     }
 
+    @Test
+    public void testListener_setProperties() throws BadConfigException, InterruptedException {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        String newKey = "key2";
+        String newValue = "value2";
+
+        OnPropertiesChangedListener changeListener = (properties) -> {
+            assertThat(properties.getNamespace()).isEqualTo(sNamespace);
+            assertThat(properties.getKeyset()).containsExactly(sKey, newKey);
+            assertThat(properties.getString(sKey, "bogus_value")).isEqualTo(sValue);
+            assertThat(properties.getString(newKey, "bogus_value")).isEqualTo(newValue);
+            assertThat(properties.getString("bogus_key", "bogus_value")).isEqualTo("bogus_value");
+            countDownLatch.countDown();
+        };
+        try {
+            DeviceConfig.addOnPropertiesChangedListener(sNamespace,
+                    ActivityThread.currentApplication().getMainExecutor(), changeListener);
+            DeviceConfig.setProperties(new Properties.Builder(sNamespace).setString(sKey,
+                    sValue).setString(newKey, newValue).build());
+            assertThat(countDownLatch.await(
+                    WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+        } finally {
+            DeviceConfig.removeOnPropertiesChangedListener(changeListener);
+        }
+    }
+
 }
 
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index a49afc7..fe23c14 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -152,6 +152,7 @@
         sContext.getTestablePermissions().setPermission(
                 android.Manifest.permission.SET_WALLPAPER,
                 PackageManager.PERMISSION_GRANTED);
+        doNothing().when(sContext).sendBroadcastAsUser(any(), any());
 
         //Wallpaper components
         sWallpaperService = mock(IWallpaperConnection.Stub.class);
@@ -188,7 +189,6 @@
         MockitoAnnotations.initMocks(this);
 
         sContext.addMockSystemService(DisplayManager.class, mDisplayManager);
-        doNothing().when(sContext).sendBroadcastAsUser(any(), any());
 
         final Display mockDisplay = mock(Display.class);
         doReturn(DISPLAY_SIZE_DIMENSION).when(mockDisplay).getMaximumSizeDimension();
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index bcb2cf8..68b8469 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -71,6 +71,7 @@
     <uses-permission android:name="android.permission.MANAGE_BIND_INSTANT_SERVICE"/>
     <uses-permission android:name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS"/>
     <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS"/>
+    <uses-permission android:name="android.permission.CONFIGURE_DISPLAY_BRIGHTNESS"/>
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
     <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/>
     <uses-permission android:name="android.permission.HARDWARE_TEST"/>
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
index d6c11a5..e612d12 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
@@ -65,6 +65,7 @@
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.content.ComponentName;
 import android.content.Context;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java
index 80e81d6..554f0a4 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java
@@ -29,6 +29,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
@@ -176,7 +177,7 @@
     }
 
     @Test
-    public void testEventHandler_shouldChangeAfterOnDisplayChanged() {
+    public void testEventHandler_shouldIncreaseAndHaveCorrectOrderAfterOnDisplayAdded() {
         prepareLooper();
 
         // Check if there is only one mEventHandler when there is one default display.
@@ -184,13 +185,51 @@
         assertEquals(1, mEventHandler.size());
 
         // Check if it has correct numbers of mEventHandler for corresponding displays.
-        setDisplayCount(4);
-        mA11yInputFilter.onDisplayChanged();
-        assertEquals(4, mEventHandler.size());
+        setDisplayCount(2);
+        mA11yInputFilter.onDisplayAdded(mDisplayList.get(SECOND_DISPLAY));
+        assertEquals(2, mEventHandler.size());
+
+        EventStreamTransformation next = mEventHandler.get(SECOND_DISPLAY);
+        assertNotNull(next);
+
+        // Start from index 1 because KeyboardInterceptor only exists in EventHandler for
+        // DEFAULT_DISPLAY.
+        for (int i = 1; next != null; i++) {
+            assertEquals(next.getClass(), mExpectedEventHandlerTypes[i]);
+            next = next.getNext();
+        }
+    }
+
+    @Test
+    public void testEventHandler_shouldDecreaseAfterOnDisplayRemoved() {
+        prepareLooper();
 
         setDisplayCount(2);
-        mA11yInputFilter.onDisplayChanged();
+        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
         assertEquals(2, mEventHandler.size());
+
+        // Check if it has correct numbers of mEventHandler for corresponding displays.
+        mA11yInputFilter.onDisplayRemoved(SECOND_DISPLAY);
+        assertEquals(1, mEventHandler.size());
+
+        EventStreamTransformation eventHandler = mEventHandler.get(SECOND_DISPLAY);
+        assertNull(eventHandler);
+    }
+
+    @Test
+    public void testEventHandler_shouldNoChangedInOtherDisplayAfterOnDisplayRemoved() {
+        prepareLooper();
+
+        setDisplayCount(2);
+        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
+        EventStreamTransformation eventHandlerBeforeDisplayRemoved =
+                mEventHandler.get(DEFAULT_DISPLAY);
+
+        mA11yInputFilter.onDisplayRemoved(SECOND_DISPLAY);
+        EventStreamTransformation eventHandlerAfterDisplayRemoved =
+                mEventHandler.get(DEFAULT_DISPLAY);
+
+        assertEquals(eventHandlerBeforeDisplayRemoved, eventHandlerAfterDisplayRemoved);
     }
 
     @Test
@@ -240,7 +279,7 @@
     }
 
     @Test
-    public void testInputEvent_shouldClearEventsForAllEventHandlers() {
+    public void testInputEvent_shouldClearEventsForDisplayEventHandlers() {
         prepareLooper();
 
         mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
@@ -253,13 +292,71 @@
         send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
         assertEquals(2, mCaptor1.mEvents.size());
 
-        // InputEvent with different input source should trigger clearEvents() for each
-        // EventStreamTransformation in EventHandler.
+        // InputEvent with different input source to the same display should trigger
+        // clearEvents() for the EventHandler in this display.
         send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_MOUSE));
         assertEquals(1, mCaptor1.mEvents.size());
     }
 
     @Test
+    public void testInputEvent_shouldNotClearEventsForOtherDisplayEventHandlers() {
+        prepareLooper();
+
+        setDisplayCount(2);
+        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
+        assertEquals(2, mEventHandler.size());
+
+        mCaptor1 = new EventCaptor();
+        mCaptor2 = new EventCaptor();
+        mEventHandler.put(DEFAULT_DISPLAY, mCaptor1);
+        mEventHandler.put(SECOND_DISPLAY, mCaptor2);
+
+        // InputEvent with different displayId should be dispatched to corresponding EventHandler.
+        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
+        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
+        send(downEvent(SECOND_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
+
+        // InputEvent with different input source should not trigger clearEvents() for
+        // the EventHandler in the other display.
+        send(downEvent(SECOND_DISPLAY, InputDevice.SOURCE_MOUSE));
+        assertEquals(2, mCaptor1.mEvents.size());
+    }
+
+    @Test
+    public void testInputEvent_shouldNotClearEventsForOtherDisplayAfterOnDisplayAdded() {
+        prepareLooper();
+
+        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
+        mCaptor1 = new EventCaptor();
+        mEventHandler.put(DEFAULT_DISPLAY, mCaptor1);
+
+        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
+        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
+        assertEquals(2, mCaptor1.mEvents.size());
+
+        setDisplayCount(2);
+        mA11yInputFilter.onDisplayAdded(mDisplayList.get(SECOND_DISPLAY));
+        assertEquals(2, mCaptor1.mEvents.size());
+    }
+
+    @Test
+    public void testInputEvent_shouldNotClearEventsForOtherDisplayAfterOnDisplayRemoved() {
+        prepareLooper();
+
+        setDisplayCount(2);
+        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
+        mCaptor1 = new EventCaptor();
+        mEventHandler.put(DEFAULT_DISPLAY, mCaptor1);
+
+        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
+        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
+        assertEquals(2, mCaptor1.mEvents.size());
+
+        mA11yInputFilter.onDisplayRemoved(SECOND_DISPLAY);
+        assertEquals(2, mCaptor1.mEvents.size());
+    }
+
+    @Test
     public void testEnabledFeatures_windowMagnificationMode_expectedMagnificationGestureHandler() {
         prepareLooper();
         doReturn(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW).when(
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
index 00daa5c..432a500 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.GestureDescription;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.content.ComponentName;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
index e4d51e4..4afe099 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
@@ -120,6 +120,7 @@
     @Mock private AccessibilityWindowManager.AccessibilityEventSender mMockA11yEventSender;
     @Mock private AccessibilitySecurityPolicy mMockA11ySecurityPolicy;
     @Mock private AccessibilitySecurityPolicy.AccessibilityUserManager mMockA11yUserManager;
+    @Mock private AccessibilityTraceManager mMockA11yTraceManager;
 
     @Mock private IBinder mMockHostToken;
     @Mock private IBinder mMockEmbeddedToken;
@@ -140,7 +141,8 @@
                 mMockWindowManagerInternal,
                 mMockA11yEventSender,
                 mMockA11ySecurityPolicy,
-                mMockA11yUserManager);
+                mMockA11yUserManager,
+                mMockA11yTraceManager);
         // Starts tracking window of default display and sets the default display
         // as top focused display before each testing starts.
         startTrackingPerDisplay(Display.DEFAULT_DISPLAY);
@@ -834,6 +836,19 @@
         assertNull(token);
     }
 
+    @Test
+    public void onDisplayReparented_shouldRemoveObserver() throws RemoteException {
+        // Starts tracking window of second display.
+        startTrackingPerDisplay(SECONDARY_DISPLAY_ID);
+        assertTrue(mA11yWindowManager.isTrackingWindowsLocked(SECONDARY_DISPLAY_ID));
+        // Notifies the second display is an embedded one of the default display.
+        final WindowsForAccessibilityCallback callbacks =
+                mCallbackOfWindows.get(Display.DEFAULT_DISPLAY);
+        callbacks.onDisplayReparented(SECONDARY_DISPLAY_ID);
+        // Makes sure the observer of the second display is removed.
+        assertFalse(mA11yWindowManager.isTrackingWindowsLocked(SECONDARY_DISPLAY_ID));
+    }
+
     private void registerLeashedTokenAndWindowId() {
         mA11yWindowManager.registerIdLocked(mMockHostToken, HOST_WINDOW_ID);
         mA11yWindowManager.registerIdLocked(mMockEmbeddedToken, EMBEDDED_WINDOW_ID);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/KeyboardInterceptorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/KeyboardInterceptorTest.java
index 78e651b..c62cae5 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/KeyboardInterceptorTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/KeyboardInterceptorTest.java
@@ -56,11 +56,13 @@
     private MessageCapturingHandler mHandler = new MessageCapturingHandler(
             msg -> mInterceptor.handleMessage(msg));
     @Mock AccessibilityManagerService mMockAms;
+    @Mock AccessibilityTraceManager mMockTraceManager;
     @Mock WindowManagerPolicy mMockPolicy;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        when(mMockAms.getTraceManager()).thenReturn(mMockTraceManager);
         mInterceptor = new KeyboardInterceptor(mMockAms, mMockPolicy, mHandler);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
index d4908ee..59b69f9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
@@ -122,6 +122,7 @@
 
     MotionEventInjector mMotionEventInjector;
     IAccessibilityServiceClient mServiceInterface;
+    AccessibilityTraceManager mTrace;
     List<GestureStep> mLineList = new ArrayList<>();
     List<GestureStep> mClickList = new ArrayList<>();
     List<GestureStep> mContinuedLineList1 = new ArrayList<>();
@@ -148,7 +149,8 @@
                 return mMotionEventInjector.handleMessage(msg);
             }
         });
-        mMotionEventInjector = new MotionEventInjector(mMessageCapturingHandler);
+        mTrace = mock(AccessibilityTraceManager.class);
+        mMotionEventInjector = new MotionEventInjector(mMessageCapturingHandler, mTrace);
         mServiceInterface = mock(IAccessibilityServiceClient.class);
 
         mLineList = createSimpleGestureFromPoints(0, 0, false, LINE_DURATION, LINE_START, LINE_END);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java
index 1603087..4ce9ba0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.app.UiAutomation;
 import android.content.Context;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index 57ee7aa..4a06611f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -36,6 +36,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityGestureEvent;
 import android.accessibilityservice.AccessibilityService;
@@ -53,6 +54,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.EventStreamTransformation;
 import com.android.server.accessibility.utils.GestureLogParser;
 import com.android.server.testutils.OffsettableClock;
@@ -107,6 +109,8 @@
 
     @Mock
     private AccessibilityManagerService mMockAms;
+    @Mock
+    private AccessibilityTraceManager mMockTraceManager;
     @Captor
     private ArgumentCaptor<AccessibilityGestureEvent> mGestureCaptor;
 
@@ -143,6 +147,7 @@
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
+        when(mMockAms.getTraceManager()).thenReturn(mMockTraceManager);
         mContext = InstrumentationRegistry.getContext();
         mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
         mCaptor = new EventCaptor();
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index 502f64a..fe4fed9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -52,6 +52,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.test.MessageCapturingHandler;
 import com.android.server.wm.WindowManagerInternal;
 import com.android.server.wm.WindowManagerInternal.MagnificationCallbacks;
@@ -93,6 +94,7 @@
             mock(FullScreenMagnificationController.ControllerContext.class);
     final Context mMockContext = mock(Context.class);
     final AccessibilityManagerService mMockAms = mock(AccessibilityManagerService.class);
+    final AccessibilityTraceManager mMockTraceManager = mock(AccessibilityTraceManager.class);
     final WindowManagerInternal mMockWindowManager = mock(WindowManagerInternal.class);
     private final MagnificationAnimationCallback mAnimationCallback = mock(
             MagnificationAnimationCallback.class);
@@ -113,9 +115,11 @@
         when(mMockContext.getMainLooper()).thenReturn(looper);
         when(mMockControllerCtx.getContext()).thenReturn(mMockContext);
         when(mMockControllerCtx.getAms()).thenReturn(mMockAms);
+        when(mMockControllerCtx.getTraceManager()).thenReturn(mMockTraceManager);
         when(mMockControllerCtx.getWindowManager()).thenReturn(mMockWindowManager);
         when(mMockControllerCtx.getHandler()).thenReturn(mMessageCapturingHandler);
         when(mMockControllerCtx.getAnimationDuration()).thenReturn(1000L);
+        when(mMockAms.getTraceManager()).thenReturn(mMockTraceManager);
         initMockWindowManager();
 
         mFullScreenMagnificationController = new FullScreenMagnificationController(
@@ -773,20 +777,20 @@
     }
 
     @Test
-    public void testRotation_resetsMagnification() {
+    public void testDisplaySizeChanged_resetsMagnification() {
         for (int i = 0; i < DISPLAY_COUNT; i++) {
-            rotation_resetsMagnification(i);
+            changeDisplaySize_resetsMagnification(i);
             resetMockWindowManager();
         }
     }
 
-    private void rotation_resetsMagnification(int displayId) {
+    private void changeDisplaySize_resetsMagnification(int displayId) {
         register(displayId);
         MagnificationCallbacks callbacks = getMagnificationCallbacks(displayId);
         zoomIn2xToMiddle(displayId);
         mMessageCapturingHandler.sendAllMessages();
         assertTrue(mFullScreenMagnificationController.isMagnifying(displayId));
-        callbacks.onRotationChanged(0);
+        callbacks.onDisplaySizeChanged();
         mMessageCapturingHandler.sendAllMessages();
         assertFalse(mFullScreenMagnificationController.isMagnifying(displayId));
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index f881f04..b14c353 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -52,6 +52,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.EventStreamTransformation;
 import com.android.server.accessibility.magnification.FullScreenMagnificationController.MagnificationInfoChangedCallback;
 import com.android.server.testutils.OffsettableClock;
@@ -129,6 +130,10 @@
     MagnificationInfoChangedCallback mMagnificationInfoChangedCallback;
     @Mock
     WindowMagnificationPromptController mWindowMagnificationPromptController;
+    @Mock
+    AccessibilityManagerService mMockAccessibilityManagerService;
+    @Mock
+    AccessibilityTraceManager mMockTraceManager;
 
     private OffsettableClock mClock;
     private FullScreenMagnificationGestureHandler mMgh;
@@ -144,7 +149,9 @@
                 mock(FullScreenMagnificationController.ControllerContext.class);
         final WindowManagerInternal mockWindowManager = mock(WindowManagerInternal.class);
         when(mockController.getContext()).thenReturn(mContext);
-        when(mockController.getAms()).thenReturn(mock(AccessibilityManagerService.class));
+        when(mockController.getAms()).thenReturn(mMockAccessibilityManagerService);
+        when(mMockAccessibilityManagerService.getTraceManager()).thenReturn(mMockTraceManager);
+        when(mockController.getTraceManager()).thenReturn(mMockTraceManager);
         when(mockController.getWindowManager()).thenReturn(mockWindowManager);
         when(mockController.getHandler()).thenReturn(new Handler(mContext.getMainLooper()));
         when(mockController.newValueAnimator()).thenReturn(new ValueAnimator());
@@ -179,7 +186,7 @@
     private FullScreenMagnificationGestureHandler newInstance(boolean detectTripleTap,
             boolean detectShortcutTrigger) {
         FullScreenMagnificationGestureHandler h = new FullScreenMagnificationGestureHandler(
-                mContext, mFullScreenMagnificationController, mMockCallback,
+                mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
                 detectTripleTap, detectShortcutTrigger,
                 mWindowMagnificationPromptController, DISPLAY_0);
         mHandler = new TestHandler(h.mDetectingState, mClock) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index b7f5f4d..e82adc8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -54,6 +54,7 @@
 
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.AccessibilityTraceManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -84,6 +85,8 @@
             Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
 
     @Mock
+    private AccessibilityTraceManager mTraceManager;
+    @Mock
     private AccessibilityManagerService mService;
     @Mock
     private MagnificationController.TransitionCallBack mTransitionCallBack;
@@ -112,7 +115,7 @@
                 CURRENT_USER_ID);
         mWindowMagnificationManager = Mockito.spy(
                 new WindowMagnificationManager(mContext, CURRENT_USER_ID,
-                        mock(WindowMagnificationManager.Callback.class)));
+                        mock(WindowMagnificationManager.Callback.class), mTraceManager));
         mMockConnection = new MockWindowMagnificationConnection(true);
         mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
         mScreenMagnificationControllerStubber = new FullScreenMagnificationControllerStubber(
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java
index 514d16a..ef6ed88 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java
@@ -31,6 +31,8 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.accessibility.AccessibilityTraceManager;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -49,6 +51,8 @@
             Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
 
     @Mock
+    AccessibilityTraceManager mTraceManager;
+    @Mock
     MagnificationGestureHandler.Callback mCallback;
 
     @Before
@@ -57,6 +61,7 @@
         mMgh = new TestMagnificationGestureHandler(DISPLAY_0,
                 /* detectTripleTap= */true,
                 /* detectShortcutTrigger= */true,
+                mTraceManager,
                 mCallback);
     }
 
@@ -129,8 +134,9 @@
         boolean mIsInternalMethodCalled = false;
 
         TestMagnificationGestureHandler(int displayId, boolean detectTripleTap,
-                boolean detectShortcutTrigger, @NonNull Callback callback) {
-            super(displayId, detectTripleTap, detectShortcutTrigger, callback);
+                boolean detectShortcutTrigger, @NonNull AccessibilityTraceManager trace,
+                @NonNull Callback callback) {
+            super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback);
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
index c88bc3b..1638563 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
@@ -29,6 +29,8 @@
 import android.view.accessibility.IWindowMagnificationConnectionCallback;
 import android.view.accessibility.MagnificationAnimationCallback;
 
+import com.android.server.accessibility.AccessibilityTraceManager;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -45,6 +47,8 @@
 
     private IWindowMagnificationConnection mConnection;
     @Mock
+    private AccessibilityTraceManager mTrace;
+    @Mock
     private IWindowMagnificationConnectionCallback mCallback;
     @Mock
     private MagnificationAnimationCallback mAnimationCallback;
@@ -57,7 +61,7 @@
         MockitoAnnotations.initMocks(this);
         mMockWindowMagnificationConnection = new MockWindowMagnificationConnection();
         mConnection = mMockWindowMagnificationConnection.getConnection();
-        mConnectionWrapper = new WindowMagnificationConnectionWrapper(mConnection);
+        mConnectionWrapper = new WindowMagnificationConnectionWrapper(mConnection, mTrace);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
index b9498d6..6a5aae6 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
@@ -35,6 +35,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.EventStreamTransformation;
 import com.android.server.accessibility.utils.TouchEventGenerator;
 
@@ -74,16 +75,18 @@
     private WindowMagnificationGestureHandler mWindowMagnificationGestureHandler;
     @Mock
     MagnificationGestureHandler.Callback mMockCallback;
+    @Mock
+    AccessibilityTraceManager mMockTrace;
 
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mWindowMagnificationManager = new WindowMagnificationManager(mContext, 0,
-                mock(WindowMagnificationManager.Callback.class));
+                mock(WindowMagnificationManager.Callback.class), mMockTrace);
         mMockConnection = new MockWindowMagnificationConnection();
         mWindowMagnificationGestureHandler = new WindowMagnificationGestureHandler(
-                mContext, mWindowMagnificationManager, mMockCallback,
+                mContext, mWindowMagnificationManager, mMockTrace, mMockCallback,
                 /** detectTripleTap= */true,   /** detectShortcutTrigger= */true, DISPLAY_0);
         mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
         mWindowMagnificationGestureHandler.setNext(strictMock(EventStreamTransformation.class));
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
index a20272a..af6d40f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
@@ -52,6 +52,7 @@
 
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
+import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.statusbar.StatusBarManagerInternal;
 
 import org.junit.Before;
@@ -72,6 +73,8 @@
     @Mock
     private Context mContext;
     @Mock
+    private AccessibilityTraceManager mMockTrace;
+    @Mock
     private StatusBarManagerInternal mMockStatusBarManagerInternal;
     @Mock
     private MagnificationAnimationCallback mAnimationCallback;
@@ -88,7 +91,7 @@
         mResolver = new MockContentResolver();
         mMockConnection = new MockWindowMagnificationConnection();
         mWindowMagnificationManager = new WindowMagnificationManager(mContext, CURRENT_USER_ID,
-                mMockCallback);
+                mMockCallback, mMockTrace);
 
         when(mContext.getContentResolver()).thenReturn(mResolver);
         doAnswer((InvocationOnMock invocation) -> {
diff --git a/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java b/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java
index 4a67ec7..6faa7e7 100644
--- a/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java
@@ -74,15 +74,20 @@
     @Test
     public void testTargetedEnergyConsumerQuerying() {
         final int numCpuClusters = 4;
+        final int numDisplays = 5;
         final int numOther = 3;
 
         // Add some energy consumers used by BatteryExternalStatsWorker.
         final IntArray tempAllIds = new IntArray();
 
-        final int displayId = mPowerStatsInternal.addEnergyConsumer(EnergyConsumerType.DISPLAY, 0,
-                "display");
-        tempAllIds.add(displayId);
-        mPowerStatsInternal.incrementEnergyConsumption(displayId, 12345);
+        final int[] displayIds = new int[numDisplays];
+        for (int i = 0; i < numDisplays; i++) {
+            displayIds[i] = mPowerStatsInternal.addEnergyConsumer(
+                    EnergyConsumerType.DISPLAY, i, "display" + i);
+            tempAllIds.add(displayIds[i]);
+            mPowerStatsInternal.incrementEnergyConsumption(displayIds[i], 12345 + i);
+        }
+        Arrays.sort(displayIds);
 
         final int wifiId = mPowerStatsInternal.addEnergyConsumer(EnergyConsumerType.WIFI, 0,
                 "wifi");
@@ -130,9 +135,13 @@
 
         final EnergyConsumerResult[] displayResults =
                 mBatteryExternalStatsWorker.getMeasuredEnergyLocked(UPDATE_DISPLAY).getNow(null);
-        // Results should only have the display energy consumer
-        assertEquals(1, displayResults.length);
-        assertEquals(displayId, displayResults[0].id);
+        // Results should only have the cpu cluster energy consumers
+        final int[] receivedDisplayIds = new int[displayResults.length];
+        for (int i = 0; i < displayResults.length; i++) {
+            receivedDisplayIds[i] = displayResults[i].id;
+        }
+        Arrays.sort(receivedDisplayIds);
+        assertArrayEquals(displayIds, receivedDisplayIds);
 
         final EnergyConsumerResult[] wifiResults =
                 mBatteryExternalStatsWorker.getMeasuredEnergyLocked(UPDATE_WIFI).getNow(null);
@@ -193,6 +202,7 @@
     public class TestBatteryStatsImpl extends BatteryStatsImpl {
         public TestBatteryStatsImpl(Context context) {
             mPowerProfile = new PowerProfile(context, true /* forTest */);
+            initTimersAndCounters();
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java b/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java
index 8c87506..a0cbcad 100644
--- a/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.am;
 
-import static com.android.server.am.MeasuredEnergySnapshot.UNAVAILABLE;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -120,7 +118,7 @@
         // results0
         MeasuredEnergyDeltaData delta = snapshot.updateAndGetDelta(RESULTS_0, VOLTAGE_0);
         if (delta != null) { // null is fine here. If non-null, it better be uninteresting though.
-            assertEquals(UNAVAILABLE, delta.displayChargeUC);
+            assertNull(delta.displayChargeUC);
             assertNull(delta.otherTotalChargeUC);
             assertNull(delta.otherUidChargesUC);
         }
@@ -130,7 +128,7 @@
         assertNotNull(delta);
         long expectedChargeUC;
         expectedChargeUC = calculateChargeConsumedUC(14_000, VOLTAGE_0, 24_000, VOLTAGE_1);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
 
         assertNotNull(delta.otherTotalChargeUC);
 
@@ -149,14 +147,14 @@
         delta = snapshot.updateAndGetDelta(RESULTS_2, VOLTAGE_2);
         assertNotNull(delta);
         expectedChargeUC = calculateChargeConsumedUC(24_000, VOLTAGE_1, 36_000, VOLTAGE_2);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
         assertNull(delta.otherUidChargesUC);
         assertNull(delta.otherTotalChargeUC);
 
         // results3
         delta = snapshot.updateAndGetDelta(RESULTS_3, VOLTAGE_3);
         assertNotNull(delta);
-        assertEquals(UNAVAILABLE, delta.displayChargeUC);
+        assertNull(delta.displayChargeUC);
 
         assertNotNull(delta.otherTotalChargeUC);
 
@@ -183,7 +181,7 @@
         delta = snapshot.updateAndGetDelta(RESULTS_4, VOLTAGE_4);
         assertNotNull(delta);
         expectedChargeUC = calculateChargeConsumedUC(36_000, VOLTAGE_2, 43_000, VOLTAGE_4);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
 
         assertNotNull(delta.otherTotalChargeUC);
         expectedChargeUC = calculateChargeConsumedUC(190_000, VOLTAGE_3, 290_000, VOLTAGE_4);
@@ -210,7 +208,7 @@
         // results0
         MeasuredEnergyDeltaData delta = snapshot.updateAndGetDelta(RESULTS_0, VOLTAGE_0);
         if (delta != null) { // null is fine here. If non-null, it better be uninteresting though.
-            assertEquals(UNAVAILABLE, delta.displayChargeUC);
+            assertNull(delta.displayChargeUC);
             assertNull(delta.otherTotalChargeUC);
             assertNull(delta.otherUidChargesUC);
         }
@@ -220,7 +218,7 @@
         assertNotNull(delta);
         final long expectedChargeUC =
                 calculateChargeConsumedUC(14_000, VOLTAGE_0, 24_000, VOLTAGE_1);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
         assertNull(delta.otherTotalChargeUC); // Although in the results, they're not in the idMap
         assertNull(delta.otherUidChargesUC);
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
new file mode 100644
index 0000000..dc39b6d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlayController;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Presubmit
+@SmallTest
+public class SensorOverlaysTest {
+
+    private static final int SENSOR_ID = 11;
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock private IUdfpsOverlayController mUdfpsOverlayController;
+    @Mock private ISidefpsController mSidefpsController;
+    @Mock private AcquisitionClient<?> mAcquisitionClient;
+
+    @Test
+    public void noopWhenBothNull() {
+        final SensorOverlays useless = new SensorOverlays(null, null);
+        useless.show(SENSOR_ID, 2, null);
+        useless.hide(SENSOR_ID);
+    }
+
+    @Test
+    public void testProvidesUdfps() {
+        final List<IUdfpsOverlayController> udfps = new ArrayList<>();
+        SensorOverlays sensorOverlays = new SensorOverlays(null, mSidefpsController);
+
+        sensorOverlays.ifUdfps(udfps::add);
+        assertThat(udfps).isEmpty();
+
+        sensorOverlays = new SensorOverlays(mUdfpsOverlayController, mSidefpsController);
+        sensorOverlays.ifUdfps(udfps::add);
+        assertThat(udfps).containsExactly(mUdfpsOverlayController);
+    }
+
+    @Test
+    public void testShow() throws Exception {
+        testShow(mUdfpsOverlayController, mSidefpsController);
+    }
+
+    @Test
+    public void testShowUdfps() throws Exception {
+        testShow(mUdfpsOverlayController, null);
+    }
+
+    @Test
+    public void testShowSidefps() throws Exception {
+        testShow(null, mSidefpsController);
+    }
+
+    private void testShow(IUdfpsOverlayController udfps, ISidefpsController sidefps)
+            throws Exception {
+        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps);
+        final int reason = BiometricOverlayConstants.REASON_UNKNOWN;
+        sensorOverlays.show(SENSOR_ID, reason, mAcquisitionClient);
+
+        if (udfps != null) {
+            verify(mUdfpsOverlayController).showUdfpsOverlay(eq(SENSOR_ID), eq(reason), any());
+        }
+        if (sidefps != null) {
+            verify(mSidefpsController).show(eq(SENSOR_ID), eq(reason));
+        }
+    }
+
+    @Test
+    public void testHide() throws Exception {
+        testHide(mUdfpsOverlayController, mSidefpsController);
+    }
+
+    @Test
+    public void testHideUdfps() throws Exception {
+        testHide(mUdfpsOverlayController, null);
+    }
+
+    @Test
+    public void testHideSidefps() throws Exception {
+        testHide(null, mSidefpsController);
+    }
+
+    private void testHide(IUdfpsOverlayController udfps, ISidefpsController sidefps)
+            throws Exception {
+        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps);
+        sensorOverlays.hide(SENSOR_ID);
+
+        if (udfps != null) {
+            verify(mUdfpsOverlayController).hideUdfpsOverlay(eq(SENSOR_ID));
+        }
+        if (sidefps != null) {
+            verify(mSidefpsController).hide(eq(SENSOR_ID));
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
index b51918e..8b7c90d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
@@ -19,10 +19,13 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.hardware.biometrics.common.CommonProps;
 import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.SensorLocation;
@@ -56,6 +59,8 @@
     @Mock
     private Context mContext;
     @Mock
+    private Resources mResources;
+    @Mock
     private UserManager mUserManager;
     @Mock
     private GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
@@ -74,19 +79,21 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.obtainTypedArray(anyInt())).thenReturn(mock(TypedArray.class));
         when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
         when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>());
 
         final SensorProps sensor1 = new SensorProps();
         sensor1.commonProps = new CommonProps();
         sensor1.commonProps.sensorId = 0;
-        sensor1.sensorLocations = new SensorLocation[] {new SensorLocation()};
+        sensor1.sensorLocations = new SensorLocation[]{new SensorLocation()};
         final SensorProps sensor2 = new SensorProps();
         sensor2.commonProps = new CommonProps();
         sensor2.commonProps.sensorId = 1;
-        sensor2.sensorLocations = new SensorLocation[] {new SensorLocation()};
+        sensor2.sensorLocations = new SensorLocation[]{new SensorLocation()};
 
-        mSensorProps = new SensorProps[] {sensor1, sensor2};
+        mSensorProps = new SensorProps[]{sensor1, sensor2};
 
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index ff8fbda..03eba9b 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -18,6 +18,10 @@
 
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
@@ -33,8 +37,12 @@
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.FlakyTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.WindowProcessController;
+
 import junit.framework.Assert;
 
 import org.junit.Before;
@@ -55,10 +63,15 @@
 @Presubmit
 @RunWith(AndroidJUnit4.class)
 public final class DeviceStateManagerServiceTest {
-    private static final DeviceState DEFAULT_DEVICE_STATE = new DeviceState(0, "DEFAULT");
-    private static final DeviceState OTHER_DEVICE_STATE = new DeviceState(1, "OTHER");
+    private static final DeviceState DEFAULT_DEVICE_STATE =
+            new DeviceState(0, "DEFAULT", 0 /* flags */);
+    private static final DeviceState OTHER_DEVICE_STATE =
+            new DeviceState(1, "OTHER", 0 /* flags */);
     // A device state that is not reported as being supported for the default test provider.
-    private static final DeviceState UNSUPPORTED_DEVICE_STATE = new DeviceState(255, "UNSUPPORTED");
+    private static final DeviceState UNSUPPORTED_DEVICE_STATE =
+            new DeviceState(255, "UNSUPPORTED", 0 /* flags */);
+
+    private static final int FAKE_PROCESS_ID = 100;
 
     private TestDeviceStatePolicy mPolicy;
     private TestDeviceStateProvider mProvider;
@@ -69,6 +82,25 @@
         mProvider = new TestDeviceStateProvider();
         mPolicy = new TestDeviceStatePolicy(mProvider);
         mService = new DeviceStateManagerService(InstrumentationRegistry.getContext(), mPolicy);
+
+        // Necessary to allow us to check for top app process id in tests
+        mService.mActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class);
+        WindowProcessController windowProcessController = mock(WindowProcessController.class);
+        when(mService.mActivityTaskManagerInternal.getTopApp())
+                .thenReturn(windowProcessController);
+        when(windowProcessController.getPid()).thenReturn(FAKE_PROCESS_ID);
+
+        flushHandler(); // Flush the handler to ensure the initial values are committed.
+    }
+
+    private void flushHandler() {
+        flushHandler(1);
+    }
+
+    private void flushHandler(int count) {
+        for (int i = 0; i < count; i++) {
+            mService.getHandler().runWithScissors(() -> {}, 0);
+        }
     }
 
     @Test
@@ -80,6 +112,7 @@
                 DEFAULT_DEVICE_STATE.getIdentifier());
 
         mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+        flushHandler();
         assertEquals(mService.getCommittedState(), Optional.of(OTHER_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.empty());
         assertEquals(mService.getBaseState(), Optional.of(OTHER_DEVICE_STATE));
@@ -92,6 +125,7 @@
         mPolicy.blockConfigure();
 
         mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+        flushHandler();
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.of(OTHER_DEVICE_STATE));
         assertEquals(mService.getBaseState(), Optional.of(OTHER_DEVICE_STATE));
@@ -99,6 +133,7 @@
                 OTHER_DEVICE_STATE.getIdentifier());
 
         mProvider.setState(DEFAULT_DEVICE_STATE.getIdentifier());
+        flushHandler();
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.of(OTHER_DEVICE_STATE));
         assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
@@ -106,6 +141,7 @@
                 OTHER_DEVICE_STATE.getIdentifier());
 
         mPolicy.resumeConfigure();
+        flushHandler();
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.empty());
         assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
@@ -147,17 +183,21 @@
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.empty());
         assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
+        assertThat(mService.getSupportedStates()).asList().containsExactly(DEFAULT_DEVICE_STATE,
+                OTHER_DEVICE_STATE);
 
-        mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE });
+        mProvider.notifySupportedDeviceStates(new DeviceState[]{DEFAULT_DEVICE_STATE});
+        flushHandler();
 
         // The current committed and requests states do not change because the current state remains
         // supported.
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.empty());
         assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
+        assertThat(mService.getSupportedStates()).asList().containsExactly(DEFAULT_DEVICE_STATE);
 
         assertArrayEquals(callback.getLastNotifiedInfo().supportedStates,
-                new int[] { DEFAULT_DEVICE_STATE.getIdentifier() });
+                new int[]{DEFAULT_DEVICE_STATE.getIdentifier()});
     }
 
     @Test
@@ -166,20 +206,26 @@
         mService.getBinderService().registerCallback(callback);
 
         // An initial callback will be triggered on registration, so we clear it here.
+        flushHandler();
         callback.clearLastNotifiedInfo();
 
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.empty());
         assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
+        assertThat(mService.getSupportedStates()).asList().containsExactly(DEFAULT_DEVICE_STATE,
+                OTHER_DEVICE_STATE);
 
-        mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE,
-                OTHER_DEVICE_STATE });
+        mProvider.notifySupportedDeviceStates(new DeviceState[]{DEFAULT_DEVICE_STATE,
+                OTHER_DEVICE_STATE});
+        flushHandler();
 
         // The current committed and requests states do not change because the current state remains
         // supported.
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.empty());
         assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
+        assertThat(mService.getSupportedStates()).asList().containsExactly(DEFAULT_DEVICE_STATE,
+                OTHER_DEVICE_STATE);
 
         // The callback wasn't notified about a change in supported states as the states have not
         // changed.
@@ -203,12 +249,14 @@
         mService.getBinderService().registerCallback(callback);
 
         mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+        flushHandler();
         assertEquals(callback.getLastNotifiedInfo().baseState,
                 OTHER_DEVICE_STATE.getIdentifier());
         assertEquals(callback.getLastNotifiedInfo().currentState,
                 OTHER_DEVICE_STATE.getIdentifier());
 
         mProvider.setState(DEFAULT_DEVICE_STATE.getIdentifier());
+        flushHandler();
         assertEquals(callback.getLastNotifiedInfo().baseState,
                 DEFAULT_DEVICE_STATE.getIdentifier());
         assertEquals(callback.getLastNotifiedInfo().currentState,
@@ -216,6 +264,7 @@
 
         mPolicy.blockConfigure();
         mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+        flushHandler();
         // The callback should not have been notified of the state change as the policy is still
         // pending callback.
         assertEquals(callback.getLastNotifiedInfo().baseState,
@@ -224,6 +273,7 @@
                 DEFAULT_DEVICE_STATE.getIdentifier());
 
         mPolicy.resumeConfigure();
+        flushHandler();
         // Now that the policy is finished processing the callback should be notified of the state
         // change.
         assertEquals(callback.getLastNotifiedInfo().baseState,
@@ -236,6 +286,7 @@
     public void registerCallback_emitsInitialValue() throws RemoteException {
         TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
+        flushHandler();
         assertNotNull(callback.getLastNotifiedInfo());
         assertEquals(callback.getLastNotifiedInfo().baseState,
                 DEFAULT_DEVICE_STATE.getIdentifier());
@@ -247,6 +298,7 @@
     public void requestState() throws RemoteException {
         TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
+        flushHandler();
 
         final IBinder token = new Binder();
         assertEquals(callback.getLastNotifiedStatus(token),
@@ -254,6 +306,10 @@
 
         mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(),
                 0 /* flags */);
+        // Flush the handler twice. The first flush ensures the request is added and the policy is
+        // notified, while the second flush ensures the callback is notified once the change is
+        // committed.
+        flushHandler(2 /* count */);
 
         assertEquals(callback.getLastNotifiedStatus(token),
                 TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -271,6 +327,7 @@
                 OTHER_DEVICE_STATE.getIdentifier());
 
         mService.getBinderService().cancelRequest(token);
+        flushHandler();
 
         assertEquals(callback.getLastNotifiedStatus(token),
                 TestDeviceStateManagerCallback.STATUS_CANCELED);
@@ -287,10 +344,12 @@
                 DEFAULT_DEVICE_STATE.getIdentifier());
     }
 
+    @FlakyTest(bugId = 200332057)
     @Test
     public void requestState_pendingStateAtRequest() throws RemoteException {
         TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
+        flushHandler();
 
         mPolicy.blockConfigure();
 
@@ -303,6 +362,10 @@
 
         mService.getBinderService().requestState(firstRequestToken,
                 OTHER_DEVICE_STATE.getIdentifier(), 0 /* flags */);
+        // Flush the handler twice. The first flush ensures the request is added and the policy is
+        // notified, while the second flush ensures the callback is notified once the change is
+        // committed.
+        flushHandler(2 /* count */);
 
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.of(OTHER_DEVICE_STATE));
@@ -312,8 +375,8 @@
 
         mService.getBinderService().requestState(secondRequestToken,
                 DEFAULT_DEVICE_STATE.getIdentifier(), 0 /* flags */);
-
         mPolicy.resumeConfigureOnce();
+        flushHandler();
 
         // First request status is now suspended as there is another pending request.
         assertEquals(callback.getLastNotifiedStatus(firstRequestToken),
@@ -330,6 +393,7 @@
                 DEFAULT_DEVICE_STATE.getIdentifier());
 
         mPolicy.resumeConfigure();
+        flushHandler();
 
         assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
         assertEquals(mService.getPendingState(), Optional.empty());
@@ -339,6 +403,7 @@
 
         // Now cancel the second request to make the first request active.
         mService.getBinderService().cancelRequest(secondRequestToken);
+        flushHandler();
 
         assertEquals(callback.getLastNotifiedStatus(firstRequestToken),
                 TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -356,6 +421,7 @@
     public void requestState_sameAsBaseState() throws RemoteException {
         TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
+        flushHandler();
 
         final IBinder token = new Binder();
         assertEquals(callback.getLastNotifiedStatus(token),
@@ -363,6 +429,7 @@
 
         mService.getBinderService().requestState(token, DEFAULT_DEVICE_STATE.getIdentifier(),
                 0 /* flags */);
+        flushHandler();
 
         assertEquals(callback.getLastNotifiedStatus(token),
                 TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -372,6 +439,7 @@
     public void requestState_flagCancelWhenBaseChanges() throws RemoteException {
         TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
+        flushHandler();
 
         final IBinder token = new Binder();
         assertEquals(callback.getLastNotifiedStatus(token),
@@ -379,6 +447,10 @@
 
         mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(),
                 DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES);
+        // Flush the handler twice. The first flush ensures the request is added and the policy is
+        // notified, while the second flush ensures the callback is notified once the change is
+        // committed.
+        flushHandler(2 /* count */);
 
         assertEquals(callback.getLastNotifiedStatus(token),
                 TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -391,6 +463,7 @@
                 OTHER_DEVICE_STATE.getIdentifier());
 
         mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+        flushHandler();
 
         // Request is canceled because the base state changed.
         assertEquals(callback.getLastNotifiedStatus(token),
@@ -403,10 +476,12 @@
                 OTHER_DEVICE_STATE.getIdentifier());
     }
 
+    @FlakyTest(bugId = 200332057)
     @Test
     public void requestState_becomesUnsupported() throws RemoteException {
         TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
+        flushHandler();
 
         final IBinder token = new Binder();
         assertEquals(callback.getLastNotifiedStatus(token),
@@ -414,6 +489,7 @@
 
         mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(),
                 0 /* flags */);
+        flushHandler();
 
         assertEquals(callback.getLastNotifiedStatus(token),
                 TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -425,6 +501,7 @@
                 OTHER_DEVICE_STATE.getIdentifier());
 
         mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE });
+        flushHandler();
 
         // Request is canceled because the state is no longer supported.
         assertEquals(callback.getLastNotifiedStatus(token),
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java
index b5c8053..e286cb2 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java
@@ -41,24 +41,26 @@
     @Test
     public void testConstruct() {
         final DeviceState state = new DeviceState(MINIMUM_DEVICE_STATE /* identifier */,
-                "CLOSED" /* name */);
+                "CLOSED" /* name */, DeviceState.FLAG_CANCEL_STICKY_REQUESTS /* flags */);
         assertEquals(state.getIdentifier(), MINIMUM_DEVICE_STATE);
         assertEquals(state.getName(), "CLOSED");
+        assertEquals(state.getFlags(), DeviceState.FLAG_CANCEL_STICKY_REQUESTS);
     }
 
     @Test
     public void testConstruct_nullName() {
         final DeviceState state = new DeviceState(MAXIMUM_DEVICE_STATE /* identifier */,
-                null /* name */);
+                null /* name */, 0/* flags */);
         assertEquals(state.getIdentifier(), MAXIMUM_DEVICE_STATE);
         assertNull(state.getName());
+        assertEquals(state.getFlags(), 0);
     }
 
     @Test
     public void testConstruct_tooLargeIdentifier() {
         assertThrows(IllegalArgumentException.class, () -> {
             final DeviceState state = new DeviceState(MAXIMUM_DEVICE_STATE + 1 /* identifier */,
-                    null /* name */);
+                    null /* name */, 0 /* flags */);
         });
     }
 
@@ -66,7 +68,7 @@
     public void testConstruct_tooSmallIdentifier() {
         assertThrows(IllegalArgumentException.class, () -> {
             final DeviceState state = new DeviceState(MINIMUM_DEVICE_STATE - 1 /* identifier */,
-                    null /* name */);
+                    null /* name */, 0 /* flags */);
         });
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
new file mode 100644
index 0000000..c9cf2f0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicestate;
+
+import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_SUSPENDED;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+
+import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.os.Binder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link OverrideRequestController}.
+ * <p/>
+ * Run with <code>atest OverrideRequestControllerTest</code>.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public final class OverrideRequestControllerTest {
+    private TestStatusChangeListener mStatusListener;
+    private OverrideRequestController mController;
+
+    @Before
+    public void setup() {
+        mStatusListener = new TestStatusChangeListener();
+        mController = new OverrideRequestController(mStatusListener);
+    }
+
+    @Test
+    public void addRequest() {
+        OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        assertNull(mStatusListener.getLastStatus(request));
+
+        mController.addRequest(request);
+        assertEquals(mStatusListener.getLastStatus(request).intValue(), STATUS_ACTIVE);
+    }
+
+    @Test
+    public void addRequest_suspendExistingRequest() {
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        assertNull(mStatusListener.getLastStatus(firstRequest));
+
+        mController.addRequest(firstRequest);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        assertNull(mStatusListener.getLastStatus(secondRequest));
+
+        mController.addRequest(secondRequest);
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+    }
+
+    @Test
+    public void addRequest_cancelActiveRequest() {
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+
+        mController.addRequest(firstRequest);
+        mController.addRequest(secondRequest);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+        mController.cancelRequest(secondRequest.getToken());
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+    }
+
+    @Test
+    public void addRequest_cancelSuspendedRequest() {
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+
+        mController.addRequest(firstRequest);
+        mController.addRequest(secondRequest);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+        mController.cancelRequest(firstRequest.getToken());
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
+    }
+
+    @Test
+    public void handleBaseStateChanged() {
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */,
+                DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES /* flags */);
+
+        mController.addRequest(firstRequest);
+        mController.addRequest(secondRequest);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+        mController.handleBaseStateChanged();
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+    }
+
+    @Test
+    public void handleProcessDied() {
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 1 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+
+        mController.addRequest(firstRequest);
+        mController.addRequest(secondRequest);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+        mController.handleProcessDied(1);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+
+        mController.handleProcessDied(0);
+
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
+    }
+
+    @Test
+    public void handleProcessDied_stickyRequests() {
+        mController.setStickyRequestsAllowed(true);
+
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 1 /* pid */,
+                0 /* requestedState */, 0 /* flags */);
+
+        mController.addRequest(firstRequest);
+        mController.addRequest(secondRequest);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+        mController.handleProcessDied(1);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+        mController.cancelStickyRequests();
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+    }
+
+    @Test
+    public void handleNewSupportedStates() {
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                1 /* requestedState */, 0 /* flags */);
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                2 /* requestedState */, 0 /* flags */);
+
+        mController.addRequest(firstRequest);
+        mController.addRequest(secondRequest);
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+        mController.handleNewSupportedStates(new int[]{ 0, 1 });
+
+        assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+
+        mController.handleNewSupportedStates(new int[]{ 0 });
+
+        assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
+    }
+
+    private static final class TestStatusChangeListener implements
+            OverrideRequestController.StatusChangeListener {
+        private Map<OverrideRequest, Integer> mLastStatusMap = new HashMap<>();
+
+        @Override
+        public void onStatusChanged(@NonNull OverrideRequest request, int newStatus) {
+            mLastStatusMap.put(request, newStatus);
+        }
+
+        @Nullable
+        public Integer getLastStatus(OverrideRequest request) {
+            return mLastStatusMap.get(request);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 5ba375b..7c55716 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -885,6 +885,61 @@
         assertFalse(callback.mDisplayAddedCalled);
     }
 
+
+
+    @Test
+    public void testSettingTwoBrightnessConfigurationsOnMultiDisplay() {
+        Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+
+        // get the first two internal displays
+        Display[] displays = displayManager.getDisplays();
+        Display internalDisplayOne = null;
+        Display internalDisplayTwo = null;
+        for (Display display : displays) {
+            if (display.getType() == Display.TYPE_INTERNAL) {
+                if (internalDisplayOne == null) {
+                    internalDisplayOne = display;
+                } else {
+                    internalDisplayTwo = display;
+                    break;
+                }
+            }
+        }
+
+        // return if there are fewer than 2 displays on this device
+        if (internalDisplayOne == null || internalDisplayTwo == null) {
+            return;
+        }
+
+        final String uniqueDisplayIdOne = internalDisplayOne.getUniqueId();
+        final String uniqueDisplayIdTwo = internalDisplayTwo.getUniqueId();
+
+        BrightnessConfiguration configOne =
+                new BrightnessConfiguration.Builder(
+                        new float[]{0.0f, 12345.0f}, new float[]{15.0f, 400.0f})
+                        .setDescription("model:1").build();
+        BrightnessConfiguration configTwo =
+                new BrightnessConfiguration.Builder(
+                        new float[]{0.0f, 6789.0f}, new float[]{12.0f, 300.0f})
+                        .setDescription("model:2").build();
+
+        displayManager.setBrightnessConfigurationForDisplay(configOne,
+                uniqueDisplayIdOne);
+        displayManager.setBrightnessConfigurationForDisplay(configTwo,
+                uniqueDisplayIdTwo);
+
+        BrightnessConfiguration configFromOne =
+                displayManager.getBrightnessConfigurationForDisplay(uniqueDisplayIdOne);
+        BrightnessConfiguration configFromTwo =
+                displayManager.getBrightnessConfigurationForDisplay(uniqueDisplayIdTwo);
+
+        assertNotNull(configFromOne);
+        assertEquals(configOne, configFromOne);
+        assertEquals(configTwo, configFromTwo);
+
+    }
+
     private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled)
             throws Exception {
         DisplayManagerService displayManager =
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 8279624..fbc1952 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -16,12 +16,17 @@
 
 package com.android.server.display;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.DEFAULT_DISPLAY_GROUP;
+
 import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED;
 import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED;
 import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.mockito.ArgumentMatchers.eq;
@@ -55,6 +60,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Arrays;
+import java.util.Set;
 
 @SmallTest
 @Presubmit
@@ -123,14 +129,14 @@
         // add
         LogicalDisplay displayAdded = add(device);
         assertEquals(info(displayAdded).address, info(device).address);
-        assertEquals(Display.DEFAULT_DISPLAY, id(displayAdded));
+        assertEquals(DEFAULT_DISPLAY, id(displayAdded));
 
         // remove
         mDisplayDeviceRepo.onDisplayDeviceEvent(device, DISPLAY_DEVICE_EVENT_REMOVED);
         verify(mListenerMock).onLogicalDisplayEventLocked(
                 mDisplayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_REMOVED));
         LogicalDisplay displayRemoved = mDisplayCaptor.getValue();
-        assertEquals(Display.DEFAULT_DISPLAY, id(displayRemoved));
+        assertEquals(DEFAULT_DISPLAY, id(displayRemoved));
         assertEquals(displayAdded, displayRemoved);
     }
 
@@ -155,11 +161,11 @@
 
         LogicalDisplay display1 = add(device1);
         assertEquals(info(display1).address, info(device1).address);
-        assertNotEquals(Display.DEFAULT_DISPLAY, id(display1));
+        assertNotEquals(DEFAULT_DISPLAY, id(display1));
 
         LogicalDisplay display2 = add(device2);
         assertEquals(info(display2).address, info(device2).address);
-        assertEquals(Display.DEFAULT_DISPLAY, id(display2));
+        assertEquals(DEFAULT_DISPLAY, id(display2));
     }
 
     @Test
@@ -171,12 +177,12 @@
 
         LogicalDisplay display1 = add(device1);
         assertEquals(info(display1).address, info(device1).address);
-        assertEquals(Display.DEFAULT_DISPLAY, id(display1));
+        assertEquals(DEFAULT_DISPLAY, id(display1));
 
         LogicalDisplay display2 = add(device2);
         assertEquals(info(display2).address, info(device2).address);
         // Despite the flags, we can only have one default display
-        assertNotEquals(Display.DEFAULT_DISPLAY, id(display2));
+        assertNotEquals(DEFAULT_DISPLAY, id(display2));
     }
 
     @Test
@@ -189,7 +195,67 @@
         int [] ids = mLogicalDisplayMapper.getDisplayIdsLocked(Process.SYSTEM_UID);
         assertEquals(3, ids.length);
         Arrays.sort(ids);
-        assertEquals(Display.DEFAULT_DISPLAY, ids[0]);
+        assertEquals(DEFAULT_DISPLAY, ids[0]);
+    }
+
+    @Test
+    public void testGetDisplayInfoForStateLocked_oneDisplayGroup_internalType() {
+        add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+        add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+        add(createDisplayDevice(Display.TYPE_INTERNAL, 700, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+
+        Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP);
+        assertThat(displayInfos.size()).isEqualTo(3);
+        for (DisplayInfo displayInfo : displayInfos) {
+            assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY);
+            assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP);
+            assertThat(displayInfo.logicalWidth).isAnyOf(600, 200, 700);
+            assertThat(displayInfo.logicalHeight).isEqualTo(800);
+        }
+    }
+
+    @Test
+    public void testGetDisplayInfoForStateLocked_oneDisplayGroup_differentTypes() {
+        add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+        add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+        add(createDisplayDevice(Display.TYPE_EXTERNAL, 700, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+
+        Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP);
+        assertThat(displayInfos.size()).isEqualTo(2);
+        for (DisplayInfo displayInfo : displayInfos) {
+            assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY);
+            assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP);
+            assertThat(displayInfo.logicalWidth).isAnyOf(600, 200);
+            assertThat(displayInfo.logicalHeight).isEqualTo(800);
+        }
+    }
+
+    @Test
+    public void testGetDisplayInfoForStateLocked_multipleDisplayGroups_defaultGroup() {
+        add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+        add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800,
+                DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY));
+        add(createDisplayDevice(Display.TYPE_VIRTUAL, 700, 800,
+                DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP));
+
+        Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP);
+        assertThat(displayInfos.size()).isEqualTo(2);
+        for (DisplayInfo displayInfo : displayInfos) {
+            assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY);
+            assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP);
+            assertThat(displayInfo.logicalWidth).isAnyOf(600, 200);
+            assertThat(displayInfo.logicalHeight).isEqualTo(800);
+        }
     }
 
     @Test
@@ -199,11 +265,11 @@
         LogicalDisplay display2 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, 0));
         LogicalDisplay display3 = add(createDisplayDevice(Display.TYPE_VIRTUAL, 600, 800, 0));
 
-        assertEquals(Display.DEFAULT_DISPLAY_GROUP,
+        assertEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display1)));
-        assertEquals(Display.DEFAULT_DISPLAY_GROUP,
+        assertEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display2)));
-        assertEquals(Display.DEFAULT_DISPLAY_GROUP,
+        assertEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display3)));
     }
 
@@ -218,11 +284,11 @@
                 DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
         LogicalDisplay display3 = add(device3);
 
-        assertEquals(Display.DEFAULT_DISPLAY_GROUP,
+        assertEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display1)));
-        assertEquals(Display.DEFAULT_DISPLAY_GROUP,
+        assertEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display2)));
-        assertNotEquals(Display.DEFAULT_DISPLAY_GROUP,
+        assertNotEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display3)));
 
         // Now switch it back to the default group by removing the flag and issuing an update
@@ -231,7 +297,7 @@
         mDisplayDeviceRepo.onDisplayDeviceEvent(device3, DISPLAY_DEVICE_EVENT_CHANGED);
 
         // Verify the new group is correct.
-        assertEquals(Display.DEFAULT_DISPLAY_GROUP,
+        assertEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display3)));
     }
 
@@ -287,14 +353,14 @@
         // add
         LogicalDisplay displayAdded = add(device);
         assertEquals(info(displayAdded).address, info(device).address);
-        assertNotEquals(Display.DEFAULT_DISPLAY, id(displayAdded));
+        assertNotEquals(DEFAULT_DISPLAY, id(displayAdded));
 
         // remove
         mDisplayDeviceRepo.onDisplayDeviceEvent(device, DISPLAY_DEVICE_EVENT_REMOVED);
         verify(mListenerMock).onLogicalDisplayEventLocked(
                 mDisplayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_REMOVED));
         LogicalDisplay displayRemoved = mDisplayCaptor.getValue();
-        assertNotEquals(Display.DEFAULT_DISPLAY, id(displayRemoved));
+        assertNotEquals(DEFAULT_DISPLAY, id(displayRemoved));
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
index 196454b..57a9cb2 100644
--- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -17,13 +17,16 @@
 package com.android.server.display;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import android.content.Context;
 import android.hardware.display.BrightnessConfiguration;
 import android.util.Pair;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -144,15 +147,93 @@
     }
 
     @Test
-    public void testStoreAndReloadOfBrightnessConfigurations() {
+    public void testStoreAndReloadOfDisplayBrightnessConfigurations() {
+        final String uniqueDisplayId = "test:123";
+        int userSerial = 0;
+        String packageName = "pdsTestPackage";
         final float[] lux = { 0f, 10f };
         final float[] nits = {1f, 100f };
         final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
                 .setDescription("a description")
                 .build();
         mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial));
+
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        mDataStore.setBrightnessConfigurationForDisplayLocked(config, testDisplayDevice, userSerial,
+                packageName);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertNotNull(newDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial));
+        assertEquals(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial), newDataStore.getBrightnessConfigurationForDisplayLocked(
+                        uniqueDisplayId, userSerial));
+    }
+
+    @Test
+    public void testSetBrightnessConfigurationFailsWithUnstableId() {
+        final String uniqueDisplayId = "test:123";
+        int userSerial = 0;
+        String packageName = "pdsTestPackage";
+        final float[] lux = { 0f, 10f };
+        final float[] nits = {1f, 100f };
+        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .setDescription("a description")
+                .build();
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial));
+
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return false;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        assertFalse(mDataStore.setBrightnessConfigurationForDisplayLocked(
+                config, testDisplayDevice, userSerial, packageName));
+    }
+
+    @Test
+    public void testStoreAndReloadOfBrightnessConfigurations() {
+        final float[] lux = { 0f, 10f };
+        final float[] nits = {1f, 100f };
+        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .setDescription("a description")
+                .build();
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        String packageName = context.getPackageName();
+
+        mDataStore.loadIfNeeded();
         assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
-        mDataStore.setBrightnessConfigurationForUser(config, 0, "packagename");
+        mDataStore.setBrightnessConfigurationForUser(config, 0, packageName);
 
         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
         mInjector.setWriteStream(baos);
@@ -173,17 +254,18 @@
     public void testNullBrightnessConfiguration() {
         final float[] lux = { 0f, 10f };
         final float[] nits = {1f, 100f };
+        int userSerial = 0;
         final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
                 .setDescription("a description")
                 .build();
         mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+        assertNull(mDataStore.getBrightnessConfiguration(userSerial));
 
-        mDataStore.setBrightnessConfigurationForUser(config, 0, "packagename");
-        assertNotNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+        mDataStore.setBrightnessConfigurationForUser(config, userSerial, "packagename");
+        assertNotNull(mDataStore.getBrightnessConfiguration(userSerial));
 
-        mDataStore.setBrightnessConfigurationForUser(null, 0, "packagename");
-        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+        mDataStore.setBrightnessConfigurationForUser(null, userSerial, "packagename");
+        assertNull(mDataStore.getBrightnessConfiguration(userSerial));
     }
 
     public class TestInjector extends PersistentDataStore.Injector {
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 4d2d2f1..8e2c1f0 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -150,8 +150,9 @@
         provider.setListener(listener);
 
         verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
-        final DeviceState[] expectedStates = new DeviceState[]{ new DeviceState(1, ""),
-                new DeviceState(2, "") };
+        final DeviceState[] expectedStates = new DeviceState[]{
+                new DeviceState(1, "", 0 /* flags */),
+                new DeviceState(2, "", 0 /* flags */) };
         assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
 
         verify(listener).onStateChanged(mIntegerCaptor.capture());
@@ -187,8 +188,9 @@
         provider.setListener(listener);
 
         verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
-        final DeviceState[] expectedStates = new DeviceState[]{ new DeviceState(1, ""),
-                new DeviceState(2, "CLOSED") };
+        final DeviceState[] expectedStates = new DeviceState[]{
+                new DeviceState(1, "", 0 /* flags */),
+                new DeviceState(2, "CLOSED", 0 /* flags */) };
         assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
 
         // onStateChanged() should not be called because the provider has not yet been notified of
@@ -264,8 +266,11 @@
 
         verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
         assertArrayEquals(
-                new DeviceState[]{ new DeviceState(1, "CLOSED"), new DeviceState(2, "HALF_OPENED"),
-                        new DeviceState(3, "OPENED") }, mDeviceStateArrayCaptor.getValue());
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */) },
+                mDeviceStateArrayCaptor.getValue());
         // onStateChanged() should not be called because the provider has not yet been notified of
         // the initial sensor state.
         verify(listener, never()).onStateChanged(mIntegerCaptor.capture());
@@ -350,8 +355,10 @@
 
         verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
         assertArrayEquals(
-                new DeviceState[]{ new DeviceState(1, "CLOSED"), new DeviceState(2, "HALF_OPENED"),
-                        }, mDeviceStateArrayCaptor.getValue());
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */)
+                }, mDeviceStateArrayCaptor.getValue());
         // onStateChanged() should be called because the provider could not find the sensor.
         verify(listener).onStateChanged(mIntegerCaptor.capture());
         assertEquals(1, mIntegerCaptor.getValue().intValue());
diff --git a/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java
index 6334e9d..11554c7 100644
--- a/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java
@@ -82,8 +82,6 @@
         MockitoAnnotations.initMocks(this);
         when(mUwbInjector.getVendorService()).thenReturn(mVendorService);
         when(mUwbInjector.checkUwbRangingPermissionForDataDelivery(any(), any())).thenReturn(true);
-        when(mUwbInjector.isPersistedUwbStateEnabled()).thenReturn(true);
-        when(mUwbInjector.isAirplaneModeOn()).thenReturn(false);
         when(mVendorService.asBinder()).thenReturn(mVendorServiceBinder);
         mUwbServiceImpl = new UwbServiceImpl(mContext, mUwbInjector);
     }
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 5a00e0d..62e0a19 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -998,6 +998,8 @@
             throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1);
         createSystemReadyService();
 
         IExternalVibrationController firstController = mock(IExternalVibrationController.class);
@@ -1006,8 +1008,11 @@
                 firstController);
         int firstScale = mExternalVibratorService.onExternalVibrationStart(firstVibration);
 
-        ExternalVibration secondVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ATTRS,
-                secondController);
+        AudioAttributes ringtoneAudioAttrs = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .build();
+        ExternalVibration secondVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                ringtoneAudioAttrs, secondController);
         int secondScale = mExternalVibratorService.onExternalVibrationStart(secondVibration);
 
         assertEquals(IExternalVibratorService.SCALE_NONE, firstScale);
@@ -1040,6 +1045,37 @@
         assertEquals(Arrays.asList(true), mVibratorProviders.get(1).getExternalControlStates());
     }
 
+    @Test
+    public void onExternalVibration_withRingtone_usesRingerModeSettings() {
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        mVibrator.setDefaultRingVibrationIntensity(Vibrator.VIBRATION_INTENSITY_MEDIUM);
+        AudioAttributes audioAttrs = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .build();
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, audioAttrs,
+                mock(IExternalVibrationController.class));
+
+        setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
+        setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
+        createSystemReadyService();
+        int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
+        setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 1);
+        createSystemReadyService();
+        scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertEquals(IExternalVibratorService.SCALE_NONE, scale);
+
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1);
+        setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
+        createSystemReadyService();
+        scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertEquals(IExternalVibratorService.SCALE_NONE, scale);
+    }
+
     private VibrationEffectSegment expectedPrebaked(int effectId) {
         return new PrebakedSegment(effectId, false, VibrationEffect.EFFECT_STRENGTH_MEDIUM);
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 4b3771b..f21991d 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -475,7 +475,7 @@
 
     @Test
     public void requestProjection_failsForBogusPackageName() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0))
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
                 .thenReturn(TestInjector.CALLING_UID + 1);
 
         assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder,
@@ -485,7 +485,7 @@
 
     @Test
     public void requestProjection_failsIfNameNotFound() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0))
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
                 .thenThrow(new PackageManager.NameNotFoundException());
 
         assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder,
@@ -495,7 +495,8 @@
 
     @Test
     public void requestProjection_failsIfNoProjectionTypes() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
 
         assertThrows(IllegalArgumentException.class,
                 () -> mService.requestProjection(mBinder, PROJECTION_TYPE_NONE, PACKAGE_NAME));
@@ -507,7 +508,8 @@
 
     @Test
     public void requestProjection_failsIfMultipleProjectionTypes() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
 
         // Don't use PROJECTION_TYPE_ALL because that's actually == -1 and will fail the > 0 check.
         int multipleProjectionTypes = PROJECTION_TYPE_AUTOMOTIVE | 0x0002 | 0x0004;
@@ -522,7 +524,8 @@
 
     @Test
     public void requestProjection_enforcesToggleAutomotiveProjectionPermission() throws Exception {
-        doThrow(new SecurityException()).when(mPackageManager).getPackageUid(PACKAGE_NAME, 0);
+        doThrow(new SecurityException())
+                .when(mPackageManager).getPackageUidAsUser(eq(PACKAGE_NAME), anyInt());
 
         assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder,
                 PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME));
@@ -531,12 +534,14 @@
 
     @Test
     public void requestProjection_automotive_failsIfAlreadySetByOtherPackage() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes());
 
         String otherPackage = "Raconteurs";
-        when(mPackageManager.getPackageUid(otherPackage, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(otherPackage), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         assertFalse(mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, otherPackage));
         assertThat(mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE),
                 contains(PACKAGE_NAME));
@@ -544,7 +549,8 @@
 
     @Test
     public void requestProjection_failsIfCannotLinkToDeath() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         doThrow(new RemoteException()).when(mBinder).linkToDeath(any(), anyInt());
 
         assertFalse(mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME));
@@ -553,7 +559,8 @@
 
     @Test
     public void requestProjection() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         // Should work for all powers of two.
         for (int i = 0; i < Integer.SIZE; ++i) {
             int projectionType = 1 << i;
@@ -568,11 +575,12 @@
 
     @Test
     public void releaseProjection_failsForBogusPackageName() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes());
 
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0))
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
                 .thenReturn(TestInjector.CALLING_UID + 1);
 
         assertThrows(SecurityException.class, () -> mService.releaseProjection(
@@ -582,10 +590,11 @@
 
     @Test
     public void releaseProjection_failsIfNameNotFound() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes());
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0))
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
                 .thenThrow(new PackageManager.NameNotFoundException());
 
         assertThrows(SecurityException.class, () -> mService.releaseProjection(
@@ -595,7 +604,8 @@
 
     @Test
     public void releaseProjection_enforcesToggleAutomotiveProjectionPermission() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes());
         doThrow(new SecurityException()).when(mContext).enforceCallingPermission(
@@ -613,7 +623,8 @@
 
     @Test
     public void releaseProjection() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         requestAllPossibleProjectionTypes();
         assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes());
 
@@ -632,7 +643,8 @@
 
     @Test
     public void binderDeath_releasesProjection() throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         requestAllPossibleProjectionTypes();
         assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes());
         ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor = ArgumentCaptor.forClass(
@@ -647,7 +659,8 @@
     @Test
     public void getActiveProjectionTypes() throws Exception {
         assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes());
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes());
         mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
@@ -657,7 +670,8 @@
     @Test
     public void getProjectingPackages() throws Exception {
         assertTrue(mService.getProjectingPackages(PROJECTION_TYPE_ALL).isEmpty());
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         assertEquals(1, mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE).size());
         assertEquals(1, mService.getProjectingPackages(PROJECTION_TYPE_ALL).size());
@@ -681,7 +695,8 @@
     @Test
     public void addOnProjectionStateChangedListener_callsListenerIfProjectionActive()
             throws Exception {
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes());
 
@@ -710,7 +725,8 @@
 
         mService.removeOnProjectionStateChangedListener(listener);
         // Now set automotive projection, should not call back.
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         verify(listener, never()).onProjectionStateChanged(anyInt(), any());
     }
@@ -726,7 +742,8 @@
         verifyNoMoreInteractions(listener);
 
         // Now set automotive projection, should call back.
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_AUTOMOTIVE),
                 eq(List.of(PACKAGE_NAME)));
@@ -752,8 +769,9 @@
         int fakeProjectionType = 0x0002;
         int otherFakeProjectionType = 0x0004;
         String otherPackageName = "Internet Arms";
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
-        when(mPackageManager.getPackageUid(otherPackageName, 0))
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(otherPackageName), anyInt()))
                 .thenReturn(TestInjector.CALLING_UID);
         IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class);
         when(listener.asBinder()).thenReturn(mBinder); // Any binder will do.
@@ -806,7 +824,8 @@
 
         // Now kill the binder for the listener. This should remove it from the list of listeners.
         listenerDeathRecipient.getValue().binderDied();
-        when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID);
+        when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()))
+                .thenReturn(TestInjector.CALLING_UID);
         mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME);
         verify(listener, never()).onProjectionStateChanged(anyInt(), any());
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
index 577e36c..a834e2b6 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
@@ -118,7 +118,7 @@
             assertActionsEqual(getSmartActions(key, i), ranking.getSmartActions());
             assertEquals(getSmartReplies(key, i), ranking.getSmartReplies());
             assertEquals(canBubble(i), ranking.canBubble());
-            assertEquals(visuallyInterruptive(i), ranking.visuallyInterruptive());
+            assertEquals(isTextChanged(i), ranking.isTextChanged());
             assertEquals(isConversation(i), ranking.isConversation());
             assertEquals(getShortcutInfo(i).getId(), ranking.getConversationShortcutInfo().getId());
             assertEquals(getRankingAdjustment(i), ranking.getRankingAdjustment());
@@ -189,7 +189,7 @@
                 (ArrayList) tweak.getSmartActions(),
                 (ArrayList) tweak.getSmartReplies(),
                 tweak.canBubble(),
-                tweak.visuallyInterruptive(),
+                tweak.isTextChanged(),
                 tweak.isConversation(),
                 tweak.getConversationShortcutInfo(),
                 tweak.getRankingAdjustment(),
@@ -270,7 +270,7 @@
                     getSmartActions(key, i),
                     getSmartReplies(key, i),
                     canBubble(i),
-                    visuallyInterruptive(i),
+                    isTextChanged(i),
                     isConversation(i),
                     getShortcutInfo(i),
                     getRankingAdjustment(i),
@@ -379,7 +379,7 @@
         return index % 4 == 0;
     }
 
-    private boolean visuallyInterruptive(int index) {
+    private boolean isTextChanged(int index) {
         return index % 4 == 0;
     }
 
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 1ae219d..c493639 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3872,6 +3872,27 @@
     }
 
     @Test
+    public void testVisuallyInterruptive_notSeen() throws Exception {
+        NotificationRecord original = generateNotificationRecord(mTestNotificationChannel);
+        mService.addNotification(original);
+
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, original.getSbn().getId(),
+                original.getSbn().getTag(), mUid, 0,
+                new Notification.Builder(mContext, mTestNotificationChannel.getId())
+                        .setContentTitle("new title").build(),
+                UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord update = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+        mService.addEnqueuedNotification(update);
+
+        NotificationManagerService.PostNotificationRunnable runnable =
+                mService.new PostNotificationRunnable(update.getKey());
+        runnable.run();
+        waitForIdle();
+
+        assertFalse(update.isInterruptive());
+    }
+
+    @Test
     public void testApplyAdjustmentMultiUser() throws Exception {
         final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         mService.addNotification(r);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 32a4774..0b91802 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -27,6 +27,7 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -44,6 +45,7 @@
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.util.ArrayMap;
+import android.window.WindowContainerToken;
 
 import androidx.test.filters.SmallTest;
 
@@ -69,6 +71,7 @@
 @Presubmit
 @RunWith(WindowTestRunner.class)
 public class ActivityMetricsLaunchObserverTests extends WindowTestsBase {
+    private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5);
     private ActivityMetricsLogger mActivityMetricsLogger;
     private ActivityMetricsLogger.LaunchingState mLaunchingState;
     private ActivityMetricsLaunchObserver mLaunchObserver;
@@ -136,7 +139,7 @@
         // messages that are waiting for the lock.
         waitHandlerIdle(mAtm.mH);
         // AMLO callbacks happen on a separate thread than AML calls, so we need to use a timeout.
-        return verify(mock, timeout(TimeUnit.SECONDS.toMillis(5)));
+        return verify(mock, timeout(TIMEOUT_MS));
     }
 
     private void verifyOnActivityLaunchFinished(ActivityRecord activity) {
@@ -257,15 +260,40 @@
 
     @Test
     public void testOnActivityLaunchWhileSleeping() {
-        notifyActivityLaunching(mTopActivity.intent);
-        notifyActivityLaunched(START_SUCCESS, mTopActivity);
-        doReturn(true).when(mTopActivity.mDisplayContent).isSleeping();
-        mTopActivity.setState(Task.ActivityState.RESUMED, "test");
-        mTopActivity.setVisibility(false);
+        notifyActivityLaunching(mTrampolineActivity.intent);
+        notifyActivityLaunched(START_SUCCESS, mTrampolineActivity);
+        doReturn(true).when(mTrampolineActivity.mDisplayContent).isSleeping();
+        mTrampolineActivity.setState(ActivityRecord.State.RESUMED, "test");
+        mTrampolineActivity.setVisibility(false);
         waitHandlerIdle(mAtm.mH);
         // Not cancel immediately because in one of real cases, the keyguard may be going away or
         // occluded later, then the activity can be drawn.
-        verify(mLaunchObserver, never()).onActivityLaunchCancelled(eqProto(mTopActivity));
+        verify(mLaunchObserver, never()).onActivityLaunchCancelled(eqProto(mTrampolineActivity));
+
+        clearInvocations(mLaunchObserver);
+        mLaunchTopByTrampoline = true;
+        mTopActivity.mVisibleRequested = false;
+        notifyActivityLaunching(mTopActivity.intent);
+        // It should schedule a message with UNKNOWN_VISIBILITY_CHECK_DELAY_MS to check whether
+        // the launch event is still valid.
+        notifyActivityLaunched(START_SUCCESS, mTopActivity);
+
+        // The posted message will acquire wm lock, so the test needs to release the lock to verify.
+        final Throwable error = awaitInWmLock(() -> {
+            try {
+                // Though the aborting target should be eqProto(mTopActivity), use any() to avoid
+                // any changes in proto that may cause failure by different arguments.
+                verify(mLaunchObserver, timeout(TIMEOUT_MS)).onActivityLaunchCancelled(any());
+            } catch (Throwable e) {
+                // Catch any errors including assertion because this runs in another thread.
+                return e;
+            }
+            return null;
+        });
+        // The launch event must be cancelled because the activity keeps invisible.
+        if (error != null) {
+            throw new AssertionError(error);
+        }
     }
 
     @Test
@@ -376,6 +404,7 @@
 
         // Another round without setting visibility of the trampoline activity.
         onActivityLaunchedTrampoline();
+        mTrampolineActivity.setState(ActivityRecord.State.PAUSING, "test");
         notifyWindowsDrawn(mTopActivity);
         // If the transition can start, the invisible activities should be discarded and the launch
         // event be reported successfully.
@@ -447,10 +476,22 @@
     }
 
     @Test
+    public void testConsecutiveLaunch() {
+        mTrampolineActivity.setState(ActivityRecord.State.INITIALIZING, "test");
+        onActivityLaunched(mTrampolineActivity);
+        mActivityMetricsLogger.notifyActivityLaunching(mTopActivity.intent,
+                mTrampolineActivity /* caller */, mTrampolineActivity.getUid());
+        notifyActivityLaunched(START_SUCCESS, mTopActivity);
+        transitToDrawnAndVerifyOnLaunchFinished(mTopActivity);
+    }
+
+    @Test
     public void testConsecutiveLaunchNewTask() {
         final IBinder launchCookie = mock(IBinder.class);
+        final WindowContainerToken launchRootTask = mock(WindowContainerToken.class);
         mTrampolineActivity.noDisplay = true;
         mTrampolineActivity.mLaunchCookie = launchCookie;
+        mTrampolineActivity.mLaunchRootTask = launchRootTask;
         onActivityLaunched(mTrampolineActivity);
         final ActivityRecord activityOnNewTask = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
@@ -464,6 +505,10 @@
                 mTrampolineActivity.mLaunchCookie).isNull();
         assertWithMessage("The last launch task has the transferred cookie").that(
                 activityOnNewTask.mLaunchCookie).isEqualTo(launchCookie);
+        assertWithMessage("Trampoline's launch root task must be transferred").that(
+                mTrampolineActivity.mLaunchRootTask).isNull();
+        assertWithMessage("The last launch task has the transferred launch root task").that(
+                activityOnNewTask.mLaunchRootTask).isEqualTo(launchRootTask);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
index b4fbf5f..184ea52 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java
@@ -110,18 +110,20 @@
             @Override
             public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
                 try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) {
-                    t.setVisibility(leash, true /* visible */).apply();
+                    t.show(leash).apply();
                 }
                 int cookieIndex = -1;
                 if (trampoline.equals(taskInfo.baseActivity)) {
                     cookieIndex = 0;
                 } else if (main.equals(taskInfo.baseActivity)) {
                     cookieIndex = 1;
-                    mainLatch.countDown();
                 }
                 if (cookieIndex >= 0) {
                     appearedCookies[cookieIndex] = taskInfo.launchCookies.isEmpty()
                             ? null : taskInfo.launchCookies.get(0);
+                    if (cookieIndex == 1) {
+                        mainLatch.countDown();
+                    }
                 }
             }
         };
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 6f04f17..b093974 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -18,7 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.FLAG_SUPPORTS_PICTURE_IN_PICTURE;
@@ -67,22 +67,22 @@
 import static com.android.server.wm.ActivityRecord.FINISH_RESULT_CANCELLED;
 import static com.android.server.wm.ActivityRecord.FINISH_RESULT_REMOVED;
 import static com.android.server.wm.ActivityRecord.FINISH_RESULT_REQUESTED;
-import static com.android.server.wm.Task.ActivityState.DESTROYED;
-import static com.android.server.wm.Task.ActivityState.DESTROYING;
-import static com.android.server.wm.Task.ActivityState.FINISHING;
-import static com.android.server.wm.Task.ActivityState.INITIALIZING;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
-import static com.android.server.wm.Task.ActivityState.STARTED;
-import static com.android.server.wm.Task.ActivityState.STOPPED;
-import static com.android.server.wm.Task.ActivityState.STOPPING;
-import static com.android.server.wm.Task.TASK_VISIBILITY_INVISIBLE;
-import static com.android.server.wm.Task.TASK_VISIBILITY_VISIBLE;
-import static com.android.server.wm.Task.TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
+import static com.android.server.wm.ActivityRecord.State.DESTROYED;
+import static com.android.server.wm.ActivityRecord.State.DESTROYING;
+import static com.android.server.wm.ActivityRecord.State.FINISHING;
+import static com.android.server.wm.ActivityRecord.State.INITIALIZING;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STARTED;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_AFTER_ANIM;
-import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_BEFORE_ANIM;
 import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_NONE;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -137,7 +137,7 @@
 import androidx.test.filters.MediumTest;
 
 import com.android.internal.R;
-import com.android.server.wm.Task.ActivityState;
+import com.android.server.wm.ActivityRecord.State;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -146,6 +146,8 @@
 import org.mockito.invocation.InvocationOnMock;
 
 import java.util.ArrayList;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 
 /**
@@ -172,25 +174,25 @@
     }
 
     @Test
-    public void testStackCleanupOnClearingTask() {
+    public void testTaskFragmentCleanupOnClearingTask() {
         final ActivityRecord activity = createActivityWith2LevelTask();
         final Task task = activity.getTask();
-        final Task rootTask = activity.getRootTask();
+        final TaskFragment taskFragment = activity.getTaskFragment();
         activity.onParentChanged(null /*newParent*/, task);
-        verify(rootTask, times(1)).cleanUpActivityReferences(any());
+        verify(taskFragment).cleanUpActivityReferences(any());
     }
 
     @Test
-    public void testStackCleanupOnActivityRemoval() {
+    public void testTaskFragmentCleanupOnActivityRemoval() {
         final ActivityRecord activity = createActivityWith2LevelTask();
         final Task task = activity.getTask();
-        final Task rootTask = activity.getRootTask();
+        final TaskFragment taskFragment = activity.getTaskFragment();
         task.removeChild(activity);
-        verify(rootTask, times(1)).cleanUpActivityReferences(any());
+        verify(taskFragment).cleanUpActivityReferences(any());
     }
 
     @Test
-    public void testStackCleanupOnTaskRemoval() {
+    public void testRootTaskCleanupOnTaskRemoval() {
         final ActivityRecord activity = createActivityWith2LevelTask();
         final Task task = activity.getTask();
         final Task rootTask = activity.getRootTask();
@@ -216,7 +218,7 @@
     public void testNoCleanupMovingActivityInSameStack() {
         final ActivityRecord activity = createActivityWith2LevelTask();
         final Task rootTask = activity.getRootTask();
-        final Task newTask = new TaskBuilder(mAtm.mTaskSupervisor).setParentTask(rootTask).build();
+        final Task newTask = createTaskInRootTask(rootTask, 0 /* userId */);
         activity.reparent(newTask, 0, null /*reason*/);
         verify(rootTask, times(0)).cleanUpActivityReferences(any());
     }
@@ -345,7 +347,7 @@
     public void testSetsRelaunchReason_NotDragResizing() {
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
-        activity.setState(Task.ActivityState.RESUMED, "Testing");
+        activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
         activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
@@ -370,7 +372,7 @@
     public void testSetsRelaunchReason_DragResizing() {
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
-        activity.setState(Task.ActivityState.RESUMED, "Testing");
+        activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
         activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
@@ -397,7 +399,7 @@
     public void testRelaunchClearTopWaitingTranslucent() {
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
-        activity.setState(Task.ActivityState.RESUMED, "Testing");
+        activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
         activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
@@ -418,7 +420,7 @@
     public void testSetsRelaunchReason_NonResizeConfigChanges() {
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
-        activity.setState(Task.ActivityState.RESUMED, "Testing");
+        activity.setState(RESUMED, "Testing");
 
         task.onRequestedOverrideConfigurationChanged(task.getConfiguration());
         activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
@@ -466,7 +468,7 @@
                 .setCreateTask(true)
                 .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)
                 .build();
-        activity.setState(Task.ActivityState.RESUMED, "Testing");
+        activity.setState(RESUMED, "Testing");
 
         activity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
                 activity.getConfiguration()));
@@ -629,7 +631,7 @@
     @Test
     public void testShouldMakeActive_deferredResume() {
         final ActivityRecord activity = createActivityWithTask();
-        activity.setState(Task.ActivityState.STOPPED, "Testing");
+        activity.setState(STOPPED, "Testing");
 
         mSupervisor.beginDeferResume();
         assertEquals(false, activity.shouldMakeActive(null /* activeActivity */));
@@ -645,7 +647,7 @@
         ActivityRecord finishingActivity = new ActivityBuilder(mAtm).setTask(task).build();
         finishingActivity.finishing = true;
         ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
-        activity.setState(Task.ActivityState.STOPPED, "Testing");
+        activity.setState(STOPPED, "Testing");
 
         assertEquals(false, activity.shouldMakeActive(null /* activeActivity */));
     }
@@ -654,15 +656,16 @@
     public void testShouldResume_stackVisibility() {
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
-        activity.setState(Task.ActivityState.STOPPED, "Testing");
+        activity.setState(STOPPED, "Testing");
 
-        doReturn(TASK_VISIBILITY_VISIBLE).when(task).getVisibility(null);
+        doReturn(TASK_FRAGMENT_VISIBILITY_VISIBLE).when(task).getVisibility(null);
         assertEquals(true, activity.shouldResumeActivity(null /* activeActivity */));
 
-        doReturn(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT).when(task).getVisibility(null);
+        doReturn(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT)
+                .when(task).getVisibility(null);
         assertEquals(false, activity.shouldResumeActivity(null /* activeActivity */));
 
-        doReturn(TASK_VISIBILITY_INVISIBLE).when(task).getVisibility(null);
+        doReturn(TASK_FRAGMENT_VISIBILITY_INVISIBLE).when(task).getVisibility(null);
         assertEquals(false, activity.shouldResumeActivity(null /* activeActivity */));
     }
 
@@ -670,13 +673,13 @@
     public void testShouldResumeOrPauseWithResults() {
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
-        activity.setState(Task.ActivityState.STOPPED, "Testing");
+        activity.setState(STOPPED, "Testing");
 
         ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
         activity.addResultLocked(topActivity, "resultWho", 0, 0, new Intent());
         topActivity.finishing = true;
 
-        doReturn(TASK_VISIBILITY_VISIBLE).when(task).getVisibility(null);
+        doReturn(TASK_FRAGMENT_VISIBILITY_VISIBLE).when(task).getVisibility(null);
         assertEquals(true, activity.shouldResumeActivity(null /* activeActivity */));
         assertEquals(false, activity.shouldPauseActivity(null /*activeActivity */));
     }
@@ -689,7 +692,7 @@
                 .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)
                 .build();
         final Task task = activity.getTask();
-        activity.setState(Task.ActivityState.STOPPED, "Testing");
+        activity.setState(STOPPED, "Testing");
 
         final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
         try {
@@ -732,7 +735,7 @@
         final ActivityRecord activity = createActivityWithTask();
         ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(activity.getTask()).build();
         topActivity.setOccludesParent(false);
-        activity.setState(Task.ActivityState.STOPPED, "Testing");
+        activity.setState(STOPPED, "Testing");
         activity.setVisibility(true);
         activity.makeActiveIfNeeded(null /* activeActivity */);
         assertEquals(STARTED, activity.getState());
@@ -1016,8 +1019,8 @@
     @Test
     public void testFinishActivityIfPossible_nonResumedFinishCompletesImmediately() {
         final ActivityRecord activity = createActivityWithTask();
-        final ActivityState[] states = {INITIALIZING, STARTED, PAUSED, STOPPING, STOPPED};
-        for (ActivityState state : states) {
+        final State[] states = {INITIALIZING, STARTED, PAUSED, STOPPING, STOPPED};
+        for (State state : states) {
             activity.finishing = false;
             activity.setState(state, "test");
             reset(activity);
@@ -1152,7 +1155,7 @@
     /**
      * Verify that finish request won't change the state of next top activity if the current
      * finishing activity doesn't need to be destroyed immediately. The case is usually like
-     * from {@link ActivityStack#completePauseLocked(boolean, ActivityRecord)} to
+     * from {@link Task#completePause(boolean, ActivityRecord)} to
      * {@link ActivityRecord#completeFinishing(String)}, so the complete-pause should take the
      * responsibility to resume the next activity with updating the state.
      */
@@ -1398,7 +1401,7 @@
     }
 
     private void testCompleteFinishing_ensureActivitiesVisible(boolean diffTask,
-            ActivityState secondActivityState) {
+            State secondActivityState) {
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
         final ActivityRecord firstActivity = new ActivityBuilder(mAtm).setTask(task).build();
@@ -1450,7 +1453,8 @@
     @Test
     public void testDestroyIfPossible() {
         final ActivityRecord activity = createActivityWithTask();
-        doReturn(false).when(mRootWindowContainer).resumeFocusedTasksTopActivities();
+        doReturn(false).when(mRootWindowContainer)
+                .resumeFocusedTasksTopActivities();
         activity.destroyIfPossible("test");
 
         assertEquals(DESTROYING, activity.getState());
@@ -1472,7 +1476,8 @@
             homeStack.removeChild(t, "test");
         }, true /* traverseTopToBottom */);
         activity.finishing = true;
-        doReturn(false).when(mRootWindowContainer).resumeFocusedTasksTopActivities();
+        doReturn(false).when(mRootWindowContainer)
+                .resumeFocusedTasksTopActivities();
 
         // Try to destroy the last activity above the home stack.
         activity.destroyIfPossible("test");
@@ -1599,16 +1604,23 @@
     }
 
     @Test
-    public void testRemoveImmediately() throws RemoteException {
-        final ActivityRecord activity = createActivityWithTask();
-        final WindowProcessController wpc = activity.app;
-        activity.getTask().removeImmediately("test");
-
-        verify(mAtm.getLifecycleManager()).scheduleTransaction(any(), eq(activity.appToken),
-                isA(DestroyActivityItem.class));
-        assertNull(activity.app);
-        assertEquals(DESTROYED, activity.getState());
-        assertFalse(wpc.hasActivities());
+    public void testRemoveImmediately() {
+        final Consumer<Consumer<ActivityRecord>> test = setup -> {
+            final ActivityRecord activity = createActivityWithTask();
+            final WindowProcessController wpc = activity.app;
+            setup.accept(activity);
+            activity.getTask().removeImmediately("test");
+            try {
+                verify(mAtm.getLifecycleManager()).scheduleTransaction(any(), eq(activity.appToken),
+                        isA(DestroyActivityItem.class));
+            } catch (RemoteException ignored) {
+            }
+            assertNull(activity.app);
+            assertEquals(DESTROYED, activity.getState());
+            assertFalse(wpc.hasActivities());
+        };
+        test.accept(activity -> activity.setState(RESUMED, "test"));
+        test.accept(activity -> activity.finishing = true);
     }
 
     @Test
@@ -1852,7 +1864,7 @@
             doReturn(WindowManagerGlobal.ADD_STARTING_NOT_NEEDED).when(session).addToDisplay(
                     any() /* window */,  any() /* attrs */,
                     anyInt() /* viewVisibility */, anyInt() /* displayId */,
-                    any() /* requestedVisibility */, any() /* outInputChannel */,
+                    any() /* requestedVisibilities */, any() /* outInputChannel */,
                     any() /* outInsetsState */, any() /* outActiveControls */);
             mAtm.mWindowManager.mStartingSurfaceController
                     .createTaskSnapshotSurface(activity, snapshot);
@@ -1908,8 +1920,7 @@
         assertTrue(wpc.registeredForActivityConfigChanges());
 
         // Create a new task with custom config to reparent the activity to.
-        final Task newTask =
-                new TaskBuilder(mSupervisor).setParentTask(initialTask.getRootTask()).build();
+        final Task newTask = new TaskBuilder(mSupervisor).build();
         final Configuration newConfig = newTask.getConfiguration();
         newConfig.densityDpi += 100;
         newTask.onRequestedOverrideConfigurationChanged(newConfig);
@@ -1941,8 +1952,7 @@
                 .diff(wpc.getRequestedOverrideConfiguration()));
 
         // Create a new task with custom config to reparent the second activity to.
-        final Task newTask =
-                new TaskBuilder(mSupervisor).setParentTask(initialTask.getRootTask()).build();
+        final Task newTask = new TaskBuilder(mSupervisor).build();
         final Configuration newConfig = newTask.getConfiguration();
         newConfig.densityDpi += 100;
         newTask.onRequestedOverrideConfigurationChanged(newConfig);
@@ -2152,7 +2162,7 @@
         assertFalse(activity.supportsPictureInPicture());
     }
 
-    private void verifyProcessInfoUpdate(ActivityRecord activity, ActivityState state,
+    private void verifyProcessInfoUpdate(ActivityRecord activity, State state,
             boolean shouldUpdate, boolean activityChange) {
         reset(activity.app);
         activity.setState(state, "test");
@@ -2499,16 +2509,19 @@
     @Test
     public void testTransferStartingWindow() {
         registerTestStartingWindowOrganizer();
-        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
-        final ActivityRecord activity2 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setVisible(false).build();
+        final ActivityRecord activity2 = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setVisible(false).build();
         activity1.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
                 false, false);
         waitUntilHandlersIdle();
         activity2.addStartingWindow(mPackageName,
-                android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity1.appToken.asBinder(),
+                android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity1,
                 true, true, false, true, false, false);
         waitUntilHandlersIdle();
+        assertFalse(mDisplayContent.mSkipAppTransitionAnimation);
         assertNoStartingWindow(activity1);
         assertHasStartingWindow(activity2);
     }
@@ -2523,7 +2536,7 @@
                     // Surprise, ...! Transfer window in the middle of the creation flow.
                     activity2.addStartingWindow(mPackageName,
                             android.R.style.Theme, null, "Test", 0, 0, 0, 0,
-                            activity1.appToken.asBinder(), true, true, false,
+                            activity1, true, true, false,
                             true, false, false);
                 });
         activity1.addStartingWindow(mPackageName,
@@ -2544,7 +2557,7 @@
                 false, false);
         waitUntilHandlersIdle();
         activity2.addStartingWindow(mPackageName,
-                android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity1.appToken.asBinder(),
+                android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity1,
                 true, true, false, true, false, false);
         waitUntilHandlersIdle();
         assertNoStartingWindow(activity1);
@@ -2590,17 +2603,17 @@
                 false /* activityCreate */, false /* suggestEmpty */);
         waitUntilHandlersIdle();
         assertHasStartingWindow(activity);
-        activity.mStartingWindowState = ActivityRecord.STARTING_WINDOW_SHOWN;
 
         doCallRealMethod().when(task).startActivityLocked(
                 any(), any(), anyBoolean(), anyBoolean(), any(), any());
         // In normal case, resumeFocusedTasksTopActivities() should be called after
         // startActivityLocked(). So skip resumeFocusedTasksTopActivities() in ActivityBuilder.
-        doReturn(false).when(mRootWindowContainer).resumeFocusedTasksTopActivities();
+        doReturn(false).when(mRootWindowContainer)
+                .resumeFocusedTasksTopActivities();
         // Make mVisibleSetFromTransferredStartingWindow true.
         final ActivityRecord middle = new ActivityBuilder(mAtm).setTask(task).build();
         task.startActivityLocked(middle, null /* focusedTopActivity */,
-                false /* newTask */, false /* keepCurTransition */, null /* options */,
+                false /* newTask */, false /* isTaskSwitch */, null /* options */,
                 null /* sourceRecord */);
         middle.makeFinishingLocked();
 
@@ -2613,9 +2626,10 @@
         top.setVisible(false);
         // The finishing middle should be able to transfer starting window to top.
         task.startActivityLocked(top, null /* focusedTopActivity */,
-                false /* newTask */, false /* keepCurTransition */, null /* options */,
+                false /* newTask */, false /* isTaskSwitch */, null /* options */,
                 null /* sourceRecord */);
 
+        assertTrue(mDisplayContent.mSkipAppTransitionAnimation);
         assertNull(middle.mStartingWindow);
         assertHasStartingWindow(top);
         assertTrue(top.isVisible());
@@ -2650,7 +2664,7 @@
         // Make sure the fixed rotation transform linked to activity2 when adding starting window
         // on activity2.
         topActivity.addStartingWindow(mPackageName,
-                android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity.appToken.asBinder(),
+                android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity,
                 false, false, false, true, false, false);
         waitUntilHandlersIdle();
         assertTrue(topActivity.hasFixedRotationTransform());
@@ -2682,6 +2696,58 @@
     }
 
     @Test
+    public void testStartingWindowInTaskFragment() {
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final WindowState startingWindow = createWindowState(
+                new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1);
+        activity1.addWindow(startingWindow);
+        activity1.mStartingData = mock(StartingData.class);
+        activity1.attachStartingWindow(startingWindow);
+        final Task task = activity1.getTask();
+        final Rect taskBounds = task.getBounds();
+        final int width = taskBounds.width();
+        final int height = taskBounds.height();
+        final BiConsumer<TaskFragment, Rect> fragmentSetup = (fragment, bounds) -> {
+            final Configuration config = fragment.getRequestedOverrideConfiguration();
+            config.windowConfiguration.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+            config.windowConfiguration.setBounds(bounds);
+            fragment.onRequestedOverrideConfigurationChanged(config);
+        };
+
+        final TaskFragment taskFragment1 = new TaskFragment(
+                mAtm, null /* fragmentToken */, false /* createdByOrganizer */);
+        fragmentSetup.accept(taskFragment1, new Rect(0, 0, width / 2, height));
+        task.addChild(taskFragment1, POSITION_TOP);
+
+        final TaskFragment taskFragment2 = new TaskFragment(
+                mAtm, null /* fragmentToken */, false /* createdByOrganizer */);
+        fragmentSetup.accept(taskFragment2, new Rect(width / 2, 0, width, height));
+        task.addChild(taskFragment2, POSITION_TOP);
+        final ActivityRecord activity2 = new ActivityBuilder(mAtm)
+                .setResizeMode(ActivityInfo.RESIZE_MODE_UNRESIZEABLE).build();
+        activity2.mVisibleRequested = true;
+        taskFragment2.addChild(activity2);
+        assertTrue(activity2.isResizeable());
+        activity1.reparent(taskFragment1, POSITION_TOP);
+
+        verify(activity1.getSyncTransaction()).reparent(eq(startingWindow.mSurfaceControl),
+                eq(task.mSurfaceControl));
+        assertEquals(activity1.mStartingData, startingWindow.mStartingData);
+        assertEquals(task.mSurfaceControl, startingWindow.getAnimationLeashParent());
+        assertEquals(task, activity1.mStartingData.mAssociatedTask);
+        assertEquals(taskFragment1.getBounds(), activity1.getBounds());
+        // The activity was resized by task fragment, but starting window must still cover the task.
+        assertEquals(taskBounds, activity1.mStartingWindow.getBounds());
+
+        // The starting window is only removed when all embedded activities are drawn.
+        final WindowState activityWindow = mock(WindowState.class);
+        activity1.onFirstWindowDrawn(activityWindow);
+        assertNotNull(activity1.mStartingWindow);
+        activity2.onFirstWindowDrawn(activityWindow);
+        assertNull(activity1.mStartingWindow);
+    }
+
+    @Test
     public void testTransitionAnimationBounds() {
         removeGlobalMinSizeRestriction();
         final Task task = new TaskBuilder(mSupervisor)
@@ -2716,10 +2782,36 @@
 
         assertEquals(taskBounds, activity.getAnimationBounds(ROOT_TASK_CLIP_AFTER_ANIM));
         assertEquals(new Point(0, 0), animationPosition);
+    }
 
-        // ROOT_TASK_CLIP_BEFORE_ANIM should use stack bounds since it won't be clipped later.
-        task.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
-        assertEquals(rootTask.getBounds(), activity.getAnimationBounds(ROOT_TASK_CLIP_BEFORE_ANIM));
+    @Test
+    public void testTransitionAnimationBounds_returnTaskFragment() {
+        removeGlobalMinSizeRestriction();
+        final Task task = new TaskBuilder(mSupervisor).setCreateParentTask(true).build();
+        final Task rootTask = task.getRootTask();
+        final TaskFragment taskFragment = createTaskFragmentWithParentTask(task,
+                false /* createEmbeddedTask */);
+        final ActivityRecord activity = taskFragment.getTopNonFinishingActivity();
+        final Rect stackBounds = new Rect(0, 0, 1000, 600);
+        final Rect taskBounds = new Rect(100, 400, 600, 800);
+        final Rect taskFragmentBounds = new Rect(100, 400, 300, 800);
+        final Rect activityBounds = new Rect(100, 400, 300, 600);
+        // Set the bounds and windowing mode to window configuration directly, otherwise the
+        // testing setups may be discarded by configuration resolving.
+        rootTask.getWindowConfiguration().setBounds(stackBounds);
+        task.getWindowConfiguration().setBounds(taskBounds);
+        taskFragment.getWindowConfiguration().setBounds(taskFragmentBounds);
+        activity.getWindowConfiguration().setBounds(activityBounds);
+
+        // Check that anim bounds for freeform window match task fragment bounds
+        task.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
+        assertEquals(taskFragment.getBounds(), activity.getAnimationBounds(ROOT_TASK_CLIP_NONE));
+
+        // ROOT_TASK_CLIP_AFTER_ANIM should use task fragment bounds since they will be clipped by
+        // bounds animation layer.
+        task.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        assertEquals(taskFragment.getBounds(),
+                activity.getAnimationBounds(ROOT_TASK_CLIP_AFTER_ANIM));
     }
 
     @Test
@@ -2739,6 +2831,40 @@
     }
 
     @Test
+    public void testCloseToSquareFixedOrientationPortrait() {
+        // create a square display
+        final DisplayContent squareDisplay = new TestDisplayContent.Builder(mAtm, 2000, 2000)
+                .setSystemDecorations(true).build();
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(squareDisplay).build();
+
+        // create a fixed portrait activity
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task)
+                .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT).build();
+
+        // check that both the configuration and app bounds are portrait
+        assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation);
+        assertTrue(activity.getConfiguration().windowConfiguration.getAppBounds().width()
+                <= activity.getConfiguration().windowConfiguration.getAppBounds().height());
+    }
+
+    @Test
+    public void testCloseToSquareFixedOrientationLandscape() {
+        // create a square display
+        final DisplayContent squareDisplay = new TestDisplayContent.Builder(mAtm, 2000, 2000)
+                .setSystemDecorations(true).build();
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(squareDisplay).build();
+
+        // create a fixed landscape activity
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task)
+                .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE).build();
+
+        // check that both the configuration and app bounds are landscape
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation);
+        assertTrue(activity.getConfiguration().windowConfiguration.getAppBounds().width()
+                > activity.getConfiguration().windowConfiguration.getAppBounds().height());
+    }
+
+    @Test
     public void testSetVisibility_visibleToVisible() {
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true).build();
@@ -2866,6 +2992,7 @@
         mDisplayContent.setImeInputTarget(app);
 
         // Simulate app is closing and expect the last IME is shown and IME insets is frozen.
+        mDisplayContent.mOpeningApps.clear();
         app.mActivityRecord.commitVisibility(false, false);
         app.mActivityRecord.onWindowsGone();
 
@@ -2884,6 +3011,28 @@
         assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
     }
 
+    @Test
+    public void testInClosingAnimation_doNotHideSurface() {
+        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        makeWindowVisibleAndDrawn(app);
+
+        // Put the activity in close transition.
+        mDisplayContent.mOpeningApps.clear();
+        mDisplayContent.mClosingApps.add(app.mActivityRecord);
+        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
+
+        // Update visibility and call to remove window
+        app.mActivityRecord.commitVisibility(false, false);
+        app.mActivityRecord.prepareSurfaces();
+
+        // Because the app is waiting for transition, it should not hide the surface.
+        assertTrue(app.mActivityRecord.isSurfaceShowing());
+
+        // Ensure onAnimationFinished will callback when the closing animation is finished.
+        verify(app.mActivityRecord).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION),
+                eq(null));
+    }
+
     private void assertHasStartingWindow(ActivityRecord atoken) {
         assertNotNull(atoken.mStartingSurface);
         assertNotNull(atoken.mStartingData);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
index a3ad09a..c103bc6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 
+import android.annotation.Nullable;
 import android.app.ActivityManagerInternal;
 import android.app.KeyguardManager;
 import android.app.admin.DevicePolicyManagerInternal;
@@ -42,6 +43,7 @@
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 import android.testing.DexmakerShareClassLoaderRule;
+import android.util.SparseArray;
 
 import androidx.test.filters.SmallTest;
 
@@ -64,7 +66,7 @@
  * Unit tests for {@link ActivityStartInterceptorTest}.
  *
  * Build/Install/Run:
- *  atest WmTests:ActivityStartInterceptorTest
+ * atest WmTests:ActivityStartInterceptorTest
  */
 @SmallTest
 @Presubmit
@@ -114,6 +116,9 @@
     private ActivityStartInterceptor mInterceptor;
     private ActivityInfo mAInfo = new ActivityInfo();
 
+    private SparseArray<ActivityInterceptorCallback> mActivityInterceptorCallbacks =
+            new SparseArray<>();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -157,6 +162,9 @@
                 TEST_USER_ID, TEST_PACKAGE_NAME, LOCK_TASK_LAUNCH_MODE_DEFAULT))
                 .thenReturn(true);
 
+        // Mock the activity start callbacks
+        when(mService.getActivityInterceptorCallbacks()).thenReturn(mActivityInterceptorCallbacks);
+
         // Initialise activity info
         mAInfo.applicationInfo = new ApplicationInfo();
         mAInfo.packageName = mAInfo.applicationInfo.packageName = TEST_PACKAGE_NAME;
@@ -285,4 +293,38 @@
         // THEN calling intercept returns false
         assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
     }
+
+    public void addMockInterceptorCallback(@Nullable Intent intent) {
+        int size = mActivityInterceptorCallbacks.size();
+        mActivityInterceptorCallbacks.put(size, new ActivityInterceptorCallback() {
+            @Override
+            public Intent intercept(ActivityInterceptorInfo info) {
+                return intent;
+            }
+        });
+    }
+
+    @Test
+    public void testInterceptionCallback_singleCallback() {
+        addMockInterceptorCallback(new Intent("android.test.foo"));
+
+        assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+        assertEquals("android.test.foo", mInterceptor.mIntent.getAction());
+    }
+
+    @Test
+    public void testInterceptionCallback_singleCallbackReturnsNull() {
+        addMockInterceptorCallback(null);
+
+        assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+    }
+
+    @Test
+    public void testInterceptionCallback_fallbackToSecondCallback() {
+        addMockInterceptorCallback(null);
+        addMockInterceptorCallback(new Intent("android.test.second"));
+
+        assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+        assertEquals("android.test.second", mInterceptor.mIntent.getAction());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index d0588a3..3e8a2e9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -32,6 +32,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
@@ -477,7 +478,7 @@
         final ActivityRecord splitSecondActivity =
                 new ActivityBuilder(mAtm).setCreateTask(true).build();
         final ActivityRecord splitPrimaryActivity = new TaskBuilder(mSupervisor)
-                .setParentTask(splitOrg.mPrimary)
+                .setParentTaskFragment(splitOrg.mPrimary)
                 .setCreateActivity(true)
                 .build()
                 .getTopMostActivity();
@@ -755,12 +756,12 @@
     }
 
     /**
-     * This test ensures that {@link ActivityStarter#setTargetStackAndMoveToFrontIfNeeded} will
-     * move the existing task to front if the current focused stack doesn't have running task.
+     * This test ensures that {@link ActivityStarter#setTargetRootTaskIfNeeded} will
+     * move the existing task to front if the current focused root task doesn't have running task.
      */
     @Test
-    public void testBringTaskToFrontWhenFocusedStackIsFinising() {
-        // Put 2 tasks in the same stack (simulate the behavior of home stack).
+    public void testBringTaskToFrontWhenFocusedTaskIsFinishing() {
+        // Put 2 tasks in the same root task (simulate the behavior of home root task).
         final Task rootTask = new TaskBuilder(mSupervisor).build();
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setParentTask(rootTask)
@@ -777,13 +778,16 @@
         assertEquals(finishingTopActivity, mRootWindowContainer.topRunningActivity());
         finishingTopActivity.finishing = true;
 
-        // Launch the bottom task of the target stack.
+        // Launch the bottom task of the target root task.
         prepareStarter(FLAG_ACTIVITY_NEW_TASK, false /* mockGetLaunchStack */)
-                .setReason("testBringTaskToFrontWhenTopStackIsFinising")
-                .setIntent(activity.intent)
+                .setReason("testBringTaskToFrontWhenFocusedTaskIsFinishing")
+                .setIntent(activity.intent.addFlags(
+                        FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))
                 .execute();
+        verify(activity.getRootTask()).startActivityLocked(any(), any(), anyBoolean(),
+                eq(true) /* isTaskSwitch */, any(), any());
         // The hierarchies of the activity should move to front.
-        assertEquals(activity, mRootWindowContainer.topRunningActivity());
+        assertEquals(activity.getTask(), mRootWindowContainer.topRunningActivity().getTask());
     }
 
     /**
@@ -852,7 +856,7 @@
         // Create another activity on top of the secondary display.
         final Task topStack = secondaryTaskContainer.createRootTask(WINDOWING_MODE_FULLSCREEN,
                 ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        final Task topTask = new TaskBuilder(mSupervisor).setParentTask(topStack).build();
+        final Task topTask = new TaskBuilder(mSupervisor).setParentTaskFragment(topStack).build();
         new ActivityBuilder(mAtm).setTask(topTask).build();
 
         doReturn(mActivityMetricsLogger).when(mSupervisor).getActivityMetricsLogger();
@@ -916,7 +920,7 @@
                 DEFAULT_COMPONENT_PACKAGE_NAME + ".SingleTaskActivity");
         final Task task = new TaskBuilder(mSupervisor)
                 .setComponent(componentName)
-                .setParentTask(stack)
+                .setParentTaskFragment(stack)
                 .build();
         return new ActivityBuilder(mAtm)
                 .setComponent(componentName)
@@ -1052,8 +1056,8 @@
         final ActivityStarter starter = prepareStarter(0 /* flags */);
         starter.mStartActivity = new ActivityBuilder(mAtm).build();
         final Task task = new TaskBuilder(mAtm.mTaskSupervisor)
-                .setParentTask(mAtm.mRootWindowContainer.getDefaultTaskDisplayArea().createRootTask(
-                        WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */))
+                .setParentTaskFragment(createTask(mDisplayContent, WINDOWING_MODE_FULLSCREEN,
+                        ACTIVITY_TYPE_STANDARD))
                 .setUserId(10)
                 .build();
 
@@ -1136,6 +1140,7 @@
                 /* doResume */true,
                 /* options */null,
                 /* inTask */null,
+                /* inTaskFragment */ null,
                 /* restrictedBgActivity */false,
                 /* intentGrants */null);
 
@@ -1146,6 +1151,31 @@
     }
 
     @Test
+    public void testStartActivityInner_inTaskFragment() {
+        final ActivityStarter starter = prepareStarter(0, false);
+        final ActivityRecord targetRecord = new ActivityBuilder(mAtm).build();
+        final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final TaskFragment taskFragment = new TaskFragment(mAtm, sourceRecord.token,
+                true /* createdByOrganizer */);
+        sourceRecord.getTask().addChild(taskFragment, POSITION_TOP);
+
+        starter.startActivityInner(
+                /* r */targetRecord,
+                /* sourceRecord */ sourceRecord,
+                /* voiceSession */null,
+                /* voiceInteractor */ null,
+                /* startFlags */ 0,
+                /* doResume */true,
+                /* options */null,
+                /* inTask */null,
+                /* inTaskFragment */ taskFragment,
+                /* restrictedBgActivity */false,
+                /* intentGrants */null);
+
+        assertTrue(taskFragment.hasChild());
+    }
+
+    @Test
     public void testLaunchCookie_newAndExistingTask() {
         final ActivityStarter starter = prepareStarter(0, false);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 741f33f..5d1a068 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -26,9 +26,16 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.ActivityInterceptorCallback.FIRST_ORDERED_ID;
+import static com.android.server.wm.ActivityInterceptorCallback.LAST_ORDERED_ID;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -37,15 +44,21 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.IApplicationThread;
 import android.app.PictureInPictureParams;
 import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.EnterPipRequestedItem;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.IBinder;
+import android.os.LocaleList;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
@@ -57,6 +70,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
 import org.mockito.MockitoSession;
 
 import java.util.ArrayList;
@@ -76,6 +90,9 @@
     private final ArgumentCaptor<ClientTransaction> mClientTransactionCaptor =
             ArgumentCaptor.forClass(ClientTransaction.class);
 
+    private static final String DEFAULT_PACKAGE_NAME = "my.application.package";
+    private static final int DEFAULT_USER_ID = 100;
+
     @Before
     public void setUp() throws Exception {
         setBooted(mAtm);
@@ -169,7 +186,10 @@
             @Override
             public void onFixedRotationFinished(int displayId) {}
         };
-        mAtm.mWindowManager.registerDisplayWindowListener(listener);
+        int[] displayIds = mAtm.mWindowManager.registerDisplayWindowListener(listener);
+        for (int i = 0; i < displayIds.length; i++) {
+            added.add(displayIds[i]);
+        }
         // Check that existing displays call added
         assertEquals(mRootWindowContainer.getChildCount(), added.size());
         assertEquals(0, changed.size());
@@ -244,7 +264,7 @@
                 .setTask(mRootWindowContainer.getDefaultTaskDisplayArea().getOrCreateRootHomeTask())
                 .build();
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
-        activity.setState(Task.ActivityState.RESUMED, "test");
+        activity.setState(RESUMED, "test");
         mSupervisor.endDeferResume();
 
         assertEquals(activity.app, mAtm.mInternal.getTopApp());
@@ -254,13 +274,13 @@
         activity.mVisibleRequested = false;
         activity.setVisible(false);
         activity.getTask().setPausingActivity(activity);
-        homeActivity.setState(Task.ActivityState.PAUSED, "test");
+        homeActivity.setState(PAUSED, "test");
 
         // Even the visibility states are invisible, the next activity should be resumed because
         // the crashed activity was pausing.
         mAtm.mInternal.handleAppDied(activity.app, false /* restarting */,
                 null /* finishInstrumentationCallback */);
-        assertEquals(Task.ActivityState.RESUMED, homeActivity.getState());
+        assertEquals(RESUMED, homeActivity.getState());
         assertEquals(homeActivity.app, mAtm.mInternal.getTopApp());
     }
 
@@ -271,7 +291,7 @@
         final Task rootHomeTask = mWm.mRoot.getDefaultTaskDisplayArea().getOrCreateRootHomeTask();
         final ActivityRecord homeActivity = new ActivityBuilder(mAtm).setTask(rootHomeTask).build();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm).setCreateTask(true).build();
-        topActivity.setState(Task.ActivityState.RESUMED, "test");
+        topActivity.setState(RESUMED, "test");
 
         final Consumer<ActivityRecord> assertTopNonSleeping = activity -> {
             assertFalse(mAtm.mInternal.isSleeping());
@@ -287,18 +307,27 @@
         verify(mSupervisor.mGoingToSleepWakeLock).acquire();
         doReturn(true).when(mSupervisor.mGoingToSleepWakeLock).isHeld();
 
-        assertEquals(Task.ActivityState.PAUSING, topActivity.getState());
+        assertEquals(PAUSING, topActivity.getState());
         assertTrue(mAtm.mInternal.isSleeping());
         assertEquals(ActivityManager.PROCESS_STATE_TOP_SLEEPING,
                 mAtm.mInternal.getTopProcessState());
         // The top app should not change while sleeping.
         assertEquals(topActivity.app, mAtm.mInternal.getTopApp());
 
+        mAtm.startLaunchPowerMode(ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY
+                | ActivityTaskManagerService.POWER_MODE_REASON_UNKNOWN_VISIBILITY);
+        assertEquals(ActivityManager.PROCESS_STATE_TOP, mAtm.mInternal.getTopProcessState());
+        // Because there is no unknown visibility record, the state will be restored if other
+        // reasons are all done.
+        mAtm.endLaunchPowerMode(ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY);
+        assertEquals(ActivityManager.PROCESS_STATE_TOP_SLEEPING,
+                mAtm.mInternal.getTopProcessState());
+
         // If all activities are stopped, the sleep wake lock must be released.
         final Task topRootTask = topActivity.getRootTask();
         doReturn(true).when(rootHomeTask).goToSleepIfPossible(anyBoolean());
         doReturn(true).when(topRootTask).goToSleepIfPossible(anyBoolean());
-        topActivity.setState(Task.ActivityState.STOPPING, "test");
+        topActivity.setState(STOPPING, "test");
         topActivity.activityStopped(null /* newIcicle */, null /* newPersistentState */,
                 null /* description */);
         verify(mSupervisor.mGoingToSleepWakeLock).release();
@@ -473,5 +502,412 @@
         assertTrue(activity.supportsMultiWindow());
         assertTrue(task.supportsMultiWindow());
     }
-}
 
+    @Test
+    public void testPackageConfigUpdate_locales_successfullyApplied() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID));
+
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")).commit();
+
+        WindowProcessController wpcAfterConfigChange = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange.getConfiguration().getLocales());
+        assertFalse(wpcAfterConfigChange.getConfiguration().isNightModeActive());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_nightMode_successfullyApplied() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID));
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        WindowProcessController wpcAfterConfigChange = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertTrue(wpcAfterConfigChange.getConfiguration().isNightModeActive());
+        assertEquals(LocaleList.forLanguageTags("en-XC"),
+                wpcAfterConfigChange.getConfiguration().getLocales());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_multipleLocaleUpdates_successfullyApplied() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        WindowProcessController wpc = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), wpc);
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB"))
+                .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        WindowProcessController wpcAfterConfigChange1 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange1.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpc.getConfiguration().getLocales());
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("ja-XC,en-XC")).commit();
+
+        WindowProcessController wpcAfterConfigChange2 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        assertEquals(LocaleList.forLanguageTags("ja-XC,en-XC"),
+                wpcAfterConfigChange2.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+        assertEquals(LocaleList.forLanguageTags("ja-XC,en-XC"),
+                wpc.getConfiguration().getLocales());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_multipleNightModeUpdates_successfullyApplied() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID));
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB"))
+                .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        WindowProcessController wpcAfterConfigChange1 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange1.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+
+        packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_NO).commit();
+
+        WindowProcessController wpcAfterConfigChange2 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange2.getConfiguration().getLocales());
+        assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_onPackageUninstall_configShouldNotApply() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID));
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB"))
+                .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        WindowProcessController wpcAfterConfigChange1 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange1.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+
+        mAtm.mInternal.onPackageUninstalled(DEFAULT_PACKAGE_NAME);
+
+        WindowProcessController wpcAfterConfigChange2 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XC"),
+                wpcAfterConfigChange2.getConfiguration().getLocales());
+        assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_LocalesEmptyAndNightModeUndefined_configShouldNotApply() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        WindowProcessController wpc = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), wpc);
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB"))
+                .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+        WindowProcessController wpcAfterConfigChange1 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange1.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpc.getConfiguration().getLocales());
+
+        packageConfigUpdater.setLocales(LocaleList.getEmptyLocaleList())
+                .setNightMode(Configuration.UI_MODE_NIGHT_UNDEFINED).commit();
+
+        WindowProcessController wpcAfterConfigChange2 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XC"),
+                wpcAfterConfigChange2.getConfiguration().getLocales());
+        assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive());
+        assertEquals(LocaleList.forLanguageTags("en-XC"),
+                wpc.getConfiguration().getLocales());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_WhenUserRemoved_configShouldNotApply() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID));
+
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB"))
+                .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        WindowProcessController wpcAfterConfigChange1 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange1.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+
+        mAtm.mInternal.removeUser(DEFAULT_USER_ID);
+
+        WindowProcessController wpcAfterConfigChange2 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XC"),
+                wpcAfterConfigChange2.getConfiguration().getLocales());
+        assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_setLocaleListToEmpty_doesNotOverlayLocaleListInWpc() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID));
+
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB"))
+                .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        WindowProcessController wpcAfterConfigChange1 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange1.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+
+        packageConfigUpdater.setLocales(LocaleList.getEmptyLocaleList()).commit();
+
+        WindowProcessController wpcAfterConfigChange2 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XC"),
+                wpcAfterConfigChange2.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange2.getConfiguration().isNightModeActive());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_resetNightMode_doesNotOverrideNightModeInWpc() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID));
+
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater();
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB"))
+                .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        WindowProcessController wpcAfterConfigChange1 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange1.getConfiguration().getLocales());
+        assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive());
+
+        packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_UNDEFINED).commit();
+
+        WindowProcessController wpcAfterConfigChange2 = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange2.getConfiguration().getLocales());
+        assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_localesNotSet_localeConfigRetrievedNull() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true,
+                DEFAULT_USER_ID);
+        WindowProcessController wpc = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), wpc);
+        mAtm.mInternal.onProcessAdded(wpc);
+
+        ActivityTaskManagerInternal.PackageConfig appSpecificConfig = mAtm.mInternal
+                .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        // when no configuration is set we get a null object.
+        assertNull(appSpecificConfig);
+
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater(DEFAULT_PACKAGE_NAME,
+                        DEFAULT_USER_ID);
+        packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_YES).commit();
+
+        ActivityTaskManagerInternal.PackageConfig appSpecificConfig2 = mAtm.mInternal
+                .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertNotNull(appSpecificConfig2);
+        assertNull(appSpecificConfig2.mLocales);
+        assertEquals(appSpecificConfig2.mNightMode.intValue(), Configuration.UI_MODE_NIGHT_YES);
+    }
+
+    @Test
+    public void testPackageConfigUpdate_appNotRunning_configSuccessfullyApplied() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true,
+                DEFAULT_USER_ID);
+
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater(DEFAULT_PACKAGE_NAME,
+                        DEFAULT_USER_ID);
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")).commit();
+
+        // Verifies if the persisted app-specific configuration is same as the committed
+        // configuration.
+        ActivityTaskManagerInternal.PackageConfig appSpecificConfig = mAtm.mInternal
+                .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertNotNull(appSpecificConfig);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB"), appSpecificConfig.mLocales);
+
+        // Verifies if the persisted configuration for an arbitrary app is applied correctly when
+        // a new WindowProcessController is created for it.
+        WindowProcessController wpcAfterConfigChange = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpcAfterConfigChange.getConfiguration().getLocales());
+    }
+
+    @Test
+    public void testPackageConfigUpdate_appRunning_configSuccessfullyApplied() {
+        Configuration config = mAtm.getGlobalConfiguration();
+        config.setLocales(LocaleList.forLanguageTags("en-XC"));
+        mAtm.updateGlobalConfigurationLocked(config, true, true,
+                DEFAULT_USER_ID);
+        WindowProcessController wpc = createWindowProcessController(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        mAtm.mProcessMap.put(Binder.getCallingPid(), wpc);
+        mAtm.mInternal.onProcessAdded(wpc);
+
+        ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater =
+                mAtm.mInternal.createPackageConfigurationUpdater(DEFAULT_PACKAGE_NAME,
+                        DEFAULT_USER_ID);
+
+        packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")).commit();
+
+        ActivityTaskManagerInternal.PackageConfig appSpecificConfig = mAtm.mInternal
+                .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        // Verifies if the persisted app-specific configuration is same as the committed
+        // configuration.
+        assertNotNull(appSpecificConfig);
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB"), appSpecificConfig.mLocales);
+
+        // Verifies if the committed configuration is successfully applied to the required
+        // application while it is currently running.
+        assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"),
+                wpc.getConfiguration().getLocales());
+    }
+
+    private WindowProcessController createWindowProcessController(String packageName,
+            int userId) {
+        WindowProcessListener mMockListener = Mockito.mock(WindowProcessListener.class);
+        ApplicationInfo info = mock(ApplicationInfo.class);
+        info.packageName = packageName;
+        WindowProcessController wpc = new WindowProcessController(
+                mAtm, info, packageName, 0, userId, null, mMockListener);
+        wpc.setThread(mock(IApplicationThread.class));
+        return wpc;
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testRegisterActivityStartInterceptor_IndexTooSmall() {
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID - 1,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testRegisterActivityStartInterceptor_IndexTooLarge() {
+        mAtm.mInternal.registerActivityStartInterceptor(LAST_ORDERED_ID + 1,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testRegisterActivityStartInterceptor_DuplicateId() {
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+    }
+
+    @Test
+    public void testRegisterActivityStartInterceptor() {
+        assertEquals(0, mAtm.getActivityInterceptorCallbacks().size());
+
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+
+        assertEquals(1, mAtm.getActivityInterceptorCallbacks().size());
+        assertTrue(mAtm.getActivityInterceptorCallbacks().contains(FIRST_ORDERED_ID));
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 19f9b75..a34586b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -52,6 +52,7 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
 
 import java.util.concurrent.TimeUnit;
 
@@ -111,16 +112,18 @@
         final ActivityMetricsLogger.LaunchingState launchingState =
                 new ActivityMetricsLogger.LaunchingState();
         spyOn(launchingState);
-        doReturn(true).when(launchingState).contains(eq(secondActivity));
+        doReturn(true).when(launchingState).hasActiveTransitionInfo();
+        doReturn(true).when(launchingState).contains(
+                ArgumentMatchers.argThat(r -> r == firstActivity || r == secondActivity));
         // The test case already runs inside global lock, so above thread can only execute after
         // this waiting method that releases the lock.
         mSupervisor.waitActivityVisibleOrLaunched(taskToFrontWait, firstActivity, launchingState);
 
         // Assert that the thread is finished.
         assertTrue(condition.block(TIMEOUT_MS));
-        assertEquals(taskToFrontWait.result, START_TASK_TO_FRONT);
-        assertEquals(taskToFrontWait.who, secondActivity.mActivityComponent);
-        assertEquals(taskToFrontWait.launchState, WaitResult.LAUNCH_STATE_HOT);
+        assertEquals(START_TASK_TO_FRONT, taskToFrontWait.result);
+        assertEquals(secondActivity.mActivityComponent, taskToFrontWait.who);
+        assertEquals(WaitResult.LAUNCH_STATE_HOT, taskToFrontWait.launchState);
         // START_TASK_TO_FRONT means that another component will be visible, so the component
         // should not be assigned as the first activity.
         assertNull(launchedComponent[0]);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 3a6aac9..5d0e34a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -26,16 +26,28 @@
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
+import android.annotation.Nullable;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -47,8 +59,9 @@
 import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationTarget;
 import android.view.WindowManager;
+import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentOrganizer;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
@@ -94,11 +107,24 @@
         assertEquals(WindowManager.TRANSIT_OLD_UNSET,
                 AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null, null, false));
+                        mDisplayContent.mChangingContainers, null, null, false));
     }
 
     @Test
-    @FlakyTest(bugId = 131005232)
+    public void testClearTaskSkipAppExecuteTransition() {
+        final ActivityRecord behind = createActivityRecord(mDisplayContent,
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+        final Task task = behind.getTask();
+        final ActivityRecord top = createActivityRecord(task);
+        top.setState(ActivityRecord.State.RESUMED, "test");
+        behind.setState(ActivityRecord.State.STARTED, "test");
+        behind.mVisibleRequested = true;
+
+        task.performClearTask("test");
+        assertFalse(mDisplayContent.mAppTransition.isReady());
+    }
+
+    @Test
     public void testTranslucentOpen() {
         final ActivityRecord behind = createActivityRecord(mDisplayContent,
                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
@@ -112,12 +138,11 @@
 
         assertEquals(WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN,
                 AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
-                    mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                    null, null, false));
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null, null, false));
     }
 
     @Test
-    @FlakyTest(bugId = 131005232)
     public void testTranslucentClose() {
         final ActivityRecord behind = createActivityRecord(mDisplayContent,
                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
@@ -129,11 +154,10 @@
         assertEquals(WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE,
                 AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null, null, false));
+                        mDisplayContent.mChangingContainers, null, null, false));
     }
 
     @Test
-    @FlakyTest(bugId = 131005232)
     public void testChangeIsNotOverwritten() {
         final ActivityRecord behind = createActivityRecord(mDisplayContent,
                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
@@ -144,14 +168,14 @@
         mDisplayContent.prepareAppTransition(TRANSIT_CHANGE);
         mDisplayContent.mOpeningApps.add(behind);
         mDisplayContent.mOpeningApps.add(translucentOpening);
+        mDisplayContent.mChangingContainers.add(translucentOpening.getTask());
         assertEquals(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE,
                 AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null, null, false));
+                        mDisplayContent.mChangingContainers, null, null, false));
     }
 
     @Test
-    @FlakyTest(bugId = 131005232)
     public void testTransitWithinTask() {
         final ActivityRecord opening = createActivityRecord(mDisplayContent,
                 WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD);
@@ -198,7 +222,7 @@
         assertEquals(WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN,
                 AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        appWindowClosing, null, false));
+                        mDisplayContent.mChangingContainers, appWindowClosing, null, false));
     }
 
     @Test
@@ -229,7 +253,7 @@
         assertEquals(WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN,
                 AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        appWindowClosing, null, false));
+                        mDisplayContent.mChangingContainers, appWindowClosing, null, false));
     }
 
     @Test
@@ -375,7 +399,7 @@
         final ArraySet<ActivityRecord> closing = new ArraySet<>();
         closing.add(activity3);
 
-        // Promote animation targets to TaskStack level. Invisible ActivityRecords don't affect
+        // Promote animation targets to root Task level. Invisible ActivityRecords don't affect
         // promotion decision.
         assertEquals(
                 new ArraySet<>(new WindowContainer[]{activity1.getRootTask()}),
@@ -520,6 +544,128 @@
                         opening, closing, false /* visible */));
     }
 
+    @Test
+    public void testGetAnimationTargets_openingClosingTaskFragment() {
+        // [DefaultTDA] - [Task] -+- [TaskFragment1] - [ActivityRecord1] (opening, invisible)
+        //                        +- [TaskFragment2] - [ActivityRecord2] (closing, visible)
+        final Task parentTask = createTask(mDisplayContent);
+        final TaskFragment taskFragment1 = createTaskFragmentWithParentTask(parentTask,
+                false /* createEmbeddedTask */);
+        final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
+        activity1.setVisible(false);
+        activity1.mVisibleRequested = true;
+
+        final TaskFragment taskFragment2 = createTaskFragmentWithParentTask(parentTask,
+                false /* createEmbeddedTask */);
+        final ActivityRecord activity2 = taskFragment2.getTopMostActivity();
+        activity2.setVisible(true);
+        activity2.mVisibleRequested = false;
+
+        final ArraySet<ActivityRecord> opening = new ArraySet<>();
+        opening.add(activity1);
+        final ArraySet<ActivityRecord> closing = new ArraySet<>();
+        closing.add(activity2);
+
+        // Promote animation targets up to TaskFragment level, not beyond.
+        assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment1}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, true /* visible */));
+        assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment2}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, false /* visible */));
+    }
+
+    @Test
+    public void testGetAnimationTargets_openingClosingTaskFragmentWithEmbeddedTask() {
+        // [DefaultTDA] - [Task] -+- [TaskFragment1] - [ActivityRecord1] (opening, invisible)
+        //                        +- [TaskFragment2] - [ActivityRecord2] (closing, visible)
+        final Task parentTask = createTask(mDisplayContent);
+        final TaskFragment taskFragment1 = createTaskFragmentWithParentTask(parentTask,
+                true /* createEmbeddedTask */);
+        final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
+        activity1.setVisible(false);
+        activity1.mVisibleRequested = true;
+
+        final TaskFragment taskFragment2 = createTaskFragmentWithParentTask(parentTask,
+                true /* createEmbeddedTask */);
+        final ActivityRecord activity2 = taskFragment2.getTopMostActivity();
+        activity2.setVisible(true);
+        activity2.mVisibleRequested = false;
+
+        final ArraySet<ActivityRecord> opening = new ArraySet<>();
+        opening.add(activity1);
+        final ArraySet<ActivityRecord> closing = new ArraySet<>();
+        closing.add(activity2);
+
+        // Promote animation targets up to TaskFragment level, not beyond.
+        assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment1}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, true /* visible */));
+        assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment2}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, false /* visible */));
+    }
+
+    @Test
+    public void testGetAnimationTargets_openingTheOnlyTaskFragmentInTask() {
+        // [DefaultTDA] -+- [Task1] - [TaskFragment1] - [ActivityRecord1] (opening, invisible)
+        //               +- [Task2] - [ActivityRecord2] (closing, visible)
+        final Task task1 = createTask(mDisplayContent);
+        final TaskFragment taskFragment1 = createTaskFragmentWithParentTask(task1,
+                false /* createEmbeddedTask */);
+        final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
+        activity1.setVisible(false);
+        activity1.mVisibleRequested = true;
+
+        final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
+        activity2.setVisible(true);
+        activity2.mVisibleRequested = false;
+
+        final ArraySet<ActivityRecord> opening = new ArraySet<>();
+        opening.add(activity1);
+        final ArraySet<ActivityRecord> closing = new ArraySet<>();
+        closing.add(activity2);
+
+        // Promote animation targets up to leaf Task level because there's only one TaskFragment in
+        // the Task.
+        assertEquals(new ArraySet<>(new WindowContainer[]{task1}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, true /* visible */));
+        assertEquals(new ArraySet<>(new WindowContainer[]{activity2.getTask()}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, false /* visible */));
+    }
+
+    @Test
+    public void testGetAnimationTargets_closingTheOnlyTaskFragmentInTask() {
+        // [DefaultTDA] -+- [Task1] - [TaskFragment1] - [ActivityRecord1] (closing, visible)
+        //               +- [Task2] - [ActivityRecord2] (opening, invisible)
+        final Task task1 = createTask(mDisplayContent);
+        final TaskFragment taskFragment1 = createTaskFragmentWithParentTask(task1,
+                false /* createEmbeddedTask */);
+        final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
+        activity1.setVisible(true);
+        activity1.mVisibleRequested = false;
+
+        final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
+        activity2.setVisible(false);
+        activity2.mVisibleRequested = true;
+
+        final ArraySet<ActivityRecord> opening = new ArraySet<>();
+        opening.add(activity2);
+        final ArraySet<ActivityRecord> closing = new ArraySet<>();
+        closing.add(activity1);
+
+        // Promote animation targets up to leaf Task level because there's only one TaskFragment in
+        // the Task.
+        assertEquals(new ArraySet<>(new WindowContainer[]{activity2.getTask()}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, true /* visible */));
+        assertEquals(new ArraySet<>(new WindowContainer[]{task1}),
+                AppTransitionController.getAnimationTargets(
+                        opening, closing, false /* visible */));
+    }
+
     static class TestRemoteAnimationRunner implements IRemoteAnimationRunner {
         @Override
         public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
@@ -619,4 +765,252 @@
                 mAppTransitionController.getRemoteAnimationOverride(
                         activity, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>()));
     }
+
+    @Test
+    public void testOverrideTaskFragmentAdapter_overrideWithEmbeddedActivity() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                new TestRemoteAnimationRunner(), 10, 1);
+        setupTaskFragmentRemoteAnimation(organizer, adapter);
+
+        // Create a TaskFragment with embedded activity.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(
+                createTask(mDisplayContent), organizer);
+        final ActivityRecord activity = taskFragment.getTopMostActivity();
+        activity.allDrawn = true;
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare a transition.
+        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
+
+        // Should be overridden.
+        verify(mDisplayContent.mAppTransition)
+                .overridePendingAppTransitionRemote(adapter, false /* sync */);
+    }
+
+    @Test
+    public void testOverrideTaskFragmentAdapter_overrideWithNonEmbeddedActivity() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                new TestRemoteAnimationRunner(), 10, 1);
+        setupTaskFragmentRemoteAnimation(organizer, adapter);
+
+        final Task task = createTask(mDisplayContent);
+        // Closing non-embedded activity.
+        final ActivityRecord closingActivity = createActivityRecord(task);
+        closingActivity.allDrawn = true;
+        // Opening TaskFragment with embedded activity.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
+        openingActivity.allDrawn = true;
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare a transition.
+        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
+
+        // Should be overridden.
+        verify(mDisplayContent.mAppTransition)
+                .overridePendingAppTransitionRemote(adapter, false /* sync */);
+    }
+
+    @Test
+    public void testOverrideTaskFragmentAdapter_overrideEmbeddedActivityWithDiffUid() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                new TestRemoteAnimationRunner(), 10, 1);
+        setupTaskFragmentRemoteAnimation(organizer, adapter);
+
+        final Task task = createTask(mDisplayContent);
+        // Closing TaskFragment with embedded activity.
+        final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord closingActivity = taskFragment1.getTopMostActivity();
+        closingActivity.allDrawn = true;
+        closingActivity.info.applicationInfo.uid = 12345;
+        // Opening TaskFragment with embedded activity with different UID.
+        final TaskFragment taskFragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord openingActivity = taskFragment2.getTopMostActivity();
+        openingActivity.info.applicationInfo.uid = 54321;
+        openingActivity.allDrawn = true;
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare a transition.
+        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1);
+
+        // Should be overridden.
+        verify(mDisplayContent.mAppTransition)
+                .overridePendingAppTransitionRemote(adapter, false /* sync */);
+    }
+
+    @Test
+    public void testOverrideTaskFragmentAdapter_noOverrideWithTwoApps() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                new TestRemoteAnimationRunner(), 10, 1);
+        setupTaskFragmentRemoteAnimation(organizer, adapter);
+
+        // Closing activity in Task1.
+        final ActivityRecord closingActivity = createActivityRecord(mDisplayContent);
+        closingActivity.allDrawn = true;
+        // Opening TaskFragment with embedded activity in Task2.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(
+                createTask(mDisplayContent), organizer);
+        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
+        openingActivity.allDrawn = true;
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare a transition for TaskFragment.
+        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
+
+        // Should not be overridden.
+        verify(mDisplayContent.mAppTransition, never())
+                .overridePendingAppTransitionRemote(adapter, false /* sync */);
+    }
+
+    @Test
+    public void testOverrideTaskFragmentAdapter_noOverrideNonEmbeddedActivityWithDiffUid() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                new TestRemoteAnimationRunner(), 10, 1);
+        setupTaskFragmentRemoteAnimation(organizer, adapter);
+
+        final Task task = createTask(mDisplayContent);
+        // Closing TaskFragment with embedded activity.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord closingActivity = taskFragment.getTopMostActivity();
+        closingActivity.allDrawn = true;
+        closingActivity.info.applicationInfo.uid = 12345;
+        // Opening non-embedded activity with different UID.
+        final ActivityRecord openingActivity = createActivityRecord(task);
+        openingActivity.info.applicationInfo.uid = 54321;
+        openingActivity.allDrawn = true;
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare a transition.
+        prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
+
+        // Should not be overridden
+        verify(mDisplayContent.mAppTransition, never())
+                .overridePendingAppTransitionRemote(adapter, false /* sync */);
+    }
+
+    @Test
+    public void testOverrideTaskFragmentAdapter_noOverrideWithWallpaper() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                new TestRemoteAnimationRunner(), 10, 1);
+        setupTaskFragmentRemoteAnimation(organizer, adapter);
+
+        // Create a TaskFragment with embedded activity.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(
+                createTask(mDisplayContent), organizer);
+        final ActivityRecord activity = taskFragment.getTopMostActivity();
+        activity.allDrawn = true;
+        // Set wallpaper as visible.
+        final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
+                mock(IBinder.class), true, mDisplayContent, true /* ownerCanManageAppTokens */);
+        spyOn(mDisplayContent.mWallpaperController);
+        doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible();
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare a transition.
+        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
+
+        // Should not be overridden when there is wallpaper in the transition.
+        verify(mDisplayContent.mAppTransition, never())
+                .overridePendingAppTransitionRemote(adapter, false /* sync */);
+    }
+
+    @Test
+    public void testTransitionGoodToGoForTaskFragments() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment changeTaskFragment =
+                createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(organizer)
+                .build();
+        changeTaskFragment.getTopMostActivity().allDrawn = true;
+        spyOn(mDisplayContent.mAppTransition);
+        spyOn(emptyTaskFragment);
+
+        prepareAndTriggerAppTransition(
+                null /* openingActivity */, null /* closingActivity*/, changeTaskFragment);
+
+        // Transition not ready because there is an empty non-finishing TaskFragment.
+        verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any());
+
+        doReturn(true).when(emptyTaskFragment).hasChild();
+        emptyTaskFragment.remove(false /* withTransition */, "test");
+
+        mDisplayContent.mAppTransitionController.handleAppTransitionReady();
+
+        // Transition ready because the empty (no running activity) TaskFragment is requested to be
+        // removed.
+        verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any());
+    }
+
+    @Test
+    public void testTransitionGoodToGoForTaskFragments_detachedApp() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment changeTaskFragment =
+                createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(organizer)
+                .build();
+        changeTaskFragment.getTopMostActivity().allDrawn = true;
+        // To make sure that having a detached activity won't cause any issue.
+        final ActivityRecord detachedActivity = createActivityRecord(task);
+        detachedActivity.removeImmediately();
+        assertNull(detachedActivity.getRootTask());
+        spyOn(mDisplayContent.mAppTransition);
+        spyOn(emptyTaskFragment);
+
+        prepareAndTriggerAppTransition(
+                null /* openingActivity */, detachedActivity, changeTaskFragment);
+
+        // Transition not ready because there is an empty non-finishing TaskFragment.
+        verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any());
+
+        doReturn(true).when(emptyTaskFragment).hasChild();
+        emptyTaskFragment.remove(false /* withTransition */, "test");
+
+        mDisplayContent.mAppTransitionController.handleAppTransitionReady();
+
+        // Transition ready because the empty (no running activity) TaskFragment is requested to be
+        // removed.
+        verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any());
+    }
+
+    /** Registers remote animation for the organizer. */
+    private void setupTaskFragmentRemoteAnimation(TaskFragmentOrganizer organizer,
+            RemoteAnimationAdapter adapter) {
+        final ITaskFragmentOrganizer iOrganizer =
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder());
+        final RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
+        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter);
+        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter);
+        definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, adapter);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(iOrganizer);
+        mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, definition);
+    }
+
+    private void prepareAndTriggerAppTransition(@Nullable ActivityRecord openingActivity,
+            @Nullable ActivityRecord closingActivity, @Nullable TaskFragment changingTaskFragment) {
+        if (openingActivity != null) {
+            mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0);
+            mDisplayContent.mOpeningApps.add(openingActivity);
+        }
+        if (closingActivity != null) {
+            mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CLOSE, 0);
+            mDisplayContent.mClosingApps.add(closingActivity);
+        }
+        if (changingTaskFragment != null) {
+            mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CHANGE, 0);
+            mDisplayContent.mChangingContainers.add(changingTaskFragment);
+        }
+        mDisplayContent.mAppTransitionController.handleAppTransitionReady();
+    }
 }
\ No newline at end of file
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index c3279bf..fb8bc7b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -18,11 +18,16 @@
 
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_UNSET;
 import static android.view.WindowManager.TRANSIT_OPEN;
 
@@ -30,22 +35,32 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
 
+import android.graphics.Rect;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
 import android.view.Display;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
 import android.view.WindowManager;
+import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentOrganizer;
 
 import androidx.test.filters.SmallTest;
 
@@ -82,8 +97,8 @@
         assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
                 AppTransitionController.getTransitCompatType(mDc.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null /* wallpaperTarget */, null /* oldWallpaper */,
-                        false /*skipAppTransitionAnimation*/));
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
     }
 
     @Test
@@ -97,8 +112,8 @@
         assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
                 AppTransitionController.getTransitCompatType(mDc.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null /* wallpaperTarget */, null /* oldWallpaper */,
-                        false /*skipAppTransitionAnimation*/));
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
     }
 
     @Test
@@ -112,8 +127,8 @@
         assertEquals(TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE,
                 AppTransitionController.getTransitCompatType(mDc.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null /* wallpaperTarget */, null /* oldWallpaper */,
-                        false /*skipAppTransitionAnimation*/));
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
     }
 
     @Test
@@ -127,8 +142,8 @@
         assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY,
                 AppTransitionController.getTransitCompatType(mDc.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null /* wallpaperTarget */, null /* oldWallpaper */,
-                        false /*skipAppTransitionAnimation*/));
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
     }
 
     @Test
@@ -142,8 +157,129 @@
         assertEquals(TRANSIT_OLD_UNSET,
                 AppTransitionController.getTransitCompatType(mDc.mAppTransition,
                         mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                        null /* wallpaperTarget */, null /* oldWallpaper */,
-                        true /*skipAppTransitionAnimation*/));
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, true /*skipAppTransitionAnimation*/));
+    }
+
+    @Test
+    public void testTaskChangeWindowingMode() {
+        final ActivityRecord activity = createActivityRecord(mDc);
+
+        mDc.prepareAppTransition(TRANSIT_OPEN);
+        mDc.prepareAppTransition(TRANSIT_CHANGE);
+        mDc.mOpeningApps.add(activity); // Make sure TRANSIT_CHANGE has the priority
+        mDc.mChangingContainers.add(activity.getTask());
+
+        assertEquals(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE,
+                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
+    }
+
+    @Test
+    public void testTaskFragmentChange() {
+        final ActivityRecord activity = createActivityRecord(mDc);
+        final TaskFragment taskFragment = new TaskFragment(mAtm, new Binder(),
+                true /* createdByOrganizer */, true /* isEmbedded */);
+        activity.getTask().addChild(taskFragment, POSITION_TOP);
+        activity.reparent(taskFragment, POSITION_TOP);
+
+        mDc.prepareAppTransition(TRANSIT_OPEN);
+        mDc.prepareAppTransition(TRANSIT_CHANGE);
+        mDc.mOpeningApps.add(activity); // Make sure TRANSIT_CHANGE has the priority
+        mDc.mChangingContainers.add(taskFragment);
+
+        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_CHANGE,
+                AppTransitionController.getTransitCompatType(mDc.mAppTransition,
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /*skipAppTransitionAnimation*/));
+    }
+
+    @Test
+    public void testTaskFragmentOpeningTransition() {
+        final ActivityRecord activity = createHierarchyForTaskFragmentTest(
+                false /* createEmbeddedTask */);
+        activity.setVisible(false);
+
+        mDisplayContent.prepareAppTransition(TRANSIT_OPEN);
+        mDisplayContent.mOpeningApps.add(activity);
+        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_OPEN,
+                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /* skipAppTransitionAnimation */));
+    }
+
+    @Test
+    public void testEmbeddedTaskOpeningTransition() {
+        final ActivityRecord activity = createHierarchyForTaskFragmentTest(
+                true /* createEmbeddedTask */);
+        activity.setVisible(false);
+
+        mDisplayContent.prepareAppTransition(TRANSIT_OPEN);
+        mDisplayContent.mOpeningApps.add(activity);
+        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_OPEN,
+                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /* skipAppTransitionAnimation */));
+    }
+
+    @Test
+    public void testTaskFragmentClosingTransition() {
+        final ActivityRecord activity = createHierarchyForTaskFragmentTest(
+                false /* createEmbeddedTask */);
+        activity.setVisible(true);
+
+        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
+        mDisplayContent.mClosingApps.add(activity);
+        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_CLOSE,
+                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /* skipAppTransitionAnimation */));
+    }
+
+    @Test
+    public void testEmbeddedTaskClosingTransition() {
+        final ActivityRecord activity = createHierarchyForTaskFragmentTest(
+                true /* createEmbeddedTask */);
+        activity.setVisible(true);
+
+        mDisplayContent.prepareAppTransition(TRANSIT_CLOSE);
+        mDisplayContent.mClosingApps.add(activity);
+        assertEquals(TRANSIT_OLD_TASK_FRAGMENT_CLOSE,
+                AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition,
+                        mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
+                        mDisplayContent.mChangingContainers, null /* wallpaperTarget */,
+                        null /* oldWallpaper */, false /* skipAppTransitionAnimation */));
+    }
+
+    /**
+     * Creates a {@link Task} with two {@link TaskFragment TaskFragments}.
+     * The bottom TaskFragment is to prevent
+     * {@link AppTransitionController#getAnimationTargets(ArraySet, ArraySet, boolean) the animation
+     * target} to promote to Task or above.
+     *
+     * @param createEmbeddedTask {@code true} to create embedded Task for verified TaskFragment
+     * @return The Activity to be put in either opening or closing Activity
+     */
+    private ActivityRecord createHierarchyForTaskFragmentTest(boolean createEmbeddedTask) {
+        final Task parentTask = createTask(mDisplayContent);
+        final TaskFragment bottomTaskFragment = createTaskFragmentWithParentTask(parentTask,
+                false /* createEmbeddedTask */);
+        final ActivityRecord bottomActivity = bottomTaskFragment.getTopMostActivity();
+        bottomActivity.setOccludesParent(true);
+        bottomActivity.setVisible(true);
+
+        final TaskFragment verifiedTaskFragment = createTaskFragmentWithParentTask(parentTask,
+                createEmbeddedTask);
+        final ActivityRecord activity = verifiedTaskFragment.getTopMostActivity();
+        activity.setOccludesParent(true);
+
+        return activity;
     }
 
     @Test
@@ -262,6 +398,42 @@
                 mDc.mAppTransition.getAnimationStyleResId(attrs));
     }
 
+    @Test
+    public void testActivityRecordReparentToTaskFragment() {
+        final ActivityRecord activity = createActivityRecord(mDc);
+        final SurfaceControl activityLeash = mock(SurfaceControl.class);
+        activity.setVisibility(true);
+        activity.setSurfaceControl(activityLeash);
+        final Task task = activity.getTask();
+
+        // Add a TaskFragment of half of the Task size.
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final ITaskFragmentOrganizer iOrganizer =
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder());
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(iOrganizer);
+        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(organizer)
+                .build();
+        final Rect taskBounds = new Rect();
+        task.getBounds(taskBounds);
+        taskFragment.setBounds(0, 0, taskBounds.right / 2, taskBounds.bottom);
+        spyOn(taskFragment);
+
+        assertTrue(mDc.mChangingContainers.isEmpty());
+        assertFalse(mDc.mAppTransition.isTransitionSet());
+
+        // Schedule app transition when reparent activity to a TaskFragment of different size.
+        final Rect startBounds = new Rect(activity.getBounds());
+        activity.reparent(taskFragment, POSITION_TOP);
+
+        // It should transit at TaskFragment level with snapshot on the activity surface.
+        verify(taskFragment).initializeChangeTransition(activity.getBounds(), activityLeash);
+        assertTrue(mDc.mChangingContainers.contains(taskFragment));
+        assertTrue(mDc.mAppTransition.containsTransitRequest(TRANSIT_CHANGE));
+        assertEquals(startBounds, taskFragment.mSurfaceFreezer.mFreezeBounds);
+    }
+
     private class TestRemoteAnimationRunner implements IRemoteAnimationRunner {
         boolean mCancelled = false;
         @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
index fd562c3..ebefeaf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
@@ -75,13 +75,19 @@
 
     @Test
     public void clipAfterAnim_boundsLayerZBoosted() {
+        final Task task = mActivity.getTask();
+        final ActivityRecord topActivity = createActivityRecord(task);
+        task.assignChildLayers(mTransaction);
+
+        assertThat(topActivity.getLastLayer()).isGreaterThan(mActivity.getLastLayer());
+
         mActivity.mNeedsAnimationBoundsLayer = true;
         mActivity.mNeedsZBoost = true;
-
         mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
                 ANIMATION_TYPE_APP_TRANSITION);
+
         verify(mTransaction).setLayer(eq(mActivity.mAnimationBoundsLayer),
-                intThat(layer -> layer >= ActivityRecord.Z_BOOST_BASE));
+                intThat(layer -> layer > topActivity.getLastLayer()));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyBuilderTest.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyBuilderTest.java
index 31d4612..af21e02 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyBuilderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyBuilderTest.java
@@ -35,10 +35,10 @@
 import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST;
 import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_LAST;
 import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
+import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
 
 import static com.android.server.wm.DisplayArea.Type.ABOVE_TASKS;
 import static com.android.server.wm.DisplayAreaPolicyBuilder.Feature;
-import static com.android.server.wm.DisplayAreaPolicyBuilder.KEY_ROOT_DISPLAY_AREA_ID;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
index 4e4e0ed..1f123cc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
@@ -63,6 +63,7 @@
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.WindowManager;
+import android.window.DisplayAreaInfo;
 import android.window.IDisplayAreaOrganizer;
 
 import com.google.android.collect.Lists;
@@ -570,6 +571,31 @@
     }
 
     @Test
+    public void testGetDisplayAreaInfo() {
+        final DisplayArea<WindowContainer> displayArea = new DisplayArea<>(
+                mWm, BELOW_TASKS, "NewArea", FEATURE_VENDOR_FIRST);
+        mDisplayContent.addChild(displayArea, 0);
+        final DisplayAreaInfo info = displayArea.getDisplayAreaInfo();
+
+        assertThat(info.token).isEqualTo(displayArea.mRemoteToken.toWindowContainerToken());
+        assertThat(info.configuration).isEqualTo(displayArea.getConfiguration());
+        assertThat(info.displayId).isEqualTo(mDisplayContent.getDisplayId());
+        assertThat(info.featureId).isEqualTo(displayArea.mFeatureId);
+        assertThat(info.rootDisplayAreaId).isEqualTo(mDisplayContent.mFeatureId);
+
+        final TaskDisplayArea tda = mDisplayContent.getDefaultTaskDisplayArea();
+        final int tdaIndex = tda.getParent().mChildren.indexOf(tda);
+        final RootDisplayArea root =
+                new DisplayAreaGroup(mWm, "TestRoot", FEATURE_VENDOR_FIRST + 1);
+        mDisplayContent.addChild(root, tdaIndex + 1);
+        displayArea.reparent(root, 0);
+
+        final DisplayAreaInfo info2 = displayArea.getDisplayAreaInfo();
+
+        assertThat(info2.rootDisplayAreaId).isEqualTo(root.mFeatureId);
+    }
+
+    @Test
     public void testRegisterSameFeatureOrganizer_expectThrowsException() {
         final IDisplayAreaOrganizer mockDisplayAreaOrganizer = mock(IDisplayAreaOrganizer.class);
         final IBinder binder = mock(IBinder.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 51e289f..24bbf46 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -25,6 +25,7 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
 import static android.os.Build.VERSION_CODES.P;
 import static android.os.Build.VERSION_CODES.Q;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -67,6 +68,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.same;
@@ -107,9 +109,13 @@
 import android.app.servertransaction.FixedRotationAdjustmentsItem;
 import android.content.res.Configuration;
 import android.graphics.Insets;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.hardware.display.VirtualDisplay;
 import android.metrics.LogMaker;
+import android.os.Binder;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
@@ -122,6 +128,7 @@
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWindowManager;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -134,6 +141,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
+import com.android.server.LocalServices;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.wm.utils.WmDisplayCutout;
 
@@ -141,6 +149,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -592,7 +602,9 @@
         dc.setImeLayeringTarget(ws);
 
         // Adjust bounds so that matchesRootDisplayAreaBounds() returns false.
-        ws.mActivityRecord.getConfiguration().windowConfiguration.setBounds(new Rect(1, 1, 1, 1));
+        final Rect bounds = new Rect(dc.getBounds());
+        bounds.scale(0.5f);
+        ws.mActivityRecord.setBounds(bounds);
         assertFalse("matchesRootDisplayAreaBounds() should return false",
                 ws.matchesDisplayAreaBounds());
 
@@ -870,19 +882,6 @@
                 .setDisplayInfoOverrideFromWindowManager(dc.getDisplayId(), null);
     }
 
-    @UseTestDisplay
-    @Test
-    public void testClearLastFocusWhenReparentingFocusedWindow() {
-        final DisplayContent defaultDisplay = mWm.getDefaultDisplayContentLocked();
-        final WindowState window = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
-                defaultDisplay, "window");
-        defaultDisplay.mLastFocus = window;
-        mDisplayContent.mCurrentFocus = window;
-        mDisplayContent.reParentWindowToken(window.mToken);
-
-        assertNull(defaultDisplay.mLastFocus);
-    }
-
     @Test
     public void testGetPreferredOptionsPanelGravityFromDifferentDisplays() {
         final DisplayContent portraitDisplay = createNewDisplay();
@@ -1217,10 +1216,10 @@
         win.getAttrs().layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
         win.getAttrs().privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION;
         win.getAttrs().insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
-        final InsetsState requestedState = new InsetsState();
-        requestedState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
-        requestedState.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        win.updateRequestedVisibility(requestedState);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, false);
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        win.setRequestedVisibilities(requestedVisibilities);
         win.mActivityRecord.mTargetSdk = P;
 
         performLayout(dc);
@@ -1311,7 +1310,7 @@
     }
 
     @UseTestDisplay(addWindows = { W_ACTIVITY, W_WALLPAPER, W_STATUS_BAR, W_NAVIGATION_BAR,
-            W_NOTIFICATION_SHADE })
+            W_INPUT_METHOD, W_NOTIFICATION_SHADE })
     @Test
     public void testApplyTopFixedRotationTransform() {
         final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
@@ -1415,6 +1414,14 @@
         assertEquals("The process should receive rotated configuration for compatibility",
                 expectedProcConfig, app2.app.getConfiguration());
 
+        // If the rotated activity requests to show IME, the IME window should use the
+        // transformation from activity to lay out in the same orientation.
+        mDisplayContent.setImeLayeringTarget(mAppWindow);
+        LocalServices.getService(WindowManagerInternal.class).onToggleImeRequested(true /* show */,
+                app.token, app.token, mDisplayContent.mDisplayId);
+        assertTrue(mImeWindow.mToken.hasFixedRotationTransform());
+        assertTrue(mImeWindow.isAnimating(PARENTS, ANIMATION_TYPE_FIXED_TRANSFORM));
+
         // The fixed rotation transform can only be finished when all animation finished.
         doReturn(false).when(app2).isAnimating(anyInt(), anyInt());
         mDisplayContent.mAppTransition.notifyAppTransitionFinishedLocked(app2.token);
@@ -1530,7 +1537,7 @@
         unblockDisplayRotation(mDisplayContent);
         final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build();
         app.setVisible(false);
-        app.setState(Task.ActivityState.RESUMED, "test");
+        app.setState(ActivityRecord.State.RESUMED, "test");
         mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_OPEN);
         mDisplayContent.mOpeningApps.add(app);
         final int newOrientation = getRotatedOrientation(mDisplayContent);
@@ -1555,6 +1562,7 @@
         final ActivityRecord activity = createActivityRecord(mDisplayContent);
         final ActivityRecord recentsActivity = createActivityRecord(mDisplayContent);
         recentsActivity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
+        doReturn(mock(RecentsAnimationController.class)).when(mWm).getRecentsAnimationController();
 
         // Do not rotate if the recents animation is animating on top.
         mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity);
@@ -2251,6 +2259,183 @@
         assertNotEquals(imeMenuDialog, mDisplayContent.findFocusedWindow());
     }
 
+    @Test
+    public void testVirtualDisplayContent() {
+        MockitoSession mockSession = mockitoSession()
+                .initMocks(this)
+                .spyStatic(SurfaceControl.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+
+        // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
+        // mirror.
+        final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken();
+
+        // GIVEN SurfaceControl can successfully mirror the provided surface.
+        Point surfaceSize = new Point(
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(),
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height());
+        surfaceControlMirrors(surfaceSize);
+
+        // WHEN creating the DisplayContent for a new virtual display.
+        final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm,
+                mDisplayInfo).build();
+
+        // THEN mirroring is initiated for the default display's DisplayArea.
+        assertThat(virtualDisplay.mTokenToMirror).isEqualTo(tokenToMirror);
+
+        mockSession.finishMocking();
+    }
+
+    @Test
+    public void testVirtualDisplayContent_capturedAreaResized() {
+        MockitoSession mockSession = mockitoSession()
+                .initMocks(this)
+                .spyStatic(SurfaceControl.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+
+        // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
+        // mirror.
+        final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken();
+
+        // GIVEN SurfaceControl can successfully mirror the provided surface.
+        Point surfaceSize = new Point(
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(),
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height());
+        SurfaceControl mirroredSurface = surfaceControlMirrors(surfaceSize);
+
+        // WHEN creating the DisplayContent for a new virtual display.
+        final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm,
+                mDisplayInfo).build();
+
+        // THEN mirroring is initiated for the default display's DisplayArea.
+        assertThat(virtualDisplay.mTokenToMirror).isEqualTo(tokenToMirror);
+
+        float xScale = 0.7f;
+        float yScale = 2f;
+        Rect displayAreaBounds = new Rect(0, 0, Math.round(surfaceSize.x * xScale),
+                Math.round(surfaceSize.y * yScale));
+        virtualDisplay.updateMirroredSurface(mTransaction, displayAreaBounds, surfaceSize);
+
+        // THEN content in the captured DisplayArea is scaled to fit the surface size.
+        verify(mTransaction, atLeastOnce()).setMatrix(mirroredSurface, 1.0f / yScale, 0, 0,
+                1.0f / yScale);
+        // THEN captured content is positioned in the centre of the output surface.
+        float scaledWidth = displayAreaBounds.width() / xScale;
+        float xInset = (surfaceSize.x - scaledWidth) / 2;
+        verify(mTransaction, atLeastOnce()).setPosition(mirroredSurface, xInset, 0);
+
+        mockSession.finishMocking();
+    }
+
+    @Test
+    public void testVirtualDisplayContent_withoutSurface() {
+        MockitoSession mockSession = mockitoSession()
+                .initMocks(this)
+                .spyStatic(SurfaceControl.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+
+        // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
+        // mirror.
+        setUpDefaultTaskDisplayAreaWindowToken();
+
+        // GIVEN SurfaceControl does not mirror a null surface.
+        Point surfaceSize = new Point(
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(),
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height());
+
+        // GIVEN a new VirtualDisplay with an associated surface.
+        final VirtualDisplay display = createVirtualDisplay(surfaceSize, null /* surface */);
+        final int displayId = display.getDisplay().getDisplayId();
+        mWm.mRoot.onDisplayAdded(displayId);
+
+        // WHEN getting the DisplayContent for the new virtual display.
+        DisplayContent actualDC = mWm.mRoot.getDisplayContent(displayId);
+
+        // THEN mirroring is not started, since a null surface indicates the VirtualDisplay is off.
+        assertThat(actualDC.mTokenToMirror).isNull();
+
+        display.release();
+        mockSession.finishMocking();
+    }
+
+    @Test
+    public void testVirtualDisplayContent_withSurface() {
+        MockitoSession mockSession = mockitoSession()
+                .initMocks(this)
+                .spyStatic(SurfaceControl.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+
+        // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
+        // mirror.
+        final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken();
+
+        // GIVEN SurfaceControl can successfully mirror the provided surface.
+        Point surfaceSize = new Point(
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(),
+                mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height());
+        surfaceControlMirrors(surfaceSize);
+
+        // GIVEN a new VirtualDisplay with an associated surface.
+        final VirtualDisplay display = createVirtualDisplay(surfaceSize, new Surface());
+        final int displayId = display.getDisplay().getDisplayId();
+        mWm.mRoot.onDisplayAdded(displayId);
+
+        // WHEN getting the DisplayContent for the new virtual display.
+        DisplayContent actualDC = mWm.mRoot.getDisplayContent(displayId);
+
+        // THEN mirroring is initiated for the default display's DisplayArea.
+        assertThat(actualDC.mTokenToMirror).isEqualTo(tokenToMirror);
+
+        display.release();
+        mockSession.finishMocking();
+    }
+
+    private class TestToken extends Binder {
+    }
+
+    /**
+     * Creates a WindowToken associated with the default task DisplayArea, in order for that
+     * DisplayArea to be mirrored.
+     */
+    private IBinder setUpDefaultTaskDisplayAreaWindowToken() {
+        // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
+        // mirror.
+        final IBinder tokenToMirror = new TestToken();
+        doReturn(tokenToMirror).when(mWm.mDisplayManagerInternal).getWindowTokenClientToMirror(
+                anyInt());
+
+        // GIVEN the default task display area is represented by the WindowToken.
+        spyOn(mWm.mWindowContextListenerController);
+        doReturn(mDefaultDisplay.getDefaultTaskDisplayArea()).when(
+                mWm.mWindowContextListenerController).getContainer(any());
+        return tokenToMirror;
+    }
+
+    /**
+     * SurfaceControl successfully creates a mirrored surface of the given size.
+     */
+    private SurfaceControl surfaceControlMirrors(Point surfaceSize) {
+        // Do not set the parent, since the mirrored surface is the root of a new surface hierarchy.
+        SurfaceControl mirroredSurface = new SurfaceControl.Builder()
+                .setName("mirroredSurface")
+                .setBufferSize(surfaceSize.x, surfaceSize.y)
+                .setCallsite("mirrorSurface")
+                .build();
+        doReturn(mirroredSurface).when(() -> SurfaceControl.mirrorSurface(any()));
+        doReturn(surfaceSize).when(mWm.mDisplayManagerInternal).getDisplaySurfaceDefaultSize(
+                anyInt());
+        return mirroredSurface;
+    }
+
+    private VirtualDisplay createVirtualDisplay(Point size, Surface surface) {
+        return mWm.mDisplayManager.createVirtualDisplay("VirtualDisplay", size.x, size.y,
+                DisplayMetrics.DENSITY_140, surface, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR);
+    }
+
     private void removeRootTaskTests(Runnable runnable) {
         final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
         final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
@@ -2261,10 +2446,10 @@
                 ACTIVITY_TYPE_STANDARD, ON_TOP);
         final Task rootTask4 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
                 ACTIVITY_TYPE_STANDARD, ON_TOP);
-        final Task task1 = new TaskBuilder(mAtm.mTaskSupervisor).setParentTask(rootTask1).build();
-        final Task task2 = new TaskBuilder(mAtm.mTaskSupervisor).setParentTask(rootTask2).build();
-        final Task task3 = new TaskBuilder(mAtm.mTaskSupervisor).setParentTask(rootTask3).build();
-        final Task task4 = new TaskBuilder(mAtm.mTaskSupervisor).setParentTask(rootTask4).build();
+        final Task task1 = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask1).build();
+        final Task task2 = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask2).build();
+        final Task task3 = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask3).build();
+        final Task task4 = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask4).build();
 
         // Reordering root tasks while removing root tasks.
         doAnswer(invocation -> {
@@ -2329,7 +2514,7 @@
         assertThat("topToBottom", actualWindows, is(reverseList(expectedWindowsBottomToTop)));
     }
 
-    private static int getRotatedOrientation(DisplayContent dc) {
+    static int getRotatedOrientation(DisplayContent dc) {
         return dc.mBaseDisplayWidth > dc.mBaseDisplayHeight
                 ? SCREEN_ORIENTATION_PORTRAIT
                 : SCREEN_ORIENTATION_LANDSCAPE;
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
index 03304bb..4957ab9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
@@ -24,7 +24,7 @@
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewRootImpl.INSETS_LAYOUT_GENERALIZATION;
 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
@@ -50,13 +50,13 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.spy;
 import static org.testng.Assert.expectThrows;
 
 import android.graphics.Insets;
-import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.util.Pair;
@@ -65,11 +65,11 @@
 import android.view.DisplayInfo;
 import android.view.Gravity;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.PrivacyIndicatorBounds;
 import android.view.RoundedCorners;
 import android.view.WindowInsets.Side;
 import android.view.WindowInsets.Type;
-import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -109,18 +109,13 @@
         mWindow = spy(createWindow(null, TYPE_APPLICATION, "window"));
         // We only test window frames set by DisplayPolicy, so here prevents computeFrameLw from
         // changing those frames.
-        doNothing().when(mWindow).computeFrame();
-
-        final WindowManager.LayoutParams attrs = mWindow.mAttrs;
-        attrs.width = MATCH_PARENT;
-        attrs.height = MATCH_PARENT;
-        attrs.format = PixelFormat.TRANSLUCENT;
+        doNothing().when(mWindow).computeFrame(any());
 
         spyOn(mStatusBarWindow);
         spyOn(mNavBarWindow);
 
         // Disabling this call for most tests since it can override the systemUiFlags when called.
-        doReturn(false).when(mDisplayPolicy).updateSystemUiVisibilityLw();
+        doNothing().when(mDisplayPolicy).updateSystemBarAttributes();
 
         updateDisplayFrames();
     }
@@ -219,7 +214,7 @@
     }
 
     @Test
-    public void addingWindow_ignoresInsetsTypes_InWindowTypeWithPredefinedInsets() {
+    public void addingWindow_InWindowTypeWithPredefinedInsets() {
         mDisplayPolicy.removeWindowLw(mStatusBarWindow);  // Removes the existing one.
         WindowState win = createWindow(null, TYPE_STATUS_BAR, "StatusBar");
         win.mAttrs.providesInsetsTypes = new int[]{ITYPE_STATUS_BAR};
@@ -230,7 +225,13 @@
 
         InsetsSourceProvider provider =
                 mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_STATUS_BAR);
-        assertNotEquals(new Rect(0, 0, 500, 100), provider.getSource().getFrame());
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            // In the new flexible insets setup, the insets frame should always respect the window
+            // layout result.
+            assertEquals(new Rect(0, 0, 500, 100), provider.getSource().getFrame());
+        } else {
+            assertNotEquals(new Rect(0, 0, 500, 100), provider.getSource().getFrame());
+        }
     }
 
     @Test
@@ -478,9 +479,9 @@
         mWindow.mAttrs.setFitInsetsTypes(0 /* types */);
         mDisplayContent.getInsetsStateController().getRawInsetsState()
                 .getSource(InsetsState.ITYPE_STATUS_BAR).setVisible(false);
-        final InsetsState requestedState = new InsetsState();
-        requestedState.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        mWindow.updateRequestedVisibility(requestedState);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        mWindow.setRequestedVisibilities(requestedVisibilities);
         addWindowWithRawInsetsState(mWindow);
 
         mDisplayPolicy.layoutWindowLw(mWindow, null, mFrames);
@@ -498,9 +499,9 @@
         mWindow.mAttrs.setFitInsetsTypes(0 /* types */);
         mDisplayContent.getInsetsStateController().getRawInsetsState()
                 .getSource(InsetsState.ITYPE_STATUS_BAR).setVisible(false);
-        final InsetsState requestedState = new InsetsState();
-        requestedState.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        mWindow.updateRequestedVisibility(requestedState);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        mWindow.setRequestedVisibilities(requestedVisibilities);
         mWindow.mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
         addWindowWithRawInsetsState(mWindow);
 
@@ -733,10 +734,12 @@
 
     @Test
     public void testFixedRotationInsetsSourceFrame() {
+        mDisplayContent.mBaseDisplayHeight = DISPLAY_HEIGHT;
+        mDisplayContent.mBaseDisplayWidth = DISPLAY_WIDTH;
         doReturn((mDisplayContent.getRotation() + 1) % 4).when(mDisplayContent)
                 .rotationForActivityInDifferentOrientation(eq(mWindow.mActivityRecord));
-        mWindow.mAboveInsetsState.addSource(mDisplayContent.getInsetsStateController()
-                .getRawInsetsState().peekSource(ITYPE_STATUS_BAR));
+        mWindow.mAboveInsetsState.set(
+                mDisplayContent.getInsetsStateController().getRawInsetsState());
         final Rect frame = mDisplayPolicy.getInsetsPolicy().getInsetsForWindow(mWindow)
                 .getSource(ITYPE_STATUS_BAR).getFrame();
         mDisplayContent.rotateInDifferentOrientationIfNeeded(mWindow.mActivityRecord);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index b793be7..70aa2a2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -37,7 +37,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 
 import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_BOTTOM;
-import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_RIGHT;
 import static com.android.server.wm.utils.WmDisplayCutout.NO_CUTOUT;
 
 import static org.junit.Assert.assertEquals;
@@ -107,8 +106,7 @@
 
     @Test
     public void testChooseNavigationColorWindowLw() {
-        final WindowState opaque = createOpaqueFullscreen(false);
-
+        final WindowState candidate = createOpaqueFullscreen(false);
         final WindowState dimmingImTarget = createDimmingDialogWindow(true);
         final WindowState dimmingNonImTarget = createDimmingDialogWindow(false);
 
@@ -116,45 +114,51 @@
         final WindowState invisibleIme = createInputMethodWindow(false, true, false);
         final WindowState imeNonDrawNavBar = createInputMethodWindow(true, false, false);
 
-        // If everything is null, return null
+        // If everything is null, return null.
         assertNull(null, DisplayPolicy.chooseNavigationColorWindowLw(
-                null, null, null, NAV_BAR_BOTTOM));
+                null, null, NAV_BAR_BOTTOM));
 
-        assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, opaque, null, NAV_BAR_BOTTOM));
+        // If no IME windows, return candidate window.
+        assertEquals(candidate, DisplayPolicy.chooseNavigationColorWindowLw(
+                candidate, null, NAV_BAR_BOTTOM));
         assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, dimmingImTarget, null, NAV_BAR_BOTTOM));
+                dimmingImTarget, null, NAV_BAR_BOTTOM));
         assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, dimmingNonImTarget, null, NAV_BAR_BOTTOM));
+                dimmingNonImTarget, null, NAV_BAR_BOTTOM));
 
-        assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
-                null, null, visibleIme, NAV_BAR_BOTTOM));
-        assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
-                null, dimmingImTarget, visibleIme, NAV_BAR_BOTTOM));
+        // If IME is not visible, return candidate window.
+        assertEquals(null, DisplayPolicy.chooseNavigationColorWindowLw(
+                null, invisibleIme, NAV_BAR_BOTTOM));
+        assertEquals(candidate, DisplayPolicy.chooseNavigationColorWindowLw(
+                candidate, invisibleIme, NAV_BAR_BOTTOM));
+        assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+                dimmingImTarget, invisibleIme, NAV_BAR_BOTTOM));
         assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
-                null, dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM));
-        assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, opaque, visibleIme, NAV_BAR_BOTTOM));
-        assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, dimmingImTarget, visibleIme, NAV_BAR_BOTTOM));
-        assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM));
+                dimmingNonImTarget, invisibleIme, NAV_BAR_BOTTOM));
 
-        assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, opaque, invisibleIme, NAV_BAR_BOTTOM));
-        assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, opaque, invisibleIme, NAV_BAR_BOTTOM));
-        assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, opaque, visibleIme, NAV_BAR_RIGHT));
+        // If IME is visible, return candidate when the candidate window is not dimming.
+        assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
+                null, visibleIme, NAV_BAR_BOTTOM));
+        assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
+                candidate, visibleIme, NAV_BAR_BOTTOM));
+
+        // If IME is visible and the candidate window is dimming, checks whether the dimming window
+        // can be IME tartget or not.
+        assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw(
+                dimmingImTarget, visibleIme, NAV_BAR_BOTTOM));
+        assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
+                dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM));
 
         // Only IME windows that have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS should be navigation color
         // window.
-        assertEquals(opaque, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, opaque, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+        assertEquals(null, DisplayPolicy.chooseNavigationColorWindowLw(
+                null, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+        assertEquals(candidate, DisplayPolicy.chooseNavigationColorWindowLw(
+                candidate, imeNonDrawNavBar, NAV_BAR_BOTTOM));
         assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, dimmingImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+                dimmingImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
         assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw(
-                opaque, dimmingNonImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
+                dimmingNonImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
     }
 
     @UseTestDisplay(addWindows = { W_NAVIGATION_BAR })
@@ -182,59 +186,32 @@
 
         // If there is no window, APPEARANCE_LIGHT_NAVIGATION_BARS is not allowed.
         assertEquals(0,
-                displayPolicy.updateLightNavigationBarLw(
-                        APPEARANCE_LIGHT_NAVIGATION_BARS, null, null,
-                        null, null));
-
-        // Opaque top fullscreen window overrides APPEARANCE_LIGHT_NAVIGATION_BARS flag.
-        assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                0, opaqueDarkNavBar, opaqueDarkNavBar, null, opaqueDarkNavBar));
-        assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                APPEARANCE_LIGHT_NAVIGATION_BARS, opaqueDarkNavBar, opaqueDarkNavBar, null,
-                opaqueDarkNavBar));
-        assertEquals(APPEARANCE_LIGHT_NAVIGATION_BARS,
-                displayPolicy.updateLightNavigationBarLw(0, opaqueLightNavBar,
-                        opaqueLightNavBar, null, opaqueLightNavBar));
-        assertEquals(APPEARANCE_LIGHT_NAVIGATION_BARS,
-                displayPolicy.updateLightNavigationBarLw(APPEARANCE_LIGHT_NAVIGATION_BARS,
-                        opaqueLightNavBar, opaqueLightNavBar, null, opaqueLightNavBar));
+                displayPolicy.updateLightNavigationBarLw(APPEARANCE_LIGHT_NAVIGATION_BARS, null));
 
         // Dimming window clears APPEARANCE_LIGHT_NAVIGATION_BARS.
+        assertEquals(0, displayPolicy.updateLightNavigationBarLw(0, dimming));
         assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                0, opaqueDarkNavBar, dimming, null, dimming));
-        assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                0, opaqueLightNavBar, dimming, null, dimming));
-        assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                APPEARANCE_LIGHT_NAVIGATION_BARS, opaqueDarkNavBar, dimming, null, dimming));
-        assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                APPEARANCE_LIGHT_NAVIGATION_BARS, opaqueLightNavBar, dimming, null, dimming));
-        assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                APPEARANCE_LIGHT_NAVIGATION_BARS, opaqueLightNavBar, dimming, imeDrawLightNavBar,
-                dimming));
+                APPEARANCE_LIGHT_NAVIGATION_BARS, dimming));
 
-        // IME window clears APPEARANCE_LIGHT_NAVIGATION_BARS
+        // Control window overrides APPEARANCE_LIGHT_NAVIGATION_BARS flag.
+        assertEquals(0, displayPolicy.updateLightNavigationBarLw(0, opaqueDarkNavBar));
         assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                APPEARANCE_LIGHT_NAVIGATION_BARS, null, null, imeDrawDarkNavBar,
-                imeDrawDarkNavBar));
-
-        // Even if the top fullscreen has APPEARANCE_LIGHT_NAVIGATION_BARS, IME window wins.
-        assertEquals(0, displayPolicy.updateLightNavigationBarLw(
-                APPEARANCE_LIGHT_NAVIGATION_BARS, opaqueLightNavBar, opaqueLightNavBar,
-                imeDrawDarkNavBar, imeDrawDarkNavBar));
-
-        // IME window should be able to use APPEARANCE_LIGHT_NAVIGATION_BARS.
-        assertEquals(APPEARANCE_LIGHT_NAVIGATION_BARS,
-                displayPolicy.updateLightNavigationBarLw(0, opaqueDarkNavBar,
-                        opaqueDarkNavBar, imeDrawLightNavBar, imeDrawLightNavBar));
+                APPEARANCE_LIGHT_NAVIGATION_BARS, opaqueDarkNavBar));
+        assertEquals(APPEARANCE_LIGHT_NAVIGATION_BARS, displayPolicy.updateLightNavigationBarLw(
+                0, opaqueLightNavBar));
+        assertEquals(APPEARANCE_LIGHT_NAVIGATION_BARS, displayPolicy.updateLightNavigationBarLw(
+                APPEARANCE_LIGHT_NAVIGATION_BARS, opaqueLightNavBar));
     }
 
-    @UseTestDisplay(addWindows = W_ACTIVITY)
+    @UseTestDisplay(addWindows = {W_ACTIVITY, W_STATUS_BAR})
     @Test
     public void testComputeTopFullscreenOpaqueWindow() {
         final WindowManager.LayoutParams attrs = mAppWindow.mAttrs;
         attrs.x = attrs.y = 0;
         attrs.height = attrs.width = WindowManager.LayoutParams.MATCH_PARENT;
         final DisplayPolicy policy = mDisplayContent.getDisplayPolicy();
+        policy.addWindowLw(mStatusBarWindow, mStatusBarWindow.mAttrs);
+
         policy.applyPostLayoutPolicyLw(
                 mAppWindow, attrs, null /* attached */, null /* imeTarget */);
 
@@ -313,7 +290,9 @@
         displayInfo.logicalHeight = 2000;
         displayInfo.rotation = ROTATION_0;
 
-        displayPolicy.addWindowLw(mNavBarWindow, mNavBarWindow.mAttrs);
+        WindowManager.LayoutParams attrs = mNavBarWindow.mAttrs;
+        displayPolicy.addWindowLw(mNavBarWindow, attrs);
+        mNavBarWindow.setRequestedSize(attrs.width, attrs.height);
         mNavBarWindow.getControllableInsetProvider().setServerVisible(true);
         final InsetsState state = mDisplayContent.getInsetsStateController().getRawInsetsState();
         mImeWindow.mAboveInsetsState.set(state);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTestsBase.java
index 683ed88..3982a83 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTestsBase.java
@@ -58,8 +58,6 @@
     static final int DISPLAY_HEIGHT = 1000;
     static final int DISPLAY_DENSITY = 320;
 
-    static final int STATUS_BAR_HEIGHT = 10;
-    static final int NAV_BAR_HEIGHT = 15;
     static final int DISPLAY_CUTOUT_HEIGHT = 8;
     static final int IME_HEIGHT = 415;
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index 223dc31..a1e8ca4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -58,6 +58,7 @@
 import android.view.SurfaceSession;
 import android.view.View;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -70,6 +71,7 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
@@ -97,14 +99,20 @@
     static class TestDragDropController extends DragDropController {
         private Runnable mCloseCallback;
         boolean mDeferDragStateClosed;
+        boolean mIsAccessibilityDrag;
 
         TestDragDropController(WindowManagerService service, Looper looper) {
             super(service, looper);
         }
 
         void setOnClosedCallbackLocked(Runnable runnable) {
-            assertTrue(dragDropActiveLocked());
-            mCloseCallback = runnable;
+            if (mIsAccessibilityDrag) {
+                // Accessibility does not use animation
+                assertTrue(!dragDropActiveLocked());
+            } else {
+                assertTrue(dragDropActiveLocked());
+                mCloseCallback = runnable;
+            }
         }
 
         @Override
@@ -171,6 +179,10 @@
         }
         latch = new CountDownLatch(1);
         mTarget.setOnClosedCallbackLocked(latch::countDown);
+        if (mTarget.mIsAccessibilityDrag) {
+            mTarget.mIsAccessibilityDrag = false;
+            return;
+        }
         assertTrue(awaitInWmLock(() -> latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)));
     }
 
@@ -180,6 +192,12 @@
     }
 
     @Test
+    public void testA11yDragFlow() {
+        mTarget.mIsAccessibilityDrag = true;
+        doA11yDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0);
+    }
+
+    @Test
     public void testPerformDrag_NullDataWithGrantUri() {
         doDragAndDrop(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 0, 0);
     }
@@ -436,4 +454,22 @@
             appSession.kill();
         }
     }
+
+    private void doA11yDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
+        spyOn(mTarget);
+        AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class);
+        when(accessibilityManager.isEnabled()).thenReturn(true);
+        doReturn(accessibilityManager).when(mTarget).getAccessibilityManager();
+        startA11yDrag(flags, data, () -> {
+            boolean dropped = mTarget.dropForAccessibility(mWindow.mClient, dropX, dropY);
+            mToken = mWindow.mClient.asBinder();
+        });
+    }
+
+    private void startA11yDrag(int flags, ClipData data, Runnable r) {
+        mToken = mTarget.performDrag(0, 0, mWindow.mClient,
+                flags | View.DRAG_FLAG_ACCESSIBILITY_ACTION, null, 0, 0, 0, 0, 0, data);
+        assertNotNull(mToken);
+        r.run();
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index bf3ed69..07d467b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
@@ -48,6 +49,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 
 import androidx.test.filters.SmallTest;
 
@@ -80,7 +82,7 @@
     }
 
     @Test
-    public void testControlsForDispatch_dockedStackVisible() {
+    public void testControlsForDispatch_dockedTaskVisible() {
         addWindow(TYPE_STATUS_BAR, "statusBar");
         addWindow(TYPE_NAVIGATION_BAR, "navBar");
 
@@ -93,7 +95,20 @@
     }
 
     @Test
-    public void testControlsForDispatch_freeformStackVisible() {
+    public void testControlsForDispatch_multiWindowTaskVisible() {
+        addWindow(TYPE_STATUS_BAR, "statusBar");
+        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+
+        final WindowState win = createWindow(null, WINDOWING_MODE_MULTI_WINDOW,
+                ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app");
+        final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);
+
+        // The app must not control any system bars.
+        assertNull(controls);
+    }
+
+    @Test
+    public void testControlsForDispatch_freeformTaskVisible() {
         addWindow(TYPE_STATUS_BAR, "statusBar");
         addWindow(TYPE_NAVIGATION_BAR, "navBar");
 
@@ -101,18 +116,6 @@
                 ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app");
         final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);
 
-        // The app must not control any bars.
-        assertNull(controls);
-    }
-
-    @Test
-    public void testControlsForDispatch_dockedDividerControllerResizing() {
-        addWindow(TYPE_STATUS_BAR, "statusBar");
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
-        mDisplayContent.getDockedDividerController().setResizing(true);
-
-        final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();
-
         // The app must not control any system bars.
         assertNull(controls);
     }
@@ -179,9 +182,9 @@
 
         // Add a fullscreen (MATCH_PARENT x MATCH_PARENT) app window which hides status bar.
         final WindowState fullscreenApp = addWindow(TYPE_APPLICATION, "fullscreenApp");
-        final InsetsState requestedState = new InsetsState();
-        requestedState.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        fullscreenApp.updateRequestedVisibility(requestedState);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        fullscreenApp.setRequestedVisibilities(requestedVisibilities);
 
         // Add a non-fullscreen dialog window.
         final WindowState dialog = addWindow(TYPE_APPLICATION, "dialog");
@@ -214,9 +217,9 @@
 
         // Assume mFocusedWindow is updated but mTopFullscreenOpaqueWindowState hasn't.
         final WindowState newFocusedFullscreenApp = addWindow(TYPE_APPLICATION, "newFullscreenApp");
-        final InsetsState newRequestedState = new InsetsState();
-        newRequestedState.getSource(ITYPE_STATUS_BAR).setVisible(true);
-        newFocusedFullscreenApp.updateRequestedVisibility(newRequestedState);
+        final InsetsVisibilities newRequestedVisibilities = new InsetsVisibilities();
+        newRequestedVisibilities.setVisibility(ITYPE_STATUS_BAR, true);
+        newFocusedFullscreenApp.setRequestedVisibilities(newRequestedVisibilities);
         // Make sure status bar is hidden by previous insets state.
         mDisplayContent.getInsetsPolicy().updateBarControlTarget(fullscreenApp);
 
@@ -277,10 +280,10 @@
         doNothing().when(policy).startAnimation(anyBoolean(), any());
 
         // Make both system bars invisible.
-        final InsetsState requestedState = new InsetsState();
-        requestedState.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        requestedState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
-        mAppWindow.updateRequestedVisibility(requestedState);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, false);
+        mAppWindow.setRequestedVisibilities(requestedVisibilities);
         policy.updateBarControlTarget(mAppWindow);
         waitUntilWindowAnimatorIdle();
         assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState()
@@ -371,7 +374,10 @@
         assertTrue(state.getSource(ITYPE_STATUS_BAR).isVisible());
         assertTrue(state.getSource(ITYPE_NAVIGATION_BAR).isVisible());
 
-        mAppWindow.updateRequestedVisibility(state);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, true);
+        requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, true);
+        mAppWindow.setRequestedVisibilities(requestedVisibilities);
         policy.onInsetsModified(mAppWindow);
         waitUntilWindowAnimatorIdle();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
index c483ae9..2987f94 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
@@ -31,7 +31,7 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.InsetsSource;
-import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 
 import androidx.test.filters.SmallTest;
 
@@ -203,9 +203,9 @@
         statusBar.getFrame().set(0, 0, 500, 100);
         mProvider.setWindow(statusBar, null, null);
         mProvider.updateControlForTarget(target, false /* force */);
-        InsetsState state = new InsetsState();
-        state.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        target.updateRequestedVisibility(state);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        target.setRequestedVisibilities(requestedVisibilities);
         mProvider.updateClientVisibility(target);
         assertFalse(mSource.isVisible());
     }
@@ -216,9 +216,9 @@
         final WindowState target = createWindow(null, TYPE_APPLICATION, "target");
         statusBar.getFrame().set(0, 0, 500, 100);
         mProvider.setWindow(statusBar, null, null);
-        InsetsState state = new InsetsState();
-        state.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        target.updateRequestedVisibility(state);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        target.setRequestedVisibilities(requestedVisibilities);
         mProvider.updateClientVisibility(target);
         assertTrue(mSource.isVisible());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index 80961d7..f8c84df 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -48,6 +48,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 
 import androidx.test.filters.SmallTest;
 
@@ -174,10 +175,10 @@
         mImeWindow.setHasSurface(true);
         getController().getSourceProvider(ITYPE_IME).setWindow(mImeWindow, null, null);
         getController().onImeControlTargetChanged(mDisplayContent.getImeTarget(IME_TARGET_INPUT));
-        final InsetsState requestedState = new InsetsState();
-        requestedState.getSource(ITYPE_IME).setVisible(true);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_IME, true);
         mDisplayContent.getImeTarget(IME_TARGET_INPUT).getWindow()
-                .updateRequestedVisibility(requestedState);
+                .setRequestedVisibilities(requestedVisibilities);
         getController().onInsetsModified(mDisplayContent.getImeTarget(IME_TARGET_INPUT));
 
         // Send our spy window (app) into the system so that we can detect the invocation.
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
index 7cb7c79d..8a6db2c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
@@ -117,7 +117,7 @@
         Task rootTask = mTestDisplay.getDefaultTaskDisplayArea()
                 .createRootTask(TEST_WINDOWING_MODE, ACTIVITY_TYPE_STANDARD, /* onTop */ true);
         mTestTask = new TaskBuilder(mSupervisor).setComponent(TEST_COMPONENT)
-                .setParentTask(rootTask).build();
+                .setParentTaskFragment(rootTask).build();
         mTestTask.mUserId = TEST_USER_ID;
         mTestTask.mLastNonFullscreenBounds = TEST_BOUNDS;
         mTestTask.setHasBeenVisible(true);
@@ -353,7 +353,7 @@
         final Task anotherTaskOfTheSameUser = new TaskBuilder(mSupervisor)
                 .setComponent(ALTERNATIVE_COMPONENT)
                 .setUserId(TEST_USER_ID)
-                .setParentTask(stack)
+                .setParentTaskFragment(stack)
                 .build();
         anotherTaskOfTheSameUser.setWindowingMode(WINDOWING_MODE_FREEFORM);
         anotherTaskOfTheSameUser.setBounds(200, 300, 400, 500);
@@ -365,7 +365,7 @@
         final Task anotherTaskOfDifferentUser = new TaskBuilder(mSupervisor)
                 .setComponent(TEST_COMPONENT)
                 .setUserId(ALTERNATIVE_USER_ID)
-                .setParentTask(stack)
+                .setParentTaskFragment(stack)
                 .build();
         anotherTaskOfDifferentUser.setWindowingMode(WINDOWING_MODE_FREEFORM);
         anotherTaskOfDifferentUser.setBounds(300, 400, 500, 600);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
index 647a898..1e86522 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
@@ -62,7 +62,8 @@
         mSurfaces = new SurfaceControlMocker();
         mLetterbox = new Letterbox(mSurfaces, StubTransaction::new,
                 () -> mAreCornersRounded, () -> Color.valueOf(mColor),
-                () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha);
+                () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha,
+                /* doubleTapCallback= */ x -> {});
         mTransaction = spy(StubTransaction.class);
     }
 
@@ -200,28 +201,37 @@
         assertTrue(mLetterbox.needsApplySurfaceChanges());
 
         mLetterbox.applySurfaceChanges(mTransaction);
-        verify(mTransaction).setAlpha(mSurfaces.top, mDarkScrimAlpha);
+        verify(mTransaction).setAlpha(mSurfaces.fullWindowSurface, mDarkScrimAlpha);
     }
 
     @Test
-    public void testApplySurfaceChanges_cornersNotRounded_surfaceBehindNotCreated() {
+    public void testApplySurfaceChanges_cornersNotRounded_surfaceFullWindowSurfaceNotCreated() {
         mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
         mLetterbox.applySurfaceChanges(mTransaction);
 
-        assertNull(mSurfaces.behind);
+        assertNull(mSurfaces.fullWindowSurface);
     }
 
     @Test
-    public void testApplySurfaceChanges_cornersRounded_surfaceBehindCreated() {
+    public void testApplySurfaceChanges_cornersRounded_surfaceFullWindowSurfaceCreated() {
         mAreCornersRounded = true;
         mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
         mLetterbox.applySurfaceChanges(mTransaction);
 
-        assertNotNull(mSurfaces.behind);
+        assertNotNull(mSurfaces.fullWindowSurface);
     }
 
     @Test
-    public void testNotIntersectsOrFullyContains_cornersRounded_doesNotCheckSurfaceBehind() {
+    public void testApplySurfaceChanges_wallpaperBackground_surfaceFullWindowSurfaceCreated() {
+        mHasWallpaperBackground = true;
+        mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
+        mLetterbox.applySurfaceChanges(mTransaction);
+
+        assertNotNull(mSurfaces.fullWindowSurface);
+    }
+
+    @Test
+    public void testNotIntersectsOrFullyContains_cornersRounded() {
         mAreCornersRounded = true;
         mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(0, 0));
         mLetterbox.applySurfaceChanges(mTransaction);
@@ -249,8 +259,8 @@
         public SurfaceControl right;
         private SurfaceControl.Builder mBottomBuilder;
         public SurfaceControl bottom;
-        private SurfaceControl.Builder mBehindBuilder;
-        public SurfaceControl behind;
+        private SurfaceControl.Builder mFullWindowSurfaceBuilder;
+        public SurfaceControl fullWindowSurface;
 
         @Override
         public SurfaceControl.Builder get() {
@@ -265,8 +275,8 @@
                     mRightBuilder = (SurfaceControl.Builder) i.getMock();
                 } else if (((String) i.getArgument(0)).contains("bottom")) {
                     mBottomBuilder = (SurfaceControl.Builder) i.getMock();
-                } else if (((String) i.getArgument(0)).contains("behind")) {
-                    mBehindBuilder = (SurfaceControl.Builder) i.getMock();
+                } else if (((String) i.getArgument(0)).contains("fullWindow")) {
+                    mFullWindowSurfaceBuilder = (SurfaceControl.Builder) i.getMock();
                 }
                 return i.getMock();
             });
@@ -281,8 +291,8 @@
                     right = control;
                 } else if (i.getMock() == mBottomBuilder) {
                     bottom = control;
-                } else if (i.getMock() == mBehindBuilder) {
-                    behind = control;
+                } else if (i.getMock() == mFullWindowSurfaceBuilder) {
+                    fullWindowSurface = control;
                 }
                 return control;
             }).when(builder).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/PossibleDisplayInfoMapperTests.java b/services/tests/wmtests/src/com/android/server/wm/PossibleDisplayInfoMapperTests.java
new file mode 100644
index 0000000..6e00568
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/PossibleDisplayInfoMapperTests.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.FLAG_PRESENTATION;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+import android.view.DisplayInfo;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+
+/**
+ * Tests for {@link PossibleDisplayInfoMapper}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:PossibleDisplayInfoMapperTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class PossibleDisplayInfoMapperTests extends WindowTestsBase {
+
+    private PossibleDisplayInfoMapper mDisplayInfoMapper;
+    private final Set<DisplayInfo> mPossibleDisplayInfo = new ArraySet<>();
+    private DisplayInfo mDefaultDisplayInfo;
+    private DisplayInfo mSecondDisplayInfo;
+
+    @Before
+    public void setUp() throws Exception {
+        mDisplayInfoMapper = mWm.mPossibleDisplayInfoMapper;
+        final DisplayInfo baseDisplayInfo = mWm.mRoot.getDisplayContent(
+                DEFAULT_DISPLAY).getDisplayInfo();
+        when(mWm.mDisplayManagerInternal.getPossibleDisplayInfo(anyInt())).thenReturn(
+                mPossibleDisplayInfo);
+
+        mDefaultDisplayInfo = new DisplayInfo(baseDisplayInfo);
+        initializeDisplayInfo(mDefaultDisplayInfo, DEFAULT_DISPLAY, new Rect(0, 0, 500, 800));
+        mSecondDisplayInfo = new DisplayInfo(baseDisplayInfo);
+        // Use the same display id for any display in the same group, due to the assumption that
+        // any display in the same grouped can be swapped out for each other (while maintaining the
+        // display id).
+        initializeDisplayInfo(mSecondDisplayInfo, DEFAULT_DISPLAY, new Rect(0, 0, 600, 1600));
+        mSecondDisplayInfo.flags |= FLAG_PRESENTATION;
+    }
+
+    @Test
+    public void testInitialization_isEmpty() {
+        // Empty after initializing.
+        assertThat(mDisplayInfoMapper.getPossibleDisplayInfos(DEFAULT_DISPLAY)).isEmpty();
+
+        // Still empty after updating.
+        mDisplayInfoMapper.updatePossibleDisplayInfos(DEFAULT_DISPLAY);
+        assertThat(mDisplayInfoMapper.getPossibleDisplayInfos(DEFAULT_DISPLAY)).isEmpty();
+    }
+
+    @Test
+    public void testUpdatePossibleDisplayInfos_singleDisplay() {
+        mPossibleDisplayInfo.add(mDefaultDisplayInfo);
+        mDisplayInfoMapper.updatePossibleDisplayInfos(DEFAULT_DISPLAY);
+
+        Set<DisplayInfo> displayInfos = mDisplayInfoMapper.getPossibleDisplayInfos(DEFAULT_DISPLAY);
+        // An entry for each possible rotation, for a display that can be in a single state.
+        assertThat(displayInfos.size()).isEqualTo(4);
+        assertPossibleDisplayInfoEntries(displayInfos, mDefaultDisplayInfo);
+    }
+
+    @Test
+    public void testUpdatePossibleDisplayInfos_secondDisplayAdded_sameGroup() {
+        mPossibleDisplayInfo.add(mDefaultDisplayInfo);
+        mDisplayInfoMapper.updatePossibleDisplayInfos(DEFAULT_DISPLAY);
+
+        assertThat(mDisplayInfoMapper.getPossibleDisplayInfos(DEFAULT_DISPLAY).size()).isEqualTo(4);
+
+        // Add another display layout to the set of supported states.
+        mPossibleDisplayInfo.add(mSecondDisplayInfo);
+        mDisplayInfoMapper.updatePossibleDisplayInfos(DEFAULT_DISPLAY);
+
+        Set<DisplayInfo> displayInfos = mDisplayInfoMapper.getPossibleDisplayInfos(DEFAULT_DISPLAY);
+        Set<DisplayInfo> defaultDisplayInfos = new ArraySet<>();
+        Set<DisplayInfo> secondDisplayInfos = new ArraySet<>();
+        for (DisplayInfo di : displayInfos) {
+            if ((di.flags & FLAG_PRESENTATION) != 0) {
+                secondDisplayInfos.add(di);
+            } else {
+                defaultDisplayInfos.add(di);
+            }
+        }
+        // An entry for each possible rotation, for the default display.
+        assertThat(defaultDisplayInfos).hasSize(4);
+        assertPossibleDisplayInfoEntries(defaultDisplayInfos, mDefaultDisplayInfo);
+
+        // An entry for each possible rotation, for the second display.
+        assertThat(secondDisplayInfos).hasSize(4);
+        assertPossibleDisplayInfoEntries(secondDisplayInfos, mSecondDisplayInfo);
+    }
+
+    @Test
+    public void testUpdatePossibleDisplayInfos_secondDisplayAdded_differentGroup() {
+        mPossibleDisplayInfo.add(mDefaultDisplayInfo);
+        mDisplayInfoMapper.updatePossibleDisplayInfos(DEFAULT_DISPLAY);
+
+        assertThat(mDisplayInfoMapper.getPossibleDisplayInfos(DEFAULT_DISPLAY).size()).isEqualTo(4);
+
+        // Add another display to a different group.
+        mSecondDisplayInfo.displayId = DEFAULT_DISPLAY + 1;
+        mSecondDisplayInfo.displayGroupId = mDefaultDisplayInfo.displayGroupId + 1;
+        mPossibleDisplayInfo.add(mSecondDisplayInfo);
+        mDisplayInfoMapper.updatePossibleDisplayInfos(mSecondDisplayInfo.displayId);
+
+        Set<DisplayInfo> displayInfos = mDisplayInfoMapper.getPossibleDisplayInfos(DEFAULT_DISPLAY);
+        // An entry for each possible rotation, for the default display.
+        assertThat(displayInfos).hasSize(4);
+        assertPossibleDisplayInfoEntries(displayInfos, mDefaultDisplayInfo);
+
+        Set<DisplayInfo> secondStateEntries =
+                mDisplayInfoMapper.getPossibleDisplayInfos(mSecondDisplayInfo.displayId);
+        // An entry for each possible rotation, for the second display.
+        assertThat(secondStateEntries).hasSize(4);
+        assertPossibleDisplayInfoEntries(secondStateEntries, mSecondDisplayInfo);
+    }
+
+    private static void initializeDisplayInfo(DisplayInfo outDisplayInfo, int displayId,
+            Rect logicalBounds) {
+        outDisplayInfo.displayId = displayId;
+        outDisplayInfo.rotation = ROTATION_0;
+        outDisplayInfo.logicalWidth = logicalBounds.width();
+        outDisplayInfo.logicalHeight = logicalBounds.height();
+    }
+
+    private static void assertPossibleDisplayInfoEntries(Set<DisplayInfo> displayInfos,
+            DisplayInfo expectedDisplayInfo) {
+        boolean[] seenEveryRotation = new boolean[4];
+        for (DisplayInfo displayInfo : displayInfos) {
+            final int rotation = displayInfo.rotation;
+            seenEveryRotation[rotation] = true;
+            assertThat(displayInfo.displayId).isEqualTo(expectedDisplayInfo.displayId);
+            assertEqualsRotatedDisplayInfo(displayInfo, expectedDisplayInfo);
+        }
+        assertThat(seenEveryRotation).isEqualTo(new boolean[]{true, true, true, true});
+    }
+
+    private static void assertEqualsRotatedDisplayInfo(DisplayInfo actual, DisplayInfo expected) {
+        if (actual.rotation == ROTATION_0 || actual.rotation == ROTATION_180) {
+            assertThat(actual.logicalWidth).isEqualTo(expected.logicalWidth);
+            assertThat(actual.logicalHeight).isEqualTo(expected.logicalHeight);
+        } else {
+            assertThat(actual.logicalWidth).isEqualTo(expected.logicalHeight);
+            assertThat(actual.logicalHeight).isEqualTo(expected.logicalWidth);
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 5af6802..22e687a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -331,7 +331,7 @@
         // other task
         Task task1 = createTaskBuilder(".Task1")
                 .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
-                .setParentTask(mTaskContainer.getRootHomeTask()).build();
+                .setParentTaskFragment(mTaskContainer.getRootHomeTask()).build();
         Task task2 = createTaskBuilder(".Task1")
                 .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)
                 .build();
@@ -471,8 +471,8 @@
         final Task root = createTaskBuilder(".CreatedByOrganizerRoot").build();
         root.mCreatedByOrganizer = true;
         // Add organized and non-organized child.
-        final Task child1 = createTaskBuilder(".Task1").setParentTask(root).build();
-        final Task child2 = createTaskBuilder(".Task2").setParentTask(root).build();
+        final Task child1 = createTaskBuilder(".Task1").setParentTaskFragment(root).build();
+        final Task child2 = createTaskBuilder(".Task2").setParentTaskFragment(root).build();
         doReturn(true).when(child1).isOrganized();
         doReturn(false).when(child2).isOrganized();
         mRecentTasks.add(root);
@@ -508,7 +508,8 @@
         // tasks because their intents are identical.
         mRecentTasks.add(task1);
         // Go home to trigger the removal of untracked tasks.
-        mRecentTasks.add(createTaskBuilder(".Home").setParentTask(mTaskContainer.getRootHomeTask())
+        mRecentTasks.add(createTaskBuilder(".Home")
+                .setParentTaskFragment(mTaskContainer.getRootHomeTask())
                 .build());
         triggerIdleToTrim();
 
@@ -675,7 +676,7 @@
     public void testVisibleTasks_excludedFromRecents_firstTaskNotVisible() {
         // Create some set of tasks, some of which are visible and some are not
         Task homeTask = createTaskBuilder("com.android.pkg1", ".HomeTask")
-                .setParentTask(mTaskContainer.getRootHomeTask())
+                .setParentTaskFragment(mTaskContainer.getRootHomeTask())
                 .build();
         homeTask.mUserSetupComplete = true;
         mRecentTasks.add(homeTask);
@@ -696,7 +697,7 @@
         t1.mUserSetupComplete = true;
         mRecentTasks.add(t1);
         Task homeTask = createTaskBuilder("com.android.pkg1", ".HomeTask")
-                .setParentTask(mTaskContainer.getRootHomeTask())
+                .setParentTaskFragment(mTaskContainer.getRootHomeTask())
                 .build();
         homeTask.mUserSetupComplete = true;
         mRecentTasks.add(homeTask);
@@ -788,6 +789,19 @@
     }
 
     @Test
+    public void testVisibleEmbeddedTask_expectNotVisible() {
+        Task task = createTaskBuilder(".Task")
+                .setFlags(FLAG_ACTIVITY_NEW_TASK)
+                .build();
+        doReturn(true).when(task).isEmbedded();
+        mRecentTasks.add(task);
+
+        assertThat(mCallbacksRecorder.mAdded).hasSize(1);
+        assertFalse("embedded task should not be visible recents",
+                mRecentTasks.isVisibleRecentTask(task));
+    }
+
+    @Test
     public void testFreezeTaskListOrder_reorderExistingTask() {
         // Add some tasks
         mRecentTasks.add(mTasks.get(0));
@@ -859,6 +873,40 @@
     }
 
     @Test
+    public void testFreezeTaskListOrder_replaceTask() {
+        // Create two tasks with the same affinity
+        Task affinityTask1 = createTaskBuilder(".AffinityTask1")
+                .setFlags(FLAG_ACTIVITY_NEW_TASK)
+                .build();
+        Task affinityTask2 = createTaskBuilder(".AffinityTask2")
+                .setFlags(FLAG_ACTIVITY_NEW_TASK)
+                .build();
+        affinityTask2.affinity = affinityTask1.affinity = "affinity";
+
+        // Add some tasks
+        mRecentTasks.add(mTasks.get(0));
+        mRecentTasks.add(affinityTask1);
+        mRecentTasks.add(mTasks.get(1));
+        mCallbacksRecorder.clear();
+
+        // Freeze the list
+        mRecentTasks.setFreezeTaskListReordering();
+        assertTrue(mRecentTasks.isFreezeTaskListReorderingSet());
+
+        // Add the affinity task
+        mRecentTasks.add(affinityTask2);
+
+        assertRecentTasksOrder(mTasks.get(1),
+                affinityTask2,
+                mTasks.get(0));
+
+        assertThat(mCallbacksRecorder.mAdded).hasSize(1);
+        assertThat(mCallbacksRecorder.mAdded).contains(affinityTask2);
+        assertThat(mCallbacksRecorder.mRemoved).hasSize(1);
+        assertThat(mCallbacksRecorder.mRemoved).contains(affinityTask1);
+    }
+
+    @Test
     public void testFreezeTaskListOrder_timeout() {
         // Add some tasks
         mRecentTasks.add(mTasks.get(0));
@@ -902,10 +950,10 @@
 
         // Add a number of tasks (beyond the max) but ensure that nothing is trimmed because all
         // the tasks belong in stacks above the home stack
-        mRecentTasks.add(createTaskBuilder(".HomeTask1").setParentTask(homeStack).build());
-        mRecentTasks.add(createTaskBuilder(".Task1").setParentTask(aboveHomeStack).build());
-        mRecentTasks.add(createTaskBuilder(".Task2").setParentTask(aboveHomeStack).build());
-        mRecentTasks.add(createTaskBuilder(".Task3").setParentTask(aboveHomeStack).build());
+        mRecentTasks.add(createTaskBuilder(".HomeTask1").setParentTaskFragment(homeStack).build());
+        mRecentTasks.add(createTaskBuilder(".Task1").setParentTaskFragment(aboveHomeStack).build());
+        mRecentTasks.add(createTaskBuilder(".Task2").setParentTaskFragment(aboveHomeStack).build());
+        mRecentTasks.add(createTaskBuilder(".Task3").setParentTaskFragment(aboveHomeStack).build());
 
         triggerTrimAndAssertNoTasksTrimmed();
     }
@@ -923,11 +971,11 @@
         // Add a number of tasks (beyond the max) but ensure that only the task in the stack behind
         // the home stack is trimmed once a new task is added
         final Task behindHomeTask = createTaskBuilder(".Task1")
-                .setParentTask(behindHomeStack)
+                .setParentTaskFragment(behindHomeStack)
                 .build();
         mRecentTasks.add(behindHomeTask);
-        mRecentTasks.add(createTaskBuilder(".HomeTask1").setParentTask(homeStack).build());
-        mRecentTasks.add(createTaskBuilder(".Task2").setParentTask(aboveHomeStack).build());
+        mRecentTasks.add(createTaskBuilder(".HomeTask1").setParentTaskFragment(homeStack).build());
+        mRecentTasks.add(createTaskBuilder(".Task2").setParentTaskFragment(aboveHomeStack).build());
 
         triggerTrimAndAssertTrimmed(behindHomeTask);
     }
@@ -943,10 +991,12 @@
 
         // Add a number of tasks (beyond the max) on each display, ensure that the tasks are not
         // removed
-        mRecentTasks.add(createTaskBuilder(".HomeTask1").setParentTask(homeTask).build());
-        mRecentTasks.add(createTaskBuilder(".Task1").setParentTask(otherDisplayRootTask).build());
-        mRecentTasks.add(createTaskBuilder(".Task2").setParentTask(otherDisplayRootTask).build());
-        mRecentTasks.add(createTaskBuilder(".HomeTask2").setParentTask(homeTask).build());
+        mRecentTasks.add(createTaskBuilder(".HomeTask1").setParentTaskFragment(homeTask).build());
+        mRecentTasks.add(createTaskBuilder(".Task1").setParentTaskFragment(otherDisplayRootTask)
+                .build());
+        mRecentTasks.add(createTaskBuilder(".Task2").setParentTaskFragment(otherDisplayRootTask)
+                .build());
+        mRecentTasks.add(createTaskBuilder(".HomeTask2").setParentTaskFragment(homeTask).build());
 
         triggerTrimAndAssertNoTasksTrimmed();
     }
@@ -976,7 +1026,7 @@
         Task t1 = createTaskBuilder("com.android.pkg1", ".Task1").build();
         mRecentTasks.add(t1);
         mRecentTasks.add(createTaskBuilder("com.android.pkg1", ".HomeTask")
-                .setParentTask(mTaskContainer.getRootHomeTask()).build());
+                .setParentTaskFragment(mTaskContainer.getRootHomeTask()).build());
         Task t2 = createTaskBuilder("com.android.pkg2", ".Task2").build();
         mRecentTasks.add(t2);
         mRecentTasks.add(createTaskBuilder("com.android.pkg1", ".PipTask")
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
index d88fbee..9d2a691 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -41,8 +41,8 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
@@ -257,7 +257,7 @@
         verify(mMockRunner).onAnimationCanceled(null /* taskSnapshot */);
 
         // Simulate the app transition finishing
-        mController.mAppTransitionListener.onAppTransitionStartingLocked(false, 0, 0, 0);
+        mController.mAppTransitionListener.onAppTransitionStartingLocked(false, false, 0, 0, 0);
         verify(mAnimationCallbacks).onAnimationFinished(REORDER_KEEP_IN_PLACE, false);
     }
 
@@ -439,6 +439,22 @@
     }
 
     @Test
+    public void testCheckRotationAfterCleanup() {
+        mWm.setRecentsAnimationController(mController);
+        spyOn(mDisplayContent.mFixedRotationTransitionListener);
+        doReturn(true).when(mDisplayContent.mFixedRotationTransitionListener)
+                .isTopFixedOrientationRecentsAnimating();
+        // Rotation update is skipped while the recents animation is running.
+        assertFalse(mDisplayContent.getDisplayRotation().updateOrientation(DisplayContentTests
+                .getRotatedOrientation(mDefaultDisplay), false /* forceUpdate */));
+        final int prevRotation = mDisplayContent.getRotation();
+        mWm.cleanupRecentsAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION);
+        waitHandlerIdle(mWm.mH);
+        // The display should be updated to the changed orientation after the animation is finished.
+        assertNotEquals(mDisplayContent.getRotation(), prevRotation);
+    }
+
+    @Test
     public void testWallpaperHasFixedRotationApplied() {
         unblockDisplayRotation(mDefaultDisplay);
         mWm.setRecentsAnimationController(mController);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
index d017c19..1b19a28 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
@@ -31,8 +31,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
 import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static com.google.common.truth.Truth.assertThat;
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index b6cfa8e..08312ef 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -51,6 +51,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.IInterface;
+import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
@@ -274,6 +275,32 @@
     }
 
     @Test
+    public void testOpeningTaskWithTopFinishingActivity() {
+        final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "win");
+        final Task task = win.getTask();
+        final ActivityRecord topFinishing = new ActivityBuilder(mAtm).setTask(task).build();
+        // Now the task contains:
+        //     - Activity[1] (top, finishing, no window)
+        //     - Activity[0] (has window)
+        topFinishing.finishing = true;
+        spyOn(mDisplayContent.mAppTransition);
+        doReturn(mController).when(mDisplayContent.mAppTransition).getRemoteAnimationController();
+        task.applyAnimationUnchecked(null /* lp */, true /* enter */, TRANSIT_OLD_TASK_OPEN,
+                false /* isVoiceInteraction */, null /* sources */);
+        mController.goodToGo(TRANSIT_OLD_TASK_OPEN);
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+        final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
+                ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+        try {
+            verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_OPEN),
+                    appsCaptor.capture(), any(), any(), any());
+        } catch (RemoteException ignored) {
+        }
+        assertEquals(1, appsCaptor.getValue().length);
+        assertEquals(RemoteAnimationTarget.MODE_OPENING, appsCaptor.getValue()[0].mode);
+    }
+
+    @Test
     public void testChangeToSmallerSize() throws Exception {
         final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
         mDisplayContent.mChangingContainers.add(win.mActivityRecord);
@@ -314,7 +341,8 @@
             verify(mMockTransaction).setWindowCrop(
                     mMockLeash, app.startBounds.width(), app.startBounds.height());
             verify(mMockTransaction).setPosition(mMockThumbnailLeash, 0, 0);
-            verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, -1, -1);
+            verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, app.startBounds.width(),
+                    app.startBounds.height());
 
             finishedCaptor.getValue().onAnimationFinished();
             verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_WINDOW_ANIMATION),
@@ -367,7 +395,63 @@
             verify(mMockTransaction).setWindowCrop(
                     mMockLeash, app.startBounds.width(), app.startBounds.height());
             verify(mMockTransaction).setPosition(mMockThumbnailLeash, 0, 0);
-            verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, -1, -1);
+            verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, app.startBounds.width(),
+                    app.startBounds.height());
+
+            finishedCaptor.getValue().onAnimationFinished();
+            verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_WINDOW_ANIMATION),
+                    eq(record.mAdapter));
+            verify(mThumbnailFinishedCallback).onAnimationFinished(
+                    eq(ANIMATION_TYPE_WINDOW_ANIMATION), eq(record.mThumbnailAdapter));
+        } finally {
+            mDisplayContent.mChangingContainers.clear();
+        }
+    }
+
+    @Test
+    public void testChangeToDifferentPosition() throws Exception {
+        final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+        mDisplayContent.mChangingContainers.add(win.mActivityRecord);
+        try {
+            final RemoteAnimationRecord record = mController.createRemoteAnimationRecord(
+                    win.mActivityRecord, new Point(100, 100), null, new Rect(150, 150, 400, 400),
+                    new Rect(50, 100, 150, 150));
+            assertNotNull(record.mThumbnailAdapter);
+            ((AnimationAdapter) record.mAdapter)
+                    .startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION,
+                            mFinishedCallback);
+            ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash,
+                    mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback);
+            mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE);
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+            final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
+                    ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+            final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
+                    ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+            final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor =
+                    ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+            final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor =
+                    ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class);
+            verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE),
+                    appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(),
+                    finishedCaptor.capture());
+            assertEquals(1, appsCaptor.getValue().length);
+            final RemoteAnimationTarget app = appsCaptor.getValue()[0];
+            assertEquals(RemoteAnimationTarget.MODE_CHANGING, app.mode);
+            assertEquals(new Point(100, 100), app.position);
+            assertEquals(new Rect(150, 150, 400, 400), app.sourceContainerBounds);
+            assertEquals(new Rect(50, 100, 150, 150), app.startBounds);
+            assertEquals(mMockLeash, app.leash);
+            assertEquals(mMockThumbnailLeash, app.startLeash);
+            assertEquals(false, app.isTranslucent);
+            verify(mMockTransaction).setPosition(
+                    mMockLeash, app.position.x + app.startBounds.left - app.screenSpaceBounds.left,
+                    app.position.y + app.startBounds.top - app.screenSpaceBounds.top);
+            verify(mMockTransaction).setWindowCrop(
+                    mMockLeash, app.startBounds.width(), app.startBounds.height());
+            verify(mMockTransaction).setPosition(mMockThumbnailLeash, 0, 0);
+            verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, app.startBounds.width(),
+                    app.startBounds.height());
 
             finishedCaptor.getValue().onAnimationFinished();
             verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_WINDOW_ANIMATION),
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
index 0c6545c..030733b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
@@ -30,28 +30,28 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.ActivityRecord.State.DESTROYED;
+import static com.android.server.wm.ActivityRecord.State.DESTROYING;
+import static com.android.server.wm.ActivityRecord.State.FINISHING;
+import static com.android.server.wm.ActivityRecord.State.INITIALIZING;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_FREE_RESIZE;
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
-import static com.android.server.wm.Task.ActivityState.DESTROYED;
-import static com.android.server.wm.Task.ActivityState.DESTROYING;
-import static com.android.server.wm.Task.ActivityState.FINISHING;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
-import static com.android.server.wm.Task.ActivityState.STOPPED;
-import static com.android.server.wm.Task.ActivityState.STOPPING;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
 import static com.android.server.wm.Task.REPARENT_KEEP_ROOT_TASK_AT_FRONT;
 import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT;
-import static com.android.server.wm.Task.TASK_VISIBILITY_INVISIBLE;
-import static com.android.server.wm.Task.TASK_VISIBILITY_VISIBLE;
-import static com.android.server.wm.Task.TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
 import static com.android.server.wm.TaskDisplayArea.getRootTaskAbove;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE;
+import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
@@ -89,9 +89,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
 /**
  * Tests for the root {@link Task} behavior.
  *
@@ -194,7 +191,6 @@
         final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
 
         // Root task removal is deferred if one of its child is animating.
-        doReturn(true).when(rootTask).hasWindowsAlive();
         doReturn(rootTask).when(task).getAnimatingContainer(
                 eq(TRANSITION | CHILDREN), anyInt());
 
@@ -280,11 +276,11 @@
     public void testResumedActivity() {
         final ActivityRecord r = new ActivityBuilder(mAtm).setCreateTask(true).build();
         final Task task = r.getTask();
-        assertNull(task.getResumedActivity());
+        assertNull(task.getTopResumedActivity());
         r.setState(RESUMED, "testResumedActivity");
-        assertEquals(r, task.getResumedActivity());
+        assertEquals(r, task.getTopResumedActivity());
         r.setState(PAUSING, "testResumedActivity");
-        assertNull(task.getResumedActivity());
+        assertNull(task.getTopResumedActivity());
     }
 
     @Test
@@ -295,15 +291,15 @@
         final Task task = r.getTask();
         // Ensure moving task between two root tasks updates resumed activity
         r.setState(RESUMED, "testResumedActivityFromTaskReparenting");
-        assertEquals(r, rootTask.getResumedActivity());
+        assertEquals(r, rootTask.getTopResumedActivity());
 
         final Task destRootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
         task.reparent(destRootTask, true /* toTop */, REPARENT_KEEP_ROOT_TASK_AT_FRONT,
                 false /* animate */, true /* deferResume*/,
                 "testResumedActivityFromTaskReparenting");
 
-        assertNull(rootTask.getResumedActivity());
-        assertEquals(r, destRootTask.getResumedActivity());
+        assertNull(rootTask.getTopResumedActivity());
+        assertEquals(r, destRootTask.getTopResumedActivity());
     }
 
     @Test
@@ -314,15 +310,15 @@
         final Task task = r.getTask();
         // Ensure moving task between two root tasks updates resumed activity
         r.setState(RESUMED, "testResumedActivityFromActivityReparenting");
-        assertEquals(r, rootTask.getResumedActivity());
+        assertEquals(r, rootTask.getTopResumedActivity());
 
         final Task destRootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
         task.reparent(destRootTask, true /*toTop*/, REPARENT_MOVE_ROOT_TASK_TO_FRONT,
                 false /* animate */, false /* deferResume*/,
                 "testResumedActivityFromActivityReparenting");
 
-        assertNull(rootTask.getResumedActivity());
-        assertEquals(r, destRootTask.getResumedActivity());
+        assertNull(rootTask.getTopResumedActivity());
+        assertEquals(r, destRootTask.getTopResumedActivity());
     }
 
     @Test
@@ -333,7 +329,7 @@
 
         // Create primary splitscreen root task.
         final Task primarySplitScreen = new TaskBuilder(mAtm.mTaskSupervisor)
-                .setParentTask(organizer.mPrimary)
+                .setParentTaskFragment(organizer.mPrimary)
                 .setOnTop(true)
                 .build();
 
@@ -509,8 +505,8 @@
                 targetActivity);
         final ComponentName alias = new ComponentName(DEFAULT_COMPONENT_PACKAGE_NAME,
                 aliasActivity);
-        final Task parentTask = new TaskBuilder(mAtm.mTaskSupervisor).build();
-        final Task task = new TaskBuilder(mAtm.mTaskSupervisor).setParentTask(parentTask).build();
+        final Task parentTask = new TaskBuilder(mSupervisor).build();
+        final Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(parentTask).build();
         task.origActivity = alias;
         task.realActivity = target;
         new ActivityBuilder(mAtm).setComponent(target).setTask(task).setTargetActivity(
@@ -618,9 +614,9 @@
         doReturn(false).when(splitScreenSecondary2).isTranslucent(any());
         assertFalse(splitScreenSecondary.shouldBeVisible(null /* starting */));
         assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
-        assertEquals(TASK_VISIBILITY_INVISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                 splitScreenSecondary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 splitScreenSecondary2.getVisibility(null /* starting */));
 
         // First split-screen secondary should be visible behind another translucent split-screen
@@ -628,9 +624,9 @@
         doReturn(true).when(splitScreenSecondary2).isTranslucent(any());
         assertTrue(splitScreenSecondary.shouldBeVisible(null /* starting */));
         assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 splitScreenSecondary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 splitScreenSecondary2.getVisibility(null /* starting */));
 
         final Task assistantRootTask = createTaskForShouldBeVisibleTest(mDefaultTaskDisplayArea,
@@ -642,13 +638,13 @@
         assertFalse(splitScreenPrimary.shouldBeVisible(null /* starting */));
         assertFalse(splitScreenSecondary.shouldBeVisible(null /* starting */));
         assertFalse(splitScreenSecondary2.shouldBeVisible(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 assistantRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_INVISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                 splitScreenPrimary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_INVISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                 splitScreenSecondary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_INVISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                 splitScreenSecondary2.getVisibility(null /* starting */));
 
         // Split-screen root tasks should be visible behind a translucent fullscreen root task.
@@ -657,13 +653,13 @@
         assertTrue(splitScreenPrimary.shouldBeVisible(null /* starting */));
         assertTrue(splitScreenSecondary.shouldBeVisible(null /* starting */));
         assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 assistantRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 splitScreenPrimary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 splitScreenSecondary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 splitScreenSecondary2.getVisibility(null /* starting */));
 
         // Assistant root task shouldn't be visible behind translucent split-screen root task,
@@ -678,25 +674,25 @@
             assertTrue(assistantRootTask.shouldBeVisible(null /* starting */));
             assertFalse(splitScreenPrimary.shouldBeVisible(null /* starting */));
             assertFalse(splitScreenSecondary2.shouldBeVisible(null /* starting */));
-            assertEquals(TASK_VISIBILITY_VISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                     assistantRootTask.getVisibility(null /* starting */));
-            assertEquals(TASK_VISIBILITY_INVISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                     splitScreenPrimary.getVisibility(null /* starting */));
-            assertEquals(TASK_VISIBILITY_INVISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                     splitScreenSecondary.getVisibility(null /* starting */));
-            assertEquals(TASK_VISIBILITY_INVISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                     splitScreenSecondary2.getVisibility(null /* starting */));
         } else {
             assertFalse(assistantRootTask.shouldBeVisible(null /* starting */));
             assertTrue(splitScreenPrimary.shouldBeVisible(null /* starting */));
             assertTrue(splitScreenSecondary2.shouldBeVisible(null /* starting */));
-            assertEquals(TASK_VISIBILITY_INVISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                     assistantRootTask.getVisibility(null /* starting */));
-            assertEquals(TASK_VISIBILITY_VISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                     splitScreenPrimary.getVisibility(null /* starting */));
-            assertEquals(TASK_VISIBILITY_INVISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                     splitScreenSecondary.getVisibility(null /* starting */));
-            assertEquals(TASK_VISIBILITY_VISIBLE,
+            assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                     splitScreenSecondary2.getVisibility(null /* starting */));
         }
     }
@@ -707,45 +703,51 @@
                 WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME, true /* onTop */);
         final Task splitPrimary = createTaskForShouldBeVisibleTest(mDefaultTaskDisplayArea,
                 WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_UNDEFINED, true /* onTop */);
+        // Creating as two-level tasks so home task can be reparented to split-secondary root task.
         final Task splitSecondary = createTaskForShouldBeVisibleTest(mDefaultTaskDisplayArea,
-                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_UNDEFINED, true /* onTop */);
+                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_UNDEFINED, true /* onTop */,
+                true /* twoLevelTask */);
 
         doReturn(false).when(homeRootTask).isTranslucent(any());
         doReturn(false).when(splitPrimary).isTranslucent(any());
         doReturn(false).when(splitSecondary).isTranslucent(any());
 
-
         // Re-parent home to split secondary.
         homeRootTask.reparent(splitSecondary, POSITION_TOP);
         // Current tasks should be visible.
-        assertEquals(TASK_VISIBILITY_VISIBLE, splitPrimary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE, splitSecondary.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
+                splitPrimary.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
+                splitSecondary.getVisibility(null /* starting */));
         // Home task should still be visible even though it is a child of another visible task.
-        assertEquals(TASK_VISIBILITY_VISIBLE, homeRootTask.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
+                homeRootTask.getVisibility(null /* starting */));
 
 
         // Add fullscreen translucent task that partially occludes split tasks
         final Task translucentRootTask = createStandardRootTaskForVisibilityTest(
                 WINDOWING_MODE_FULLSCREEN, true /* translucent */);
         // Fullscreen translucent task should be visible
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 translucentRootTask.getVisibility(null /* starting */));
         // Split tasks should be visible behind translucent
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 splitPrimary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 splitSecondary.getVisibility(null /* starting */));
         // Home task should be visible behind translucent since its parent is visible behind
         // translucent.
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 homeRootTask.getVisibility(null /* starting */));
 
 
         // Hide split-secondary
         splitSecondary.setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, true /* set */);
         // Home split secondary and home task should be invisible.
-        assertEquals(TASK_VISIBILITY_INVISIBLE, splitSecondary.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_INVISIBLE, homeRootTask.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
+                splitSecondary.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
+                homeRootTask.getVisibility(null /* starting */));
     }
 
     @Test
@@ -757,9 +759,9 @@
                 createStandardRootTaskForVisibilityTest(WINDOWING_MODE_FULLSCREEN,
                         true /* translucent */);
 
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 bottomRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 translucentRootTask.getVisibility(null /* starting */));
     }
 
@@ -775,10 +777,12 @@
                 createStandardRootTaskForVisibilityTest(WINDOWING_MODE_FULLSCREEN,
                         false /* translucent */);
 
-        assertEquals(TASK_VISIBILITY_INVISIBLE, bottomRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_INVISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
+                bottomRootTask.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                 translucentRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE, opaqueRootTask.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
+                opaqueRootTask.getVisibility(null /* starting */));
     }
 
     @Test
@@ -793,10 +797,11 @@
                 createStandardRootTaskForVisibilityTest(WINDOWING_MODE_FULLSCREEN,
                         true /* translucent */);
 
-        assertEquals(TASK_VISIBILITY_INVISIBLE, bottomRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
+                bottomRootTask.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 opaqueRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 translucentRootTask.getVisibility(null /* starting */));
     }
 
@@ -809,9 +814,9 @@
                 createStandardRootTaskForVisibilityTest(WINDOWING_MODE_FULLSCREEN,
                         true /* translucent */);
 
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 bottomTranslucentRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 translucentRootTask.getVisibility(null /* starting */));
     }
 
@@ -824,9 +829,10 @@
                 createStandardRootTaskForVisibilityTest(WINDOWING_MODE_FULLSCREEN,
                         false /* translucent */);
 
-        assertEquals(TASK_VISIBILITY_INVISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_INVISIBLE,
                 bottomTranslucentRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE, opaqueRootTask.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
+                opaqueRootTask.getVisibility(null /* starting */));
     }
 
     @Test
@@ -840,16 +846,17 @@
         final Task pinnedRootTask = createTaskForShouldBeVisibleTest(mDefaultTaskDisplayArea,
                 WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, true /* onTop */);
 
-        assertEquals(TASK_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT,
                 bottomRootTask.getVisibility(null /* starting */));
-        assertEquals(TASK_VISIBILITY_VISIBLE,
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
                 translucentRootTask.getVisibility(null /* starting */));
         // Add an activity to the pinned root task so it isn't considered empty for visibility
         // check.
         final ActivityRecord pinnedActivity = new ActivityBuilder(mAtm)
                 .setTask(pinnedRootTask)
                 .build();
-        assertEquals(TASK_VISIBILITY_VISIBLE, pinnedRootTask.getVisibility(null /* starting */));
+        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
+                pinnedRootTask.getVisibility(null /* starting */));
     }
 
     @Test
@@ -1142,12 +1149,12 @@
         } else if (twoLevelTask) {
             task = new TaskBuilder(mSupervisor)
                     .setTaskDisplayArea(taskDisplayArea)
-                    .setWindowingMode(windowingMode)
                     .setActivityType(activityType)
                     .setOnTop(onTop)
                     .setCreateActivity(true)
                     .setCreateParentTask(true)
                     .build().getRootTask();
+            task.setWindowingMode(windowingMode);
         } else {
             task = new TaskBuilder(mSupervisor)
                     .setTaskDisplayArea(taskDisplayArea)
@@ -1301,9 +1308,9 @@
         final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
         topActivity.info.flags |= FLAG_RESUME_WHILE_PAUSING;
 
-        task.startPausingLocked(false /* uiSleeping */, topActivity,
+        task.startPausing(false /* uiSleeping */, topActivity,
                 "test");
-        verify(task).completePauseLocked(anyBoolean(), eq(topActivity));
+        verify(task).completePause(anyBoolean(), eq(topActivity));
     }
 
     @Test
@@ -1494,41 +1501,6 @@
     }
 
     @Test
-    public void testIterateOccludedActivity() {
-        final ArrayList<ActivityRecord> occludedActivities = new ArrayList<>();
-        final Consumer<ActivityRecord> handleOccludedActivity = occludedActivities::add;
-        final Task task = new TaskBuilder(mSupervisor).build();
-        final ActivityRecord bottomActivity = new ActivityBuilder(mAtm).setTask(task).build();
-        final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
-        // Top activity occludes bottom activity.
-        doReturn(true).when(task).shouldBeVisible(any());
-        assertTrue(topActivity.shouldBeVisible());
-        assertFalse(bottomActivity.shouldBeVisible());
-
-        task.forAllOccludedActivities(handleOccludedActivity);
-        assertThat(occludedActivities).containsExactly(bottomActivity);
-
-        // Top activity doesn't occlude parent, so the bottom activity is not occluded.
-        doReturn(false).when(topActivity).occludesParent();
-        assertTrue(bottomActivity.shouldBeVisible());
-
-        occludedActivities.clear();
-        task.forAllOccludedActivities(handleOccludedActivity);
-        assertThat(occludedActivities).isEmpty();
-
-        // A finishing activity should not occlude other activities behind.
-        final ActivityRecord finishingActivity = new ActivityBuilder(mAtm).setTask(task).build();
-        finishingActivity.finishing = true;
-        doCallRealMethod().when(finishingActivity).occludesParent();
-        assertTrue(topActivity.shouldBeVisible());
-        assertTrue(bottomActivity.shouldBeVisible());
-
-        occludedActivities.clear();
-        task.forAllOccludedActivities(handleOccludedActivity);
-        assertThat(occludedActivities).isEmpty();
-    }
-
-    @Test
     public void testClearUnknownAppVisibilityBehindFullscreenActivity() {
         final UnknownAppVisibilityController unknownAppVisibilityController =
                 mDefaultTaskDisplayArea.mDisplayContent.mUnknownAppVisibilityController;
@@ -1544,7 +1516,7 @@
             activities[i] = r;
             doReturn(null).when(mAtm).getProcessController(
                     eq(r.processName), eq(r.info.applicationInfo.uid));
-            r.setState(Task.ActivityState.INITIALIZING, "test");
+            r.setState(INITIALIZING, "test");
             // Ensure precondition that the activity is opaque.
             assertTrue(r.occludesParent());
             mSupervisor.startSpecificActivity(r, false /* andResume */,
@@ -1552,14 +1524,13 @@
         }
         mSupervisor.endDeferResume();
 
-        setBooted(mAtm);
         // 2 activities are started while keyguard is locked, so they are waiting to be resolved.
         assertFalse(unknownAppVisibilityController.allResolved());
 
-        // Assume the top activity is going to resume and
-        // {@link RootWindowContainer#cancelInitializingActivities} should clear the unknown
-        // visibility records that are occluded.
-        task.resumeTopActivityUncheckedLocked(null /* prev */, null /* options */);
+        // Any common path that updates activity visibility should clear the unknown visibility
+        // records that are no longer visible according to hierarchy.
+        task.ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
+                false /* preserveWindows */);
         // Assume the top activity relayouted, just remove it directly.
         unknownAppVisibilityController.appRemovedOrHidden(activities[1]);
         // All unresolved records should be removed.
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 9cf29d4..4069f0f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -31,13 +31,14 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.ActivityRecord.State.FINISHING;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE;
-import static com.android.server.wm.Task.ActivityState.FINISHING;
-import static com.android.server.wm.Task.ActivityState.PAUSED;
-import static com.android.server.wm.Task.ActivityState.PAUSING;
-import static com.android.server.wm.Task.ActivityState.STOPPED;
-import static com.android.server.wm.Task.ActivityState.STOPPING;
 import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -167,7 +168,7 @@
     @Test
     public void testTaskLayerRank() {
         final Task rootTask = new TaskBuilder(mSupervisor).build();
-        final Task task1 = new TaskBuilder(mSupervisor).setParentTask(rootTask).build();
+        final Task task1 = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
         new ActivityBuilder(mAtm).setTask(task1).build().mVisibleRequested = true;
         mWm.mRoot.rankTaskLayers();
 
@@ -365,7 +366,7 @@
         TaskDisplayArea defaultTaskDisplayArea = display.getDefaultTaskDisplayArea();
         doReturn(isFocusedTask ? task : null).when(defaultTaskDisplayArea).getFocusedRootTask();
         mRootWindowContainer.applySleepTokens(true);
-        verify(task, times(expectWakeFromSleep ? 1 : 0)).awakeFromSleepingLocked();
+        verify(task, times(expectWakeFromSleep ? 1 : 0)).awakeFromSleeping();
         verify(task, times(expectResumeTopActivity ? 1 : 0)).resumeTopActivityUncheckedLocked(
                 null /* target */, null /* targetOptions */);
     }
@@ -386,7 +387,7 @@
         // landscape and the portrait lockscreen is shown.
         activity.setLastReportedConfiguration(
                 new MergedConfiguration(mAtm.getGlobalConfiguration(), rotatedConfig));
-        activity.setState(Task.ActivityState.STOPPED, "sleep");
+        activity.setState(STOPPED, "sleep");
 
         display.setIsSleeping(true);
         doReturn(false).when(display).shouldSleep();
@@ -522,7 +523,8 @@
         final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
         final Task targetRootTask = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
                 ACTIVITY_TYPE_STANDARD, false /* onTop */);
-        final Task targetTask = new TaskBuilder(mSupervisor).setParentTask(targetRootTask).build();
+        final Task targetTask = new TaskBuilder(mSupervisor).setParentTaskFragment(targetRootTask)
+                .build();
 
         // Create Recents on secondary display.
         final TestDisplayContent secondDisplay = addNewDisplayContentAt(
@@ -557,8 +559,8 @@
         doReturn(rootTask).when(mRootWindowContainer).getTopDisplayFocusedRootTask();
 
         // Use the task as target to resume.
-        mRootWindowContainer.resumeFocusedTasksTopActivities(
-                rootTask, activity, null /* targetOptions */);
+        mRootWindowContainer.resumeFocusedTasksTopActivities(rootTask, activity,
+                null /* targetOptions */);
 
         // Verify the target task should resume its activity.
         verify(rootTask, times(1)).resumeTopActivityUncheckedLocked(
@@ -626,7 +628,7 @@
                 ACTIVITY_TYPE_STANDARD, false /* onTop */));
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setTask(rootTask).setOnTop(true).build();
-        activity.setState(Task.ActivityState.RESUMED, "test");
+        activity.setState(RESUMED, "test");
 
         // Assume the task is at the topmost position
         assertTrue(rootTask.isTopRootTaskInDisplayArea());
@@ -646,7 +648,7 @@
                 ACTIVITY_TYPE_STANDARD, false /* onTop */));
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setTask(rootTask).setOnTop(true).build();
-        activity.setState(Task.ActivityState.RESUMED, "test");
+        activity.setState(RESUMED, "test");
         taskDisplayArea.positionChildAt(POSITION_BOTTOM, rootTask, false /*includingParents*/);
 
         // Assume the task is at the topmost position
@@ -774,7 +776,7 @@
     }
 
     /**
-     * Tests that when starting {@link #ResolverActivity} for home, it should use the standard
+     * Tests that when starting {@link ResolverActivity} for home, it should use the standard
      * activity type (in a new root task) so the order of back stack won't be broken.
      */
     @Test
@@ -1006,33 +1008,31 @@
 
     @Test
     public void testLockAllProfileTasks() {
-        // Make an activity visible with the user id set to 0
-        final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
-        final int taskId = task.mTaskId;
-        final ActivityRecord activity = task.getTopMostActivity();
+        final int profileUid = UserHandle.PER_USER_RANGE + UserHandle.MIN_SECONDARY_USER_ID;
+        final int profileUserId = UserHandle.getUserId(profileUid);
+        // Create an activity belonging to the profile user.
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setUid(profileUid).build();
+        final Task task = activity.getTask();
 
-        // Create another activity on top and the user id is 1
-        final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task)
-                .setUid(UserHandle.PER_USER_RANGE + 1).build();
-        doReturn(true).when(topActivity).okToShowLocked();
+        // Create another activity belonging to current user on top.
+        final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
         topActivity.intent.setAction(Intent.ACTION_MAIN);
 
         // Make sure the listeners will be notified for putting the task to locked state
         TaskChangeNotificationController controller = mAtm.getTaskChangeNotificationController();
         spyOn(controller);
-        mWm.mRoot.lockAllProfileTasks(0);
-        verify(controller).notifyTaskProfileLocked(eq(taskId), eq(0));
+        mWm.mRoot.lockAllProfileTasks(profileUserId);
+        verify(controller).notifyTaskProfileLocked(eq(task.mTaskId), eq(profileUserId));
 
         // Create the work lock activity on top of the task
-        final ActivityRecord workLockActivity = new ActivityBuilder(mAtm).setTask(task)
-                .setUid(UserHandle.PER_USER_RANGE + 1).build();
-        doReturn(true).when(workLockActivity).okToShowLocked();
+        final ActivityRecord workLockActivity = new ActivityBuilder(mAtm).setTask(task).build();
         workLockActivity.intent.setAction(ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER);
         doReturn(workLockActivity.mActivityComponent).when(mAtm).getSysUiServiceComponentLocked();
 
         // Make sure the listener won't be notified again.
         clearInvocations(controller);
-        mWm.mRoot.lockAllProfileTasks(0);
+        mWm.mRoot.lockAllProfileTasks(profileUserId);
         verify(controller, never()).notifyTaskProfileLocked(anyInt(), anyInt());
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java
index c44c22f..cb85884 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java
@@ -166,7 +166,7 @@
         final Task task = new TaskBuilder(mAtm.mTaskSupervisor)
                 .setComponent(new ComponentName(mContext.getPackageName(), className))
                 .setTaskId(taskId)
-                .setParentTask(stack)
+                .setParentTaskFragment(stack)
                 .build();
         task.lastActiveTime = lastActiveTime;
         final ActivityRecord activity = new ActivityBuilder(mAtm)
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index f35e85c..7266631 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -40,8 +40,15 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
-import static com.android.server.wm.Task.ActivityState.STOPPED;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -53,7 +60,9 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doCallRealMethod;
 
@@ -99,10 +108,14 @@
 
     private Task mTask;
     private ActivityRecord mActivity;
+    private ActivityMetricsLogger mActivityMetricsLogger;
     private Properties mInitialConstrainDisplayApisFlags;
 
     @Before
     public void setUp() throws Exception {
+        mActivityMetricsLogger = mock(ActivityMetricsLogger.class);
+        clearInvocations(mActivityMetricsLogger);
+        doReturn(mActivityMetricsLogger).when(mAtm.mTaskSupervisor).getActivityMetricsLogger();
         mInitialConstrainDisplayApisFlags = DeviceConfig.getProperties(
                 NAMESPACE_CONSTRAIN_DISPLAY_APIS);
         DeviceConfig.setProperties(
@@ -130,7 +143,7 @@
         doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
         mActivity.mVisibleRequested = true;
         mActivity.setSavedState(null /* savedState */);
-        mActivity.setState(Task.ActivityState.RESUMED, "testRestart");
+        mActivity.setState(RESUMED, "testRestart");
         prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_UNSPECIFIED);
 
         final Rect originalOverrideBounds = new Rect(mActivity.getBounds());
@@ -138,7 +151,7 @@
         // The visible activity should recompute configuration according to the last parent bounds.
         mAtm.mActivityClientController.restartActivityProcessIfVisible(mActivity.appToken);
 
-        assertEquals(Task.ActivityState.RESTARTING_PROCESS, mActivity.getState());
+        assertEquals(RESTARTING_PROCESS, mActivity.getState());
         assertNotEquals(originalOverrideBounds, mActivity.getBounds());
     }
 
@@ -161,7 +174,9 @@
 
         // The activity should be able to accept negative x position [-150, 100 - 150, 600].
         final int dx = bounds.left + bounds.width() / 2;
-        mTask.setBounds(bounds.left - dx, bounds.top, bounds.right - dx, bounds.bottom);
+        final int dy = bounds.top + bounds.height() / 2;
+        mTask.setBounds(bounds.left - dx, bounds.top - dy, bounds.right - dx, bounds.bottom - dy);
+        // expected:<Rect(-150, 100 - 150, 600)> but was:<Rect(-150, 0 - 150, 500)>
         assertEquals(mTask.getBounds(), mActivity.getBounds());
 
         final int density = mActivity.getConfiguration().densityDpi;
@@ -318,6 +333,11 @@
         assertScaled();
         // Activity is sandboxed due to size compat mode.
         assertActivityMaxBoundsSandboxed();
+
+        final WindowState appWindow = addWindowToActivity(mActivity);
+        assertTrue(mActivity.hasSizeCompatBounds());
+        assertEquals("App window must use size compat bounds for layout in screen space",
+                mActivity.getBounds(), appWindow.getBounds());
     }
 
     @Test
@@ -585,7 +605,7 @@
     public void testHandleActivitySizeCompatModeChanged() {
         setUpDisplaySizeWithApp(1000, 2000);
         doReturn(true).when(mTask).isOrganized();
-        mActivity.setState(Task.ActivityState.RESUMED, "testHandleActivitySizeCompatModeChanged");
+        mActivity.setState(RESUMED, "testHandleActivitySizeCompatModeChanged");
         prepareUnresizable(mActivity, -1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
         assertFitted();
 
@@ -604,7 +624,7 @@
         mActivity.mVisibleRequested = true;
         mActivity.restartProcessIfVisible();
         // The full lifecycle isn't hooked up so manually set state to resumed
-        mActivity.setState(Task.ActivityState.RESUMED, "testHandleActivitySizeCompatModeChanged");
+        mActivity.setState(RESUMED, "testHandleActivitySizeCompatModeChanged");
         mTask.mDisplayContent.handleActivitySizeCompatModeIfNeeded(mActivity);
 
         // Expect null token when switching to non-size-compat mode activity.
@@ -618,7 +638,7 @@
     public void testHandleActivitySizeCompatModeChangedOnDifferentTask() {
         setUpDisplaySizeWithApp(1000, 2000);
         doReturn(true).when(mTask).isOrganized();
-        mActivity.setState(Task.ActivityState.RESUMED, "testHandleActivitySizeCompatModeChanged");
+        mActivity.setState(RESUMED, "testHandleActivitySizeCompatModeChanged");
         prepareUnresizable(mActivity, -1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
         assertFitted();
 
@@ -637,7 +657,7 @@
                 .setCreateActivity(true).build();
         final ActivityRecord secondActivity = secondTask.getTopNonFinishingActivity();
         doReturn(true).when(secondTask).isOrganized();
-        secondActivity.setState(Task.ActivityState.RESUMED,
+        secondActivity.setState(RESUMED,
                 "testHandleActivitySizeCompatModeChanged");
         prepareUnresizable(secondActivity, -1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -1553,6 +1573,30 @@
     }
 
     @Test
+    public void testSandboxDisplayApis_unresizableAppNotSandboxed() {
+        // Set up a display in landscape with an unresizable app.
+        setUpDisplaySizeWithApp(2500, 1000);
+        mActivity.mDisplayContent.setSandboxDisplayApis(false /* sandboxDisplayApis */);
+        prepareUnresizable(mActivity, 1.5f, SCREEN_ORIENTATION_LANDSCAPE);
+        assertFitted();
+
+        // Activity max bounds not be sandboxed since sandboxing is disabled.
+        assertMaxBoundsInheritDisplayAreaBounds();
+    }
+
+    @Test
+    public void testSandboxDisplayApis_unresizableAppSandboxed() {
+        // Set up a display in landscape with an unresizable app.
+        setUpDisplaySizeWithApp(2500, 1000);
+        mActivity.mDisplayContent.setSandboxDisplayApis(true /* sandboxDisplayApis */);
+        prepareUnresizable(mActivity, 1.5f, SCREEN_ORIENTATION_LANDSCAPE);
+        assertFitted();
+
+        // Activity max bounds should be sandboxed since sandboxing is enabled.
+        assertActivityMaxBoundsSandboxed();
+    }
+
+    @Test
     public void testResizableApp_notSandboxed() {
         // Set up a display in landscape with a fully resizable app.
         setUpDisplaySizeWithApp(2500, 1000);
@@ -1808,7 +1852,7 @@
                 // At launch.
                 /* fixedOrientationLetterbox */ new Rect(0, 0, 700, 1400),
                 // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(0, 0, 700, 1400),
+                /* sizeCompatUnscaled */ new Rect(0, 700, 700, 2100),
                 // After the display is resized to (700, 1400).
                 /* sizeCompatScaled */ new Rect(0, 0, 350, 700));
     }
@@ -1821,7 +1865,7 @@
                 // At launch.
                 /* fixedOrientationLetterbox */ new Rect(1050, 0, 1750, 1400),
                 // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(350, 0, 1050, 1400),
+                /* sizeCompatUnscaled */ new Rect(350, 700, 1050, 2100),
                 // After the display is resized to (700, 1400).
                 /* sizeCompatScaled */ new Rect(525, 0, 875, 700));
     }
@@ -1836,7 +1880,7 @@
                 // At launch.
                 /* fixedOrientationLetterbox */ new Rect(1050, 0, 1750, 1400),
                 // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(350, 0, 1050, 1400),
+                /* sizeCompatUnscaled */ new Rect(350, 700, 1050, 2100),
                 // After the display is resized to (700, 1400).
                 /* sizeCompatScaled */ new Rect(525, 0, 875, 700));
 
@@ -1846,7 +1890,7 @@
                 // At launch.
                 /* fixedOrientationLetterbox */ new Rect(1050, 0, 1750, 1400),
                 // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(350, 0, 1050, 1400),
+                /* sizeCompatUnscaled */ new Rect(350, 700, 1050, 2100),
                 // After the display is resized to (700, 1400).
                 /* sizeCompatScaled */ new Rect(525, 0, 875, 700));
     }
@@ -1859,7 +1903,7 @@
                 // At launch.
                 /* fixedOrientationLetterbox */ new Rect(2100, 0, 2800, 1400),
                 // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(700, 0, 1400, 1400),
+                /* sizeCompatUnscaled */ new Rect(700, 700, 1400, 2100),
                 // After the display is resized to (700, 1400).
                 /* sizeCompatScaled */ new Rect(1050, 0, 1400, 700));
     }
@@ -1908,13 +1952,19 @@
         setUpDisplaySizeWithApp(1000, 2500);
 
         assertFalse(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity, APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED);
 
         prepareUnresizable(mActivity, /* maxAspect= */ 2, SCREEN_ORIENTATION_PORTRAIT);
         assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
         assertFalse(mActivity.inSizeCompatMode());
         assertTrue(mActivity.areBoundsLetterboxed());
 
+        verifyLogAppCompatState(mActivity,
+                APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO);
+
         rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+        verifyLogAppCompatState(mActivity,
+                APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE);
         rotateDisplay(mActivity.mDisplayContent, ROTATION_0);
 
         // After returning to the original rotation, bounds are computed in
@@ -1924,6 +1974,18 @@
         assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
         assertFalse(mActivity.inSizeCompatMode());
         assertTrue(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity,
+                APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO);
+
+        // After setting the visibility of the activity to false, areBoundsLetterboxed() still
+        // returns true but the NOT_VISIBLE App Compat state is logged.
+        mActivity.setVisibility(false);
+        assertTrue(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity, APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE);
+        mActivity.setVisibility(true);
+        assertTrue(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity,
+                APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO);
     }
 
     @Test
@@ -1932,12 +1994,15 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
         assertFalse(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity, APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
         assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
         assertFalse(mActivity.inSizeCompatMode());
         assertTrue(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity,
+                APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION);
     }
 
     @Test
@@ -1947,12 +2012,70 @@
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
         assertFalse(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity, APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED);
 
         rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
 
         assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
         assertTrue(mActivity.inSizeCompatMode());
         assertTrue(mActivity.areBoundsLetterboxed());
+        verifyLogAppCompatState(mActivity,
+                APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE);
+    }
+
+    /**
+     * Tests that all three paths in which aspect ratio logic can be applied yield the same
+     * result, which is that aspect ratio is respected on app bounds. The three paths are
+     * fixed orientation, no fixed orientation but fixed aspect ratio, and size compat mode.
+     */
+    @Test
+    public void testAllAspectRatioLogicConsistent() {
+        // Create display that has all stable insets and does not rotate. Make sure that status bar
+        // height is greater than notch height so that stable bounds do not equal app bounds.
+        final int notchHeight = 75;
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600)
+                .setSystemDecorations(true).setNotch(notchHeight)
+                .setStatusBarHeight(notchHeight + 20).setCanRotate(false).build();
+
+        // Create task on test display.
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+        // Target min aspect ratio must be larger than parent aspect ratio to be applied.
+        final float targetMinAspectRatio = 3.0f;
+
+        // Create fixed portait activity with min aspect ratio greater than parent aspect ratio.
+        final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm)
+                .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .setMinAspectRatio(targetMinAspectRatio).build();
+        final Rect fixedOrientationAppBounds = new Rect(fixedOrientationActivity.getConfiguration()
+                .windowConfiguration.getAppBounds());
+
+        // Create activity with no fixed orientation and min aspect ratio greater than parent aspect
+        // ratio.
+        final ActivityRecord minAspectRatioActivity = new ActivityBuilder(mAtm).setTask(task)
+                .setMinAspectRatio(targetMinAspectRatio).build();
+        final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration()
+                .windowConfiguration.getAppBounds());
+
+        // Create unresizeable fixed portait activity with min aspect ratio greater than parent
+        // aspect ratio.
+        final ActivityRecord sizeCompatActivity = new ActivityBuilder(mAtm)
+                .setTask(task).setResizeMode(RESIZE_MODE_UNRESIZEABLE)
+                .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .setMinAspectRatio(targetMinAspectRatio).build();
+        // Resize display running unresizeable activity to make it enter size compat mode.
+        resizeDisplay(display, 1800, 1000);
+        final Rect sizeCompatAppBounds = new Rect(sizeCompatActivity.getConfiguration()
+                .windowConfiguration.getAppBounds());
+
+        // Check that aspect ratio of app bounds is equal to the min aspect ratio.
+        final float delta = 0.01f;
+        assertEquals(targetMinAspectRatio, ActivityRecord
+                .computeAspectRatio(fixedOrientationAppBounds), delta);
+        assertEquals(targetMinAspectRatio, ActivityRecord
+                .computeAspectRatio(minAspectRatioAppBounds), delta);
+        assertEquals(targetMinAspectRatio, ActivityRecord
+                .computeAspectRatio(sizeCompatAppBounds), delta);
     }
 
     private void assertHorizontalPositionForDifferentDisplayConfigsForLandscapeActivity(
@@ -1972,7 +2095,7 @@
 
         assertTrue(mActivity.inSizeCompatMode());
         // Activity is in size compat mode but not scaled.
-        assertEquals(new Rect(0, 0, 1400, 700), mActivity.getBounds());
+        assertEquals(new Rect(0, 1050, 1400, 1750), mActivity.getBounds());
     }
 
     private static WindowState addWindowToActivity(ActivityRecord activity) {
@@ -2111,6 +2234,11 @@
                 .isEqualTo(activity.getWindowConfiguration().getBounds());
     }
 
+    private void verifyLogAppCompatState(ActivityRecord activity, int state) {
+        verify(mActivityMetricsLogger, atLeastOnce()).logAppCompatState(
+                argThat(r -> activity == r && r.getAppCompatState() == state));
+    }
+
     static Configuration rotateDisplay(DisplayContent display, int rotation) {
         final Configuration c = new Configuration();
         display.getDisplayRotation().setRotation(rotation);
diff --git a/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java b/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java
index 3a2190d..cac948c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java
+++ b/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java
@@ -178,6 +178,11 @@
     }
 
     @Override
+    public SurfaceControl.Transaction setDisplayFlags(IBinder displayToken, int flags) {
+        return this;
+    }
+
+    @Override
     public SurfaceControl.Transaction setDisplayProjection(IBinder displayToken,
             int orientation, Rect layerStackRect, Rect displayRect) {
         return this;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 8e7ba4bc..5bc45d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -271,7 +271,6 @@
         doNothing().when(amInternal).cleanUpServices(anyInt(), any(), any());
         doReturn(UserHandle.USER_SYSTEM).when(amInternal).getCurrentUserId();
         doReturn(TEST_USER_PROFILE_IDS).when(amInternal).getCurrentProfileIds();
-        doReturn(true).when(amInternal).isCurrentProfile(anyInt());
         doReturn(true).when(amInternal).isUserRunning(anyInt(), anyInt());
         doReturn(true).when(amInternal).hasStartedUserState(anyInt());
         doReturn(false).when(amInternal).shouldConfirmCredentials(anyInt());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 67b273a..d68edba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -37,8 +37,8 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -84,8 +84,7 @@
                 mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
         adjacentRootTask.mCreatedByOrganizer = true;
         final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
-        adjacentRootTask.mAdjacentTask = rootTask;
-        rootTask.mAdjacentTask = adjacentRootTask;
+        adjacentRootTask.setAdjacentTaskFragment(rootTask);
 
         taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
         Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -111,8 +110,7 @@
         final Task adjacentRootTask = createTask(
                 mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
         adjacentRootTask.mCreatedByOrganizer = true;
-        adjacentRootTask.mAdjacentTask = rootTask;
-        rootTask.mAdjacentTask = adjacentRootTask;
+        adjacentRootTask.setAdjacentTaskFragment(rootTask);
 
         taskDisplayArea.setLaunchRootTask(rootTask,
                 new int[]{WINDOWING_MODE_MULTI_WINDOW}, new int[]{ACTIVITY_TYPE_STANDARD});
@@ -133,8 +131,7 @@
                 mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
         adjacentRootTask.mCreatedByOrganizer = true;
         final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
-        adjacentRootTask.mAdjacentTask = rootTask;
-        rootTask.mAdjacentTask = adjacentRootTask;
+        adjacentRootTask.setAdjacentTaskFragment(rootTask);
 
         taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
         final Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -623,7 +620,7 @@
         final Task pinnedRootTask = mRootWindowContainer.getDefaultTaskDisplayArea()
                 .createRootTask(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, ON_TOP);
         final Task pinnedTask = new TaskBuilder(mAtm.mTaskSupervisor)
-                .setParentTask(pinnedRootTask).build();
+                .setParentTaskFragment(pinnedRootTask).build();
         new ActivityBuilder(mAtm).setActivityFlags(FLAG_ALWAYS_FOCUSABLE)
                 .setTask(pinnedTask).build();
         pinnedRootTask.moveToFront("movePinnedRootTaskToFront");
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
new file mode 100644
index 0000000..d475c46e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.testing.Assert.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.view.RemoteAnimationDefinition;
+import android.view.SurfaceControl;
+import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentCreationParams;
+import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOrganizer;
+import android.window.TaskFragmentOrganizerToken;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransactionCallback;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ *  atest WmTests:TaskFragmentOrganizerControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
+
+    private TaskFragmentOrganizerController mController;
+    private TaskFragmentOrganizer mOrganizer;
+    private TaskFragmentOrganizerToken mOrganizerToken;
+    private ITaskFragmentOrganizer mIOrganizer;
+    private TaskFragment mTaskFragment;
+    private TaskFragmentInfo mTaskFragmentInfo;
+    private IBinder mFragmentToken;
+    private WindowContainerTransaction mTransaction;
+    private WindowContainerToken mFragmentWindowToken;
+    private RemoteAnimationDefinition mDefinition;
+
+    @Before
+    public void setup() {
+        mController = mAtm.mWindowOrganizerController.mTaskFragmentOrganizerController;
+        mOrganizer = new TaskFragmentOrganizer(Runnable::run);
+        mOrganizerToken = mOrganizer.getOrganizerToken();
+        mIOrganizer = ITaskFragmentOrganizer.Stub.asInterface(mOrganizerToken.asBinder());
+        mTaskFragmentInfo = mock(TaskFragmentInfo.class);
+        mFragmentToken = new Binder();
+        mTaskFragment =
+                new TaskFragment(mAtm, mFragmentToken, true /* createdByOrganizer */);
+        mTransaction = new WindowContainerTransaction();
+        mFragmentWindowToken = mTaskFragment.mRemoteToken.toWindowContainerToken();
+        mDefinition = new RemoteAnimationDefinition();
+
+        spyOn(mController);
+        spyOn(mOrganizer);
+        spyOn(mTaskFragment);
+        doReturn(mIOrganizer).when(mTaskFragment).getTaskFragmentOrganizer();
+        doReturn(mTaskFragmentInfo).when(mTaskFragment).getTaskFragmentInfo();
+        doReturn(new SurfaceControl()).when(mTaskFragment).getSurfaceControl();
+        doReturn(mFragmentToken).when(mTaskFragment).getFragmentToken();
+        doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration();
+    }
+
+    @Test
+    public void testCallTaskFragmentCallbackWithoutRegister_throwsException() {
+        assertThrows(IllegalArgumentException.class, () -> mController
+                .onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment));
+
+        assertThrows(IllegalArgumentException.class, () -> mController
+                .onTaskFragmentInfoChanged(
+                        mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment));
+
+        assertThrows(IllegalArgumentException.class, () -> mController
+                .onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment));
+
+        assertThrows(IllegalArgumentException.class, () -> mController
+                .onTaskFragmentParentInfoChanged(mTaskFragment.getTaskFragmentOrganizer(),
+                        mTaskFragment));
+    }
+
+    @Test
+    public void testOnTaskFragmentAppeared() {
+        mController.registerOrganizer(mIOrganizer);
+
+        mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer).onTaskFragmentAppeared(any());
+    }
+
+    @Test
+    public void testOnTaskFragmentInfoChanged() {
+        mController.registerOrganizer(mIOrganizer);
+        mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        // No callback if the info is not changed.
+        doReturn(true).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
+        doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration();
+
+        mController.onTaskFragmentInfoChanged(mTaskFragment.getTaskFragmentOrganizer(),
+                mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+
+        // Trigger callback if the info is changed.
+        doReturn(false).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
+
+        mController.onTaskFragmentInfoChanged(mTaskFragment.getTaskFragmentOrganizer(),
+                mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer).onTaskFragmentInfoChanged(mTaskFragmentInfo);
+    }
+
+    @Test
+    public void testOnTaskFragmentVanished() {
+        mController.registerOrganizer(mIOrganizer);
+
+        mTaskFragment.mTaskFragmentAppearedSent = true;
+        mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer).onTaskFragmentVanished(any());
+    }
+
+    @Test
+    public void testOnTaskFragmentParentInfoChanged() {
+        mController.registerOrganizer(mIOrganizer);
+        final Task parent = mock(Task.class);
+        final Configuration parentConfig = new Configuration();
+        parentConfig.smallestScreenWidthDp = 10;
+        doReturn(parent).when(mTaskFragment).getParent();
+        doReturn(parentConfig).when(parent).getConfiguration();
+        doReturn(parent).when(parent).asTask();
+
+        mTaskFragment.mTaskFragmentAppearedSent = true;
+        mController.onTaskFragmentParentInfoChanged(
+                mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mFragmentToken), any());
+
+        // No extra callback if the info is not changed.
+        clearInvocations(mOrganizer);
+
+        mController.onTaskFragmentParentInfoChanged(
+                mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), any());
+
+        // Trigger callback if the info is changed.
+        parentConfig.smallestScreenWidthDp = 100;
+
+        mController.onTaskFragmentParentInfoChanged(
+                mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mFragmentToken), any());
+    }
+
+    @Test
+    public void testOnTaskFragmentError() throws RemoteException {
+        final IBinder errorCallbackToken = new Binder();
+        final Throwable exception = new IllegalArgumentException("Test exception");
+
+        mController.registerOrganizer(mIOrganizer);
+        mController.onTaskFragmentError(mTaskFragment.getTaskFragmentOrganizer(),
+                errorCallbackToken, exception);
+        mController.dispatchPendingEvents();
+
+        verify(mOrganizer).onTaskFragmentError(eq(errorCallbackToken), eq(exception));
+    }
+
+    @Test
+    public void testRegisterRemoteAnimations() {
+        mController.registerOrganizer(mIOrganizer);
+        mController.registerRemoteAnimations(mIOrganizer, mDefinition);
+
+        assertEquals(mDefinition, mController.getRemoteAnimationDefinition(mIOrganizer));
+
+        mController.unregisterRemoteAnimations(mIOrganizer);
+
+        assertNull(mController.getRemoteAnimationDefinition(mIOrganizer));
+    }
+
+    @Test
+    public void testWindowContainerTransaction_setTaskFragmentOrganizer() {
+        mOrganizer.applyTransaction(mTransaction);
+
+        assertEquals(mIOrganizer, mTransaction.getTaskFragmentOrganizer());
+
+        mTransaction = new WindowContainerTransaction();
+        mOrganizer.applySyncTransaction(
+                mTransaction, mock(WindowContainerTransactionCallback.class));
+
+        assertEquals(mIOrganizer, mTransaction.getTaskFragmentOrganizer());
+    }
+
+    @Test
+    public void testApplyTransaction_enforceConfigurationChangeOnOrganizedTaskFragment()
+            throws RemoteException {
+        mOrganizer.applyTransaction(mTransaction);
+
+        // Throw exception if the transaction is trying to change a window that is not organized by
+        // the organizer.
+        mTransaction.setBounds(mFragmentWindowToken, new Rect(0, 0, 100, 100));
+
+        assertThrows(SecurityException.class, () -> {
+            try {
+                mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+            } catch (RemoteException e) {
+                fail();
+            }
+        });
+
+        // Allow transaction to change a TaskFragment created by the organizer.
+        mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* pid */);
+
+        mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+    }
+
+    @Test
+    public void testApplyTransaction_enforceHierarchyChange_reorder() throws RemoteException {
+        mOrganizer.applyTransaction(mTransaction);
+
+        // Throw exception if the transaction is trying to change a window that is not organized by
+        // the organizer.
+        mTransaction.reorder(mFragmentWindowToken, true /* onTop */);
+
+        assertThrows(SecurityException.class, () -> {
+            try {
+                mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+            } catch (RemoteException e) {
+                fail();
+            }
+        });
+
+        // Allow transaction to change a TaskFragment created by the organizer.
+        mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* pid */);
+
+        mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+    }
+
+    @Test
+    public void testApplyTransaction_enforceHierarchyChange_deleteTaskFragment()
+            throws RemoteException {
+        mController.registerOrganizer(mIOrganizer);
+        mOrganizer.applyTransaction(mTransaction);
+        doReturn(true).when(mTaskFragment).isAttached();
+
+        // Throw exception if the transaction is trying to change a window that is not organized by
+        // the organizer.
+        mTransaction.deleteTaskFragment(mFragmentWindowToken);
+
+        assertThrows(SecurityException.class, () -> {
+            try {
+                mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+            } catch (RemoteException e) {
+                fail();
+            }
+        });
+
+        // Allow transaction to change a TaskFragment created by the organizer.
+        mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* pid */);
+        clearInvocations(mAtm.mRootWindowContainer);
+
+        mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+
+        // No lifecycle update when the TaskFragment is not recorded.
+        verify(mAtm.mRootWindowContainer, never()).resumeFocusedTasksTopActivities();
+
+        mAtm.mWindowOrganizerController.mLaunchTaskFragments
+                .put(mFragmentToken, mTaskFragment);
+        mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+
+        verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities();
+    }
+
+    @Test
+    public void testApplyTransaction_enforceHierarchyChange_setAdjacentRoots()
+            throws RemoteException {
+        final TaskFragment taskFragment2 =
+                new TaskFragment(mAtm, new Binder(), true /* createdByOrganizer */);
+        final WindowContainerToken token2 = taskFragment2.mRemoteToken.toWindowContainerToken();
+        mOrganizer.applyTransaction(mTransaction);
+
+        // Throw exception if the transaction is trying to change a window that is not organized by
+        // the organizer.
+        mTransaction.setAdjacentRoots(mFragmentWindowToken, token2);
+
+        assertThrows(SecurityException.class, () -> {
+            try {
+                mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+            } catch (RemoteException e) {
+                fail();
+            }
+        });
+
+        // Allow transaction to change a TaskFragment created by the organizer.
+        mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* pid */);
+        taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* pid */);
+        clearInvocations(mAtm.mRootWindowContainer);
+
+        mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+
+        verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities();
+    }
+
+    @Test
+    public void testApplyTransaction_enforceHierarchyChange_createTaskFragment() {
+        mOrganizer.applyTransaction(mTransaction);
+
+        // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment.
+        final TaskFragmentCreationParams mockParams = mock(TaskFragmentCreationParams.class);
+        doReturn(mOrganizerToken).when(mockParams).getOrganizer();
+        mTransaction.createTaskFragment(mockParams);
+        mTransaction.startActivityInTaskFragment(
+                mFragmentToken, null /* callerToken */, new Intent(), null /* activityOptions */);
+        mTransaction.reparentActivityToTaskFragment(mFragmentToken, mock(IBinder.class));
+        mTransaction.setAdjacentTaskFragments(mFragmentToken, mock(IBinder.class),
+                null /* options */);
+
+        // It is expected to fail for the mock TaskFragmentCreationParams. It is ok as we are
+        // testing the security check here.
+        assertThrows(IllegalArgumentException.class, () -> {
+            try {
+                mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+            } catch (RemoteException e) {
+                fail();
+            }
+        });
+    }
+
+    @Test
+    public void testApplyTransaction_enforceHierarchyChange_reparentChildren()
+            throws RemoteException {
+        mOrganizer.applyTransaction(mTransaction);
+        mController.registerOrganizer(mIOrganizer);
+        doReturn(true).when(mTaskFragment).isAttached();
+
+        // Throw exception if the transaction is trying to change a window that is not organized by
+        // the organizer.
+        mTransaction.reparentChildren(mFragmentWindowToken, null /* newParent */);
+
+        assertThrows(SecurityException.class, () -> {
+            try {
+                mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+            } catch (RemoteException e) {
+                fail();
+            }
+        });
+
+        // Allow transaction to change a TaskFragment created by the organizer.
+        mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* pid */);
+        clearInvocations(mAtm.mRootWindowContainer);
+
+        mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+
+        verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities();
+    }
+
+    @Test
+    public void testApplyTransaction_reparentActivityToTaskFragment_triggerLifecycleUpdate()
+            throws RemoteException {
+        final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
+        mOrganizer.applyTransaction(mTransaction);
+        mController.registerOrganizer(mIOrganizer);
+        mTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setFragmentToken(mFragmentToken)
+                .build();
+        mAtm.mWindowOrganizerController.mLaunchTaskFragments
+                .put(mFragmentToken, mTaskFragment);
+        mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.appToken);
+        clearInvocations(mAtm.mRootWindowContainer);
+
+        mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
+
+        verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities();
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
new file mode 100644
index 0000000..cb209abf
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.mockito.Mockito.clearInvocations;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.SurfaceControl;
+import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentOrganizer;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test class for {@link TaskFragment}.
+ *
+ * Build/Install/Run:
+ *  atest WmTests:TaskFragmentTest
+ */
+@MediumTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class TaskFragmentTest extends WindowTestsBase {
+
+    private TaskFragmentOrganizer mOrganizer;
+    private TaskFragment mTaskFragment;
+    private SurfaceControl mLeash;
+    @Mock
+    private SurfaceControl.Transaction mTransaction;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mOrganizer = new TaskFragmentOrganizer(Runnable::run);
+        final ITaskFragmentOrganizer iOrganizer =
+                ITaskFragmentOrganizer.Stub.asInterface(mOrganizer.getOrganizerToken().asBinder());
+        mAtm.mWindowOrganizerController.mTaskFragmentOrganizerController
+                .registerOrganizer(iOrganizer);
+        mTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setOrganizer(mOrganizer)
+                .build();
+        mLeash = mTaskFragment.getSurfaceControl();
+        spyOn(mTaskFragment);
+        doReturn(mTransaction).when(mTaskFragment).getSyncTransaction();
+        doReturn(mTransaction).when(mTaskFragment).getPendingTransaction();
+    }
+
+    @Test
+    public void testOnConfigurationChanged_updateSurface() {
+        final Rect bounds = new Rect(100, 100, 1100, 1100);
+        mTaskFragment.setBounds(bounds);
+
+        verify(mTransaction).setPosition(mLeash, 100, 100);
+        verify(mTransaction).setWindowCrop(mLeash, 1000, 1000);
+    }
+
+    @Test
+    public void testStartChangeTransition_resetSurface() {
+        final Rect startBounds = new Rect(0, 0, 1000, 1000);
+        final Rect endBounds = new Rect(500, 500, 1000, 1000);
+        mTaskFragment.setBounds(startBounds);
+        doReturn(true).when(mTaskFragment).isVisible();
+
+        clearInvocations(mTransaction);
+        mTaskFragment.setBounds(endBounds);
+
+        // Surface reset when prepare transition.
+        verify(mTaskFragment).initializeChangeTransition(startBounds);
+        verify(mTransaction).setPosition(mLeash, 0, 0);
+        verify(mTransaction).setWindowCrop(mLeash, 0, 0);
+
+        clearInvocations(mTransaction);
+        mTaskFragment.mSurfaceFreezer.unfreeze(mTransaction);
+
+        // Update surface after animation.
+        verify(mTransaction).setPosition(mLeash, 500, 500);
+        verify(mTransaction).setWindowCrop(mLeash, 500, 500);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index 5e4c67c..168c250 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -1321,6 +1321,50 @@
     }
 
     @Test
+    public void testDefaultFreeformSizeRespectsMinAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMinAspectRatio(5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder()
+                        .setOptions(options).calculate());
+
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at least 5.0, but was " + aspectRatio,
+                aspectRatio >= 5f);
+    }
+
+    @Test
+    public void testDefaultFreeformSizeRespectsMaxAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMaxAspectRatio(1.5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder()
+                        .setOptions(options).calculate());
+
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at most 1.5, but was " + aspectRatio,
+                aspectRatio <= 1.5f);
+    }
+
+    @Test
     public void testCascadesToSourceSizeForFreeform() {
         final TestDisplayContent freeformDisplay = createNewDisplayContent(
                 WINDOWING_MODE_FREEFORM);
@@ -1348,6 +1392,72 @@
     }
 
     @Test
+    public void testCascadesToSourceSizeForFreeformRespectingMinAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        final ActivityRecord source = createSourceActivity(freeformDisplay);
+        source.setBounds(0, 0, 412, 732);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMinAspectRatio(5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder().setSource(source).setOptions(options).calculate());
+
+        final Rect displayBounds = freeformDisplay.getBounds();
+        assertTrue("Left bounds should be larger than 0.", mResult.mBounds.left > 0);
+        assertTrue("Top bounds should be larger than 0.", mResult.mBounds.top > 0);
+        assertTrue("Bounds should be centered at somewhere in the left half, but it's "
+                        + "centerX is " + mResult.mBounds.centerX(),
+                mResult.mBounds.centerX() < displayBounds.centerX());
+        assertTrue("Bounds should be centered at somewhere in the top half, but it's "
+                        + "centerY is " + mResult.mBounds.centerY(),
+                mResult.mBounds.centerY() < displayBounds.centerY());
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at least 5.0, but was " + aspectRatio,
+                aspectRatio >= 5f);
+    }
+
+    @Test
+    public void testCascadesToSourceSizeForFreeformRespectingMaxAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        final ActivityRecord source = createSourceActivity(freeformDisplay);
+        source.setBounds(0, 0, 412, 732);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMaxAspectRatio(1.5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder().setSource(source).setOptions(options).calculate());
+
+        final Rect displayBounds = freeformDisplay.getBounds();
+        assertTrue("Left bounds should be larger than 0.", mResult.mBounds.left > 0);
+        assertTrue("Top bounds should be larger than 0.", mResult.mBounds.top > 0);
+        assertTrue("Bounds should be centered at somewhere in the left half, but it's "
+                        + "centerX is " + mResult.mBounds.centerX(),
+                mResult.mBounds.centerX() < displayBounds.centerX());
+        assertTrue("Bounds should be centered at somewhere in the top half, but it's "
+                        + "centerY is " + mResult.mBounds.centerY(),
+                mResult.mBounds.centerY() < displayBounds.centerY());
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at most 1.5, but was " + aspectRatio,
+                aspectRatio <= 1.5f);
+    }
+
+    @Test
     public void testAdjustBoundsToFitDisplay_TopLeftOutOfDisplay() {
         final TestDisplayContent freeformDisplay = createNewDisplayContent(
                 WINDOWING_MODE_FREEFORM);
@@ -1716,7 +1826,7 @@
         final Task rootTask = display.getDefaultTaskDisplayArea()
                 .createRootTask(display.getWindowingMode(), ACTIVITY_TYPE_STANDARD, true);
         rootTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
-        final Task task = new TaskBuilder(mSupervisor).setParentTask(rootTask).build();
+        final Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
         // Just work around the unnecessary adjustments for bounds.
         task.getWindowConfiguration().setBounds(bounds);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 0ebff1d..ce568f1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -126,8 +126,15 @@
         final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
         final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
 
-        task.removeIfPossible();
-        // Assert that the container was removed.
+        task.remove(false /* withTransition */, "testRemoveContainer");
+        // There is still an activity to be destroyed, so the task is not removed immediately.
+        assertNotNull(task.getParent());
+        assertTrue(rootTask.hasChild());
+        assertTrue(task.hasChild());
+        assertTrue(activity.finishing);
+
+        activity.destroyed("testRemoveContainer");
+        // Assert that the container was removed after the activity is destroyed.
         assertNull(task.getParent());
         assertEquals(0, task.getChildCount());
         assertNull(activity.getParent());
@@ -420,7 +427,7 @@
         TaskDisplayArea taskDisplayArea = mAtm.mRootWindowContainer.getDefaultTaskDisplayArea();
         Task rootTask = taskDisplayArea.createRootTask(WINDOWING_MODE_FREEFORM,
                 ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        Task task = new TaskBuilder(mSupervisor).setParentTask(rootTask).build();
+        Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
         final Configuration parentConfig = rootTask.getConfiguration();
         parentConfig.windowConfiguration.setBounds(parentBounds);
         parentConfig.densityDpi = DisplayMetrics.DENSITY_DEFAULT;
@@ -750,7 +757,7 @@
         DisplayInfo displayInfo = new DisplayInfo();
         mAtm.mContext.getDisplay().getDisplayInfo(displayInfo);
         final int displayHeight = displayInfo.logicalHeight;
-        final Task task = new TaskBuilder(mSupervisor).setParentTask(rootTask).build();
+        final Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
         final Configuration inOutConfig = new Configuration();
         final Configuration parentConfig = new Configuration();
         final int longSide = 1200;
@@ -891,7 +898,8 @@
 
     /**
      * Test that root activity index is reported correctly when looking for the 'effective root' in
-     * case when bottom activities are relinquishing task identity or finishing.
+     * case when bottom activity is finishing. Ignore the relinquishing task identity if it's not a
+     * system activity even with the FLAG_RELINQUISH_TASK_IDENTITY.
      */
     @Test
     public void testFindRootIndex_effectiveRoot_finishingAndRelinquishing() {
@@ -905,7 +913,7 @@
         new ActivityBuilder(mAtm).setTask(task).build();
 
         assertEquals("The first non-finishing activity and non-relinquishing task identity "
-                + "must be reported.", task.getChildAt(2), task.getRootActivity(
+                + "must be reported.", task.getChildAt(0), task.getRootActivity(
                 false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
     }
 
@@ -925,8 +933,8 @@
     }
 
     /**
-     * Test that the topmost activity index is reported correctly when looking for the
-     * 'effective root' for the case when all activities have relinquishTaskIdentity set.
+     * Test that the root activity index is reported correctly when looking for the
+     * 'effective root' for the case when all non-system activities have relinquishTaskIdentity set.
      */
     @Test
     public void testFindRootIndex_effectiveRoot_relinquishingMultipleActivities() {
@@ -937,9 +945,9 @@
         final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task).build();
         activity1.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
 
-        assertEquals("The topmost activity in the task must be reported.",
-                task.getChildAt(task.getChildCount() - 1), task.getRootActivity(
-                        false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/));
+        assertEquals("The topmost activity in the task must be reported.", task.getChildAt(0),
+                task.getRootActivity(false /*ignoreRelinquishIdentity*/,
+                        true /*setToBottomIfNone*/));
     }
 
     /** Test that bottom-most activity is reported in {@link Task#getRootActivity()}. */
@@ -1077,8 +1085,8 @@
     }
 
     /**
-     * Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} with activity that
-     * relinquishes task identity.
+     * Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} with non-system
+     * activity that relinquishes task identity.
      */
     @Test
     public void testGetTaskForActivity_onlyRoot_relinquishTaskIdentity() {
@@ -1093,7 +1101,7 @@
 
         assertEquals(task.mTaskId,
                 ActivityRecord.getTaskForActivityLocked(activity0.appToken, true /* onlyRoot */));
-        assertEquals(task.mTaskId,
+        assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
                 ActivityRecord.getTaskForActivityLocked(activity1.appToken, true /* onlyRoot */));
         assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
                 ActivityRecord.getTaskForActivityLocked(activity2.appToken, true /* onlyRoot */));
@@ -1257,7 +1265,8 @@
         LaunchParamsPersister persister = mAtm.mTaskSupervisor.mLaunchParamsPersister;
         spyOn(persister);
 
-        final Task task = getTestTask();
+        final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true)
+                .setCreateParentTask(true).build().getRootTask();
         task.setHasBeenVisible(false);
         task.getDisplayContent().setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
         task.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
@@ -1367,7 +1376,7 @@
         TaskDisplayArea taskDisplayArea = mAtm.mRootWindowContainer.getDefaultTaskDisplayArea();
         Task rootTask = taskDisplayArea.createRootTask(windowingMode, ACTIVITY_TYPE_STANDARD,
                 true /* onTop */);
-        Task task = new TaskBuilder(mSupervisor).setParentTask(rootTask).build();
+        Task task = new TaskBuilder(mSupervisor).setParentTaskFragment(rootTask).build();
 
         final Configuration parentConfig = rootTask.getConfiguration();
         parentConfig.windowConfiguration.setAppBounds(parentBounds);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index ce2d748..0e504d3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -19,7 +19,9 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -77,6 +79,7 @@
         private int mPosition = POSITION_BOTTOM;
         protected final ActivityTaskManagerService mService;
         private boolean mSystemDecorations = false;
+        private int mStatusBarHeight = 0;
 
         Builder(ActivityTaskManagerService service, int width, int height) {
             mService = service;
@@ -125,6 +128,10 @@
                     Insets.of(0, height, 0, 0), null, new Rect(20, 0, 80, height), null, null);
             return this;
         }
+        Builder setStatusBarHeight(int height) {
+            mStatusBarHeight = height;
+            return this;
+        }
         Builder setCanRotate(boolean canRotate) {
             mCanRotate = canRotate;
             return this;
@@ -158,6 +165,14 @@
                 doReturn(false).when(displayPolicy).hasStatusBar();
                 doReturn(false).when(newDisplay).supportsSystemDecorations();
             }
+            if (mStatusBarHeight > 0) {
+                doReturn(true).when(displayPolicy).hasStatusBar();
+                doAnswer(invocation -> {
+                    Rect inOutInsets = (Rect) invocation.getArgument(0);
+                    inOutInsets.top = mStatusBarHeight;
+                    return null;
+                }).when(displayPolicy).convertNonDecorInsetsToStableInsets(any(), anyInt());
+            }
             Configuration c = new Configuration();
             newDisplay.computeScreenConfiguration(c);
             c.windowConfiguration.setWindowingMode(mWindowingMode);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index acadb74..16d75ca 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -368,7 +368,7 @@
     }
 
     @Override
-    public int applyKeyguardOcclusionChange() {
+    public int applyKeyguardOcclusionChange(boolean keyguardOccludingStarted) {
         return 0;
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 2dfb3a1..a1c24c2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
@@ -33,14 +34,18 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.window.ITaskOrganizer;
+import android.window.ITransitionPlayer;
 import android.window.TransitionInfo;
 
 import androidx.test.filters.SmallTest;
@@ -339,6 +344,44 @@
     }
 
     @Test
+    public void testTargets_noIntermediatesToWallpaper() {
+        final Transition transition = createTestTransition(TRANSIT_OLD_TASK_OPEN);
+
+        final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
+                mock(IBinder.class), true, mDisplayContent, true /* ownerCanManageAppTokens */);
+        // Make DA organized so we can check that they don't get included.
+        WindowContainer parent = wallpaperWindowToken.getParent();
+        while (parent != null && parent != mDisplayContent) {
+            if (parent.asDisplayArea() != null) {
+                parent.asDisplayArea().setOrganizer(
+                        mock(android.window.IDisplayAreaOrganizer.class), true /* skipAppear */);
+            }
+            parent = parent.getParent();
+        }
+        final WindowState wallpaperWindow = createWindow(null, TYPE_WALLPAPER, wallpaperWindowToken,
+                "wallpaperWindow");
+        wallpaperWindowToken.setVisibleRequested(false);
+        transition.collect(wallpaperWindowToken);
+        wallpaperWindowToken.setVisibleRequested(true);
+        wallpaperWindow.mHasSurface = true;
+        doReturn(true).when(mDisplayContent).isAttached();
+        transition.collect(mDisplayContent);
+        mDisplayContent.getWindowConfiguration().setRotation(
+                (mDisplayContent.getWindowConfiguration().getRotation() + 1) % 4);
+
+        ArraySet<WindowContainer> targets = Transition.calculateTargets(
+                transition.mParticipants, transition.mChanges);
+        TransitionInfo info = Transition.calculateTransitionInfo(
+                0, 0, targets, transition.mChanges);
+        // The wallpaper is not organized, so it won't have a token; however, it will be marked
+        // as IS_WALLPAPER
+        assertEquals(FLAG_IS_WALLPAPER, info.getChanges().get(0).getFlags());
+        // Make sure no intermediate display areas were pulled in between wallpaper and display.
+        assertEquals(mDisplayContent.mRemoteToken.toWindowContainerToken(),
+                info.getChanges().get(0).getParent());
+    }
+
+    @Test
     public void testIndependent() {
         final Transition transition = createTestTransition(TRANSIT_OPEN);
         ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
@@ -406,6 +449,137 @@
                 info.getChange(openInChangeTask.mRemoteToken.toWindowContainerToken()), info));
     }
 
+    @Test
+    public void testIntermediateVisibility() {
+        final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class);
+        final TransitionController controller = new TransitionController(mAtm, snapshotController);
+        final ITransitionPlayer player = new ITransitionPlayer.Default();
+        controller.registerTransitionPlayer(player);
+        ITaskOrganizer mockOrg = mock(ITaskOrganizer.class);
+        final Transition openTransition = controller.createTransition(TRANSIT_OPEN);
+
+        // Start out with task2 visible and set up a transition that closes task2 and opens task1
+        final Task task1 = createTask(mDisplayContent);
+        task1.mTaskOrganizer = mockOrg;
+        final ActivityRecord activity1 = createActivityRecord(task1);
+        activity1.mVisibleRequested = false;
+        activity1.setVisible(false);
+        final Task task2 = createTask(mDisplayContent);
+        task2.mTaskOrganizer = mockOrg;
+        final ActivityRecord activity2 = createActivityRecord(task1);
+        activity2.mVisibleRequested = true;
+        activity2.setVisible(true);
+
+        openTransition.collectExistenceChange(task1);
+        openTransition.collectExistenceChange(activity1);
+        openTransition.collectExistenceChange(task2);
+        openTransition.collectExistenceChange(activity2);
+
+        activity1.mVisibleRequested = true;
+        activity1.setVisible(true);
+        activity2.mVisibleRequested = false;
+
+        // Using abort to force-finish the sync (since we can't wait for drawing in unit test).
+        // We didn't call abort on the transition itself, so it will still run onTransactionReady
+        // normally.
+        mWm.mSyncEngine.abort(openTransition.getSyncId());
+
+        // Before finishing openTransition, we are now going to simulate closing task1 to return
+        // back to (open) task2.
+        final Transition closeTransition = controller.createTransition(TRANSIT_CLOSE);
+
+        closeTransition.collectExistenceChange(task1);
+        closeTransition.collectExistenceChange(activity1);
+        closeTransition.collectExistenceChange(task2);
+        closeTransition.collectExistenceChange(activity2);
+
+        activity1.mVisibleRequested = false;
+        activity2.mVisibleRequested = true;
+
+        openTransition.finishTransition();
+
+        // We finished the openTransition. Even though activity1 is visibleRequested=false, since
+        // the closeTransition animation hasn't played yet, make sure that we didn't commit
+        // visible=false on activity1 since it needs to remain visible for the animation.
+        assertTrue(activity1.isVisible());
+        assertTrue(activity2.isVisible());
+
+        // Using abort to force-finish the sync (since we obviously can't wait for drawing).
+        // We didn't call abort on the actual transition, so it will still run onTransactionReady
+        // normally.
+        mWm.mSyncEngine.abort(closeTransition.getSyncId());
+
+        closeTransition.finishTransition();
+
+        assertFalse(activity1.isVisible());
+        assertTrue(activity2.isVisible());
+    }
+
+    @Test
+    public void testTransientLaunch() {
+        final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class);
+        final TransitionController controller = new TransitionController(mAtm, snapshotController);
+        final ITransitionPlayer player = new ITransitionPlayer.Default();
+        controller.registerTransitionPlayer(player);
+        ITaskOrganizer mockOrg = mock(ITaskOrganizer.class);
+        final Transition openTransition = controller.createTransition(TRANSIT_OPEN);
+
+        // Start out with task2 visible and set up a transition that closes task2 and opens task1
+        final Task task1 = createTask(mDisplayContent);
+        task1.mTaskOrganizer = mockOrg;
+        final ActivityRecord activity1 = createActivityRecord(task1);
+        activity1.mVisibleRequested = false;
+        activity1.setVisible(false);
+        final Task task2 = createTask(mDisplayContent);
+        task2.mTaskOrganizer = mockOrg;
+        final ActivityRecord activity2 = createActivityRecord(task2);
+        activity2.mVisibleRequested = true;
+        activity2.setVisible(true);
+
+        openTransition.collectExistenceChange(task1);
+        openTransition.collectExistenceChange(activity1);
+        openTransition.collectExistenceChange(task2);
+        openTransition.collectExistenceChange(activity2);
+
+        activity1.mVisibleRequested = true;
+        activity1.setVisible(true);
+        activity2.mVisibleRequested = false;
+
+        // Using abort to force-finish the sync (since we can't wait for drawing in unit test).
+        // We didn't call abort on the transition itself, so it will still run onTransactionReady
+        // normally.
+        mWm.mSyncEngine.abort(openTransition.getSyncId());
+
+        verify(snapshotController, times(1)).recordTaskSnapshot(eq(task2), eq(false));
+
+        openTransition.finishTransition();
+
+        // We are now going to simulate closing task1 to return back to (open) task2.
+        final Transition closeTransition = controller.createTransition(TRANSIT_CLOSE);
+
+        closeTransition.collectExistenceChange(task1);
+        closeTransition.collectExistenceChange(activity1);
+        closeTransition.collectExistenceChange(task2);
+        closeTransition.collectExistenceChange(activity2);
+        closeTransition.setTransientLaunch(activity2);
+
+        activity1.mVisibleRequested = false;
+        activity2.mVisibleRequested = true;
+
+        // Using abort to force-finish the sync (since we obviously can't wait for drawing).
+        // We didn't call abort on the actual transition, so it will still run onTransactionReady
+        // normally.
+        mWm.mSyncEngine.abort(closeTransition.getSyncId());
+
+        // Make sure we haven't called recordSnapshot (since we are transient, it shouldn't be
+        // called until finish).
+        verify(snapshotController, times(0)).recordTaskSnapshot(eq(task1), eq(false));
+
+        closeTransition.finishTransition();
+
+        verify(snapshotController, times(1)).recordTaskSnapshot(eq(task1), eq(false));
+    }
+
     /** Fill the change map with all the parents of top. Change maps are usually fully populated */
     private static void fillChangeMap(ArrayMap<WindowContainer, Transition.ChangeInfo> changes,
             WindowContainer top) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java
index e970c2a..e2f1334 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowAnimationSpecTest.java
@@ -19,7 +19,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_AFTER_ANIM;
-import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_BEFORE_ANIM;
 import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_NONE;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -89,43 +88,6 @@
     }
 
     @Test
-    public void testApply_clipBeforeNoAnimationBounds() {
-        // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 0, 0)
-        WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation, null,
-                mStackBounds, false /* canSkipFirstFrame */, ROOT_TASK_CLIP_BEFORE_ANIM,
-                true /* isAppAnimation */, 0 /* windowCornerRadius */);
-        windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
-        verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
-                argThat(rect -> rect.equals(mStackBounds)));
-    }
-
-    @Test
-    public void testApply_clipBeforeNoStackBounds() {
-        // Stack bounds is (0, 0, 0, 0) animation clip is (0, 0, 20, 20)
-        Rect windowCrop = new Rect(0, 0, 20, 20);
-        Animation a = createClipRectAnimation(windowCrop, windowCrop);
-        a.initialize(0, 0, 0, 0);
-        WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
-                null, false /* canSkipFirstFrame */, ROOT_TASK_CLIP_BEFORE_ANIM,
-                true /* isAppAnimation */, 0 /* windowCornerRadius */);
-        windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
-        verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
-    }
-
-    @Test
-    public void testApply_setCornerRadius() {
-        final float windowCornerRadius = 30f;
-        WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation, null,
-                mStackBounds, false /* canSkipFirstFrame */, ROOT_TASK_CLIP_BEFORE_ANIM,
-                true /* isAppAnimation */, windowCornerRadius);
-        windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
-        verify(mTransaction, never()).setCornerRadius(eq(mSurfaceControl), eq(windowCornerRadius));
-        when(mAnimation.hasRoundedCorners()).thenReturn(true);
-        windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
-        verify(mTransaction).setCornerRadius(eq(mSurfaceControl), eq(windowCornerRadius));
-    }
-
-    @Test
     public void testApply_setCornerRadius_noClip() {
         final float windowCornerRadius = 30f;
         WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(mAnimation, null,
@@ -136,32 +98,6 @@
         verify(mTransaction, never()).setCornerRadius(any(), anyFloat());
     }
 
-    @Test
-    public void testApply_clipBeforeSmallerAnimationClip() {
-        // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 5, 5)
-        Rect windowCrop = new Rect(0, 0, 5, 5);
-        Animation a = createClipRectAnimation(windowCrop, windowCrop);
-        WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
-                mStackBounds, false /* canSkipFirstFrame */, ROOT_TASK_CLIP_BEFORE_ANIM,
-                true /* isAppAnimation */, 0 /* windowCornerRadius */);
-        windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
-        verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
-                argThat(rect -> rect.equals(windowCrop)));
-    }
-
-    @Test
-    public void testApply_clipBeforeSmallerStackClip() {
-        // Stack bounds is (0, 0, 10, 10) animation clip is (0, 0, 20, 20)
-        Rect windowCrop = new Rect(0, 0, 20, 20);
-        Animation a = createClipRectAnimation(windowCrop, windowCrop);
-        WindowAnimationSpec windowAnimationSpec = new WindowAnimationSpec(a, null,
-                mStackBounds, false /* canSkipFirstFrame */, ROOT_TASK_CLIP_BEFORE_ANIM,
-                true /* isAppAnimation */, 0 /* windowCornerRadius */);
-        windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
-        verify(mTransaction).setWindowCrop(eq(mSurfaceControl),
-                argThat(rect -> rect.equals(mStackBounds)));
-    }
-
     private Animation createClipRectAnimation(Rect fromClip, Rect toClip) {
         Animation a = new ClipRectAnimation(fromClip, toClip);
         a.initialize(0, 0, 0, 0);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 00f3d8b..bbeb980 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
@@ -52,6 +53,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -1071,6 +1073,106 @@
         verify(surfaceAnimator).setRelativeLayer(t, relativeParent, 1 /* layer */);
     }
 
+    @Test
+    public void testAssignAnimationLayer() {
+        final WindowContainer container = new WindowContainer(mWm);
+        container.mSurfaceControl = mock(SurfaceControl.class);
+        final SurfaceAnimator surfaceAnimator = container.mSurfaceAnimator;
+        final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer;
+        final SurfaceControl relativeParent = mock(SurfaceControl.class);
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        spyOn(container);
+        spyOn(surfaceAnimator);
+        spyOn(surfaceFreezer);
+
+        container.setLayer(t, 1);
+        container.setRelativeLayer(t, relativeParent, 2);
+
+        // Set through surfaceAnimator if surfaceFreezer doesn't have leash.
+        verify(surfaceAnimator).setLayer(t, 1);
+        verify(surfaceAnimator).setRelativeLayer(t, relativeParent, 2);
+        verify(surfaceFreezer, never()).setLayer(any(), anyInt());
+        verify(surfaceFreezer, never()).setRelativeLayer(any(), any(), anyInt());
+
+        clearInvocations(surfaceAnimator);
+        clearInvocations(surfaceFreezer);
+        doReturn(true).when(surfaceFreezer).hasLeash();
+
+        container.setLayer(t, 1);
+        container.setRelativeLayer(t, relativeParent, 2);
+
+        // Set through surfaceFreezer if surfaceFreezer has leash.
+        verify(surfaceFreezer).setLayer(t, 1);
+        verify(surfaceFreezer).setRelativeLayer(t, relativeParent, 2);
+        verify(surfaceAnimator, never()).setLayer(any(), anyInt());
+        verify(surfaceAnimator, never()).setRelativeLayer(any(), any(), anyInt());
+    }
+
+    @Test
+    public void testStartChangeTransitionWhenPreviousIsNotFinished() {
+        final WindowContainer container = createTaskFragmentWithParentTask(
+                createTask(mDisplayContent), false);
+        container.mSurfaceControl = mock(SurfaceControl.class);
+        final SurfaceAnimator surfaceAnimator = container.mSurfaceAnimator;
+        final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer;
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        spyOn(container);
+        spyOn(surfaceAnimator);
+        spyOn(surfaceFreezer);
+        doReturn(t).when(container).getPendingTransaction();
+        doReturn(t).when(container).getSyncTransaction();
+
+        // Leash and snapshot created for change transition.
+        container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
+        // Can't really take a snapshot, manually set one.
+        surfaceFreezer.mSnapshot = mock(SurfaceFreezer.Snapshot.class);
+
+        assertNotNull(surfaceFreezer.mLeash);
+        assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
+
+        // Start animation: surfaceAnimator take over the leash and snapshot from surfaceFreezer.
+        container.applyAnimationUnchecked(null /* lp */, true /* enter */,
+                TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */,
+                null /* sources */);
+
+        assertNull(surfaceFreezer.mLeash);
+        assertNull(surfaceFreezer.mSnapshot);
+        assertNotNull(surfaceAnimator.mLeash);
+        assertNotNull(surfaceAnimator.mSnapshot);
+        final SurfaceControl prevLeash = surfaceAnimator.mLeash;
+        final SurfaceFreezer.Snapshot prevSnapshot = surfaceAnimator.mSnapshot;
+
+        // Prepare another change transition.
+        container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
+        surfaceFreezer.mSnapshot = mock(SurfaceFreezer.Snapshot.class);
+
+        assertNotNull(surfaceFreezer.mLeash);
+        assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
+        assertNotEquals(prevLeash, container.getAnimationLeash());
+
+        // Start another animation before the previous one is finished, it should reset the previous
+        // one, but not change the current one.
+        container.applyAnimationUnchecked(null /* lp */, true /* enter */,
+                TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */,
+                null /* sources */);
+
+        verify(container, never()).onAnimationLeashLost(any());
+        verify(surfaceFreezer, never()).unfreeze(any());
+        assertNotNull(surfaceAnimator.mLeash);
+        assertNotNull(surfaceAnimator.mSnapshot);
+        assertEquals(surfaceAnimator.mLeash, container.getAnimationLeash());
+        assertNotEquals(prevLeash, surfaceAnimator.mLeash);
+        assertNotEquals(prevSnapshot, surfaceAnimator.mSnapshot);
+
+        // Clean up after animation finished.
+        surfaceAnimator.mInnerAnimationFinishedCallback.onAnimationFinished(
+                ANIMATION_TYPE_APP_TRANSITION, surfaceAnimator.getAnimation());
+
+        verify(container).onAnimationLeashLost(any());
+        assertNull(surfaceAnimator.mLeash);
+        assertNull(surfaceAnimator.mSnapshot);
+    }
+
     /* Used so we can gain access to some protected members of the {@link WindowContainer} class */
     private static class TestWindowContainer extends WindowContainer<TestWindowContainer> {
         private final int mLayer;
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
index a1f89ec..316309c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
@@ -35,6 +35,7 @@
 import android.view.Gravity;
 import android.view.InsetsSource;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.WindowManager;
 
 import androidx.test.filters.FlakyTest;
@@ -57,12 +58,14 @@
 public class WindowFrameTests extends WindowTestsBase {
 
     private DisplayContent mTestDisplayContent;
+    private DisplayFrames mTestDisplayFrames;
 
     @Before
     public void setUp() throws Exception {
         DisplayInfo testDisplayInfo = new DisplayInfo(mDisplayInfo);
         testDisplayInfo.displayCutout = null;
         mTestDisplayContent = createNewDisplay(testDisplayInfo);
+        mTestDisplayFrames = mTestDisplayContent.mDisplayFrames;
     }
 
     // Do not use this function directly in the tests below. Instead, use more explicit function
@@ -99,7 +102,7 @@
         // Here the window has FILL_PARENT, FILL_PARENT
         // so we expect it to fill the entire available frame.
         w.getWindowFrames().setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, 0, 0, 1000, 1000);
         assertRelFrame(w, 0, 0, 1000, 1000);
 
@@ -108,14 +111,14 @@
         // and we use mRequestedWidth/mRequestedHeight
         w.mAttrs.width = 300;
         w.mAttrs.height = 300;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         // Explicit width and height without requested width/height
         // gets us nothing.
         assertFrame(w, 0, 0, 0, 0);
 
         w.mRequestedWidth = 300;
         w.mRequestedHeight = 300;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         // With requestedWidth/Height we can freely choose our size within the
         // parent bounds.
         assertFrame(w, 0, 0, 300, 300);
@@ -128,14 +131,14 @@
         w.mRequestedWidth = -1;
         w.mAttrs.width = 100;
         w.mAttrs.height = 100;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, 0, 0, 100, 100);
         w.mAttrs.flags = 0;
 
         // But sizes too large will be clipped to the containing frame
         w.mRequestedWidth = 1200;
         w.mRequestedHeight = 1200;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, 0, 0, 1000, 1000);
 
         // Before they are clipped though windows will be shifted
@@ -143,7 +146,7 @@
         w.mAttrs.y = 300;
         w.mRequestedWidth = 1000;
         w.mRequestedHeight = 1000;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, 0, 0, 1000, 1000);
 
         // If there is room to move around in the parent frame the window will be shifted according
@@ -153,18 +156,18 @@
         w.mRequestedWidth = 300;
         w.mRequestedHeight = 300;
         w.mAttrs.gravity = Gravity.RIGHT | Gravity.TOP;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, 700, 0, 1000, 300);
         assertRelFrame(w, 700, 0, 1000, 300);
         w.mAttrs.gravity = Gravity.RIGHT | Gravity.BOTTOM;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, 700, 700, 1000, 1000);
         assertRelFrame(w, 700, 700, 1000, 1000);
         // Window specified  x and y are interpreted as offsets in the opposite
         // direction of gravity
         w.mAttrs.x = 100;
         w.mAttrs.y = 100;
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, 600, 600, 900, 900);
         assertRelFrame(w, 600, 600, 900, 900);
     }
@@ -191,7 +194,7 @@
         final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
         final WindowFrames windowFrames = w.getWindowFrames();
         windowFrames.setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         // For non fullscreen tasks the containing frame is based off the
         // task bounds not the parent frame.
         assertEquals(resolvedTaskBounds, w.getFrame());
@@ -204,7 +207,7 @@
         final int cfBottom = logicalHeight / 2;
         final Rect cf = new Rect(0, 0, cfRight, cfBottom);
         windowFrames.setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertEquals(resolvedTaskBounds, w.getFrame());
         assertEquals(0, w.getRelativeFrame().left);
         assertEquals(0, w.getRelativeFrame().top);
@@ -233,7 +236,7 @@
         final Rect pf = new Rect(0, 0, logicalWidth, logicalHeight);
         final WindowFrames windowFrames = w.getWindowFrames();
         windowFrames.setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         // For non fullscreen tasks the containing frame is based off the
         // task bounds not the parent frame.
         assertFrame(w, taskLeft, taskTop, taskRight, taskBottom);
@@ -249,7 +252,7 @@
         task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         task.setBounds(null);
         windowFrames.setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, cf);
     }
 
@@ -274,7 +277,9 @@
         imeFrame.top = 400;
         imeSource.setFrame(imeFrame);
         imeSource.setVisible(true);
-        w.updateRequestedVisibility(state);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_IME, true);
+        w.setRequestedVisibilities(requestedVisibilities);
         w.mAboveInsetsState.addSource(imeSource);
 
         // With no insets or system decor all the frames incoming from PhoneWindowManager
@@ -285,7 +290,7 @@
         final Rect winRect = new Rect(200, 200, 300, 500);
         task.setBounds(winRect);
         w.getWindowFrames().setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, winRect.left, imeFrame.top - winRect.height(), winRect.right, imeFrame.top);
 
         // Now check that it won't get moved beyond the top
@@ -293,7 +298,7 @@
         task.setBounds(winRect);
         w.setBounds(winRect);
         w.getWindowFrames().setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, winRect.left, 0, winRect.right, winRect.height());
 
         // Now we have status bar. Check that it won't go into the status bar area.
@@ -301,14 +306,14 @@
         statusBarFrame.bottom = 60;
         state.getSource(ITYPE_STATUS_BAR).setFrame(statusBarFrame);
         w.getWindowFrames().setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertFrame(w, winRect.left, statusBarFrame.bottom, winRect.right,
                 statusBarFrame.bottom + winRect.height());
 
         // Check that it's moved back without ime insets
         state.removeSource(ITYPE_IME);
         w.getWindowFrames().setFrames(pf, pf);
-        w.computeFrame();
+        w.computeFrame(mTestDisplayFrames);
         assertEquals(winRect, w.getFrame());
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index d9aa871..a91298f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -56,6 +56,7 @@
 import android.view.IWindowSessionCallback;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.View;
 import android.view.WindowManager;
 
@@ -107,9 +108,9 @@
         Task tappedTask = createTaskInRootTask(tappedRootTask, 0 /* userId */);
         spyOn(mWm.mAtmService);
 
-        mWm.handleTaskFocusChange(tappedTask);
+        mWm.handleTaskFocusChange(tappedTask, null /* window */);
 
-        verify(mWm.mAtmService).setFocusedTask(tappedTask.mTaskId);
+        verify(mWm.mAtmService).setFocusedTask(tappedTask.mTaskId, null);
     }
 
     @Test
@@ -128,9 +129,9 @@
         Task tappedTask = createTaskInRootTask(tappedRootTask, 0 /* userId */);
         spyOn(mWm.mAtmService);
 
-        mWm.handleTaskFocusChange(tappedTask);
+        mWm.handleTaskFocusChange(tappedTask, null /* window */);
 
-        verify(mWm.mAtmService, never()).setFocusedTask(tappedTask.mTaskId);
+        verify(mWm.mAtmService, never()).setFocusedTask(tappedTask.mTaskId, null);
     }
 
     @Test
@@ -151,9 +152,9 @@
         Task tappedTask = createTaskInRootTask(tappedRootTask, 0 /* userId */);
         spyOn(mWm.mAtmService);
 
-        mWm.handleTaskFocusChange(tappedTask);
+        mWm.handleTaskFocusChange(tappedTask, null /* window */);
 
-        verify(mWm.mAtmService).setFocusedTask(tappedTask.mTaskId);
+        verify(mWm.mAtmService).setFocusedTask(tappedTask.mTaskId, null);
     }
 
     @Test
@@ -278,7 +279,7 @@
                 .getWindowType(eq(windowContextToken));
 
         mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, DEFAULT_DISPLAY,
-                UserHandle.USER_SYSTEM, new InsetsState(), null, new InsetsState(),
+                UserHandle.USER_SYSTEM, new InsetsVisibilities(), null, new InsetsState(),
                 new InsetsSourceControl[0]);
 
         verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(),
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index ab496cf..a482bda 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -42,8 +42,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-import static com.android.server.wm.DisplayArea.Type.ABOVE_TASKS;
-import static com.android.server.wm.Task.ActivityState.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowContainer.SYNC_STATE_READY;
 
@@ -62,6 +61,7 @@
 
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityOptions;
 import android.app.ActivityTaskManager.RootTaskInfo;
 import android.app.IRequestFinishCallback;
 import android.app.PictureInPictureParams;
@@ -70,7 +70,6 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
@@ -81,7 +80,9 @@
 import android.window.ITaskOrganizer;
 import android.window.IWindowContainerTransactionCallback;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
@@ -346,7 +347,7 @@
     @Test
     public void testDisplayAreaTransaction() {
         removeGlobalMinSizeRestriction();
-        final DisplayArea displayArea = new DisplayArea<>(mWm, ABOVE_TASKS, "DisplayArea");
+        final DisplayArea displayArea = mDisplayContent.getDefaultTaskDisplayArea();
         testTransaction(displayArea);
     }
 
@@ -364,7 +365,7 @@
                 .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
         testSetWindowingMode(rootTask);
 
-        final DisplayArea displayArea = new DisplayArea<>(mWm, ABOVE_TASKS, "DisplayArea");
+        final DisplayArea displayArea = mDisplayContent.getDefaultTaskDisplayArea();
         displayArea.setWindowingMode(WINDOWING_MODE_FREEFORM);
         testSetWindowingMode(displayArea);
     }
@@ -779,8 +780,7 @@
         @Override
         public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { }
         @Override
-        public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) { }
+        public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { }
         @Override
         public void copySplashScreenView(int taskId) { }
         @Override
@@ -1259,21 +1259,42 @@
     @Test
     public void testStartTasksInTransaction() {
         WindowContainerTransaction wct = new WindowContainerTransaction();
-        Bundle testOptions = new Bundle();
-        testOptions.putInt("test", 20);
+        ActivityOptions testOptions = ActivityOptions.makeBasic();
+        testOptions.setTransientLaunch();
         wct.startTask(1, null /* options */);
-        wct.startTask(2, testOptions);
-        spyOn(mWm.mAtmService);
-        doReturn(START_CANCELED).when(mWm.mAtmService).startActivityFromRecents(anyInt(), any());
+        wct.startTask(2, testOptions.toBundle());
+        spyOn(mWm.mAtmService.mTaskSupervisor);
+        doReturn(START_CANCELED).when(mWm.mAtmService.mTaskSupervisor).startActivityFromRecents(
+                anyInt(), anyInt(), anyInt(), any());
         clearInvocations(mWm.mAtmService);
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
 
-        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
-        verify(mWm.mAtmService, times(1)).startActivityFromRecents(eq(1), bundleCaptor.capture());
-        assertTrue(bundleCaptor.getValue().isEmpty());
+        verify(mWm.mAtmService.mTaskSupervisor, times(1)).startActivityFromRecents(
+                anyInt(), anyInt(), eq(1), any());
 
-        verify(mWm.mAtmService, times(1)).startActivityFromRecents(eq(2), bundleCaptor.capture());
-        assertEquals(20, bundleCaptor.getValue().getInt("test"));
+        final ArgumentCaptor<SafeActivityOptions> optionsCaptor =
+                ArgumentCaptor.forClass(SafeActivityOptions.class);
+        verify(mWm.mAtmService.mTaskSupervisor, times(1)).startActivityFromRecents(
+                anyInt(), anyInt(), eq(2), optionsCaptor.capture());
+        assertTrue(optionsCaptor.getValue().getOriginalOptions().getTransientLaunch());
+    }
+
+    @Test
+    public void testResumeTopsWhenLeavingPinned() {
+        final ActivityRecord record = makePipableActivity();
+        final Task rootTask = record.getRootTask();
+
+        clearInvocations(mWm.mAtmService.mRootWindowContainer);
+        final WindowContainerTransaction t = new WindowContainerTransaction();
+        WindowContainerToken wct = rootTask.mRemoteToken.toWindowContainerToken();
+        t.setWindowingMode(wct, WINDOWING_MODE_PINNED);
+        mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
+        verify(mWm.mAtmService.mRootWindowContainer).resumeFocusedTasksTopActivities();
+
+        clearInvocations(mWm.mAtmService.mRootWindowContainer);
+        t.setWindowingMode(wct, WINDOWING_MODE_FULLSCREEN);
+        mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
+        verify(mWm.mAtmService.mRootWindowContainer).resumeFocusedTasksTopActivities();
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index ed18d26..c56b614 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -23,11 +23,18 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.ActivityRecord.State.PAUSED;
+import static com.android.server.wm.ActivityRecord.State.PAUSING;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.ActivityRecord.State.STARTED;
+import static com.android.server.wm.ActivityRecord.State.STOPPED;
+import static com.android.server.wm.ActivityRecord.State.STOPPING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -43,6 +50,7 @@
 import android.content.pm.ServiceInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.LocaleList;
 import android.platform.test.annotations.Presubmit;
 
 import org.junit.Before;
@@ -311,17 +319,17 @@
 
         callbackResult[0] = 0;
         activity.mVisibleRequested = false;
-        activity.setState(Task.ActivityState.PAUSED, "test");
+        activity.setState(PAUSED, "test");
         mWpc.computeOomAdjFromActivities(callback);
         assertEquals(paused, callbackResult[0]);
 
         callbackResult[0] = 0;
-        activity.setState(Task.ActivityState.STOPPING, "test");
+        activity.setState(STOPPING, "test");
         mWpc.computeOomAdjFromActivities(callback);
         assertEquals(stopping, callbackResult[0]);
 
         callbackResult[0] = 0;
-        activity.setState(Task.ActivityState.STOPPED, "test");
+        activity.setState(STOPPED, "test");
         mWpc.computeOomAdjFromActivities(callback);
         assertEquals(other, callbackResult[0]);
     }
@@ -332,25 +340,25 @@
         spyOn(tracker);
         final ActivityRecord activity = createActivityRecord(mWpc);
         activity.mVisibleRequested = true;
-        activity.setState(Task.ActivityState.STARTED, "test");
+        activity.setState(STARTED, "test");
 
         verify(tracker).onAnyActivityVisible(mWpc);
         assertTrue(mWpc.hasVisibleActivities());
 
-        activity.setState(Task.ActivityState.RESUMED, "test");
+        activity.setState(RESUMED, "test");
 
         verify(tracker).onActivityResumedWhileVisible(mWpc);
         assertTrue(tracker.hasResumedActivity(mWpc.mUid));
 
         activity.makeFinishingLocked();
-        activity.setState(Task.ActivityState.PAUSING, "test");
+        activity.setState(PAUSING, "test");
 
         assertFalse(tracker.hasResumedActivity(mWpc.mUid));
         assertTrue(mWpc.hasForegroundActivities());
 
         activity.setVisibility(false);
         activity.mVisibleRequested = false;
-        activity.setState(Task.ActivityState.STOPPED, "test");
+        activity.setState(STOPPED, "test");
 
         verify(tracker).onAllActivitiesInvisible(mWpc);
         assertFalse(mWpc.hasVisibleActivities());
@@ -365,8 +373,9 @@
     public void testTopActivityUiModeChangeScheduleConfigChange() {
         final ActivityRecord activity = createActivityRecord(mWpc);
         activity.mVisibleRequested = true;
-        doReturn(true).when(activity).setOverrideNightMode(anyInt());
-        mWpc.updateNightModeForAllActivities(Configuration.UI_MODE_NIGHT_YES);
+        doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any());
+        mWpc.updateAppSpecificSettingsForAllActivities(Configuration.UI_MODE_NIGHT_YES,
+                LocaleList.forLanguageTags("en-XA"));
         verify(activity).ensureActivityConfiguration(anyInt(), anyBoolean());
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index d88ac25..e6ad68a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.view.InsetsState.ITYPE_IME;
@@ -85,6 +86,7 @@
 import android.view.InputWindowHandle;
 import android.view.InsetsSource;
 import android.view.InsetsState;
+import android.view.InsetsVisibilities;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 
@@ -437,9 +439,9 @@
                 .setWindow(statusBar, null /* frameProvider */, null /* imeFrameProvider */);
         mDisplayContent.getInsetsStateController().onBarControlTargetChanged(
                 app, null /* fakeTopControlling */, app, null /* fakeNavControlling */);
-        final InsetsState state = new InsetsState();
-        state.getSource(ITYPE_STATUS_BAR).setVisible(false);
-        app.updateRequestedVisibility(state);
+        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
+        app.setRequestedVisibilities(requestedVisibilities);
         mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_STATUS_BAR)
                 .updateClientVisibility(app);
         waitUntilHandlersIdle();
@@ -834,8 +836,7 @@
         WindowState sameTokenWindow = createWindow(null, TYPE_BASE_APPLICATION, mAppWindow.mToken,
                 "SameTokenWindow");
         mDisplayContent.setImeLayeringTarget(mAppWindow);
-        sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
         assertTrue(sameTokenWindow.needsRelativeLayeringToIme());
         sameTokenWindow.removeImmediately();
         assertFalse(sameTokenWindow.needsRelativeLayeringToIme());
@@ -847,8 +848,7 @@
         WindowState sameTokenWindow = createWindow(null, TYPE_APPLICATION_STARTING,
                 mAppWindow.mToken, "SameTokenWindow");
         mDisplayContent.setImeLayeringTarget(mAppWindow);
-        sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(
-                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
         assertFalse(sameTokenWindow.needsRelativeLayeringToIme());
     }
 
@@ -980,4 +980,19 @@
         assertNotNull(state.peekSource(ITYPE_IME));
         assertTrue(state.getSource(ITYPE_IME).isVisible());
     }
+
+    @Test
+    public void testRequestedVisibility() {
+        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        app.mActivityRecord.setVisible(false);
+        app.mActivityRecord.setVisibility(false /* visible */, false /* deferHidingClient */);
+        assertFalse(app.isVisibleRequested());
+
+        // It doesn't have a surface yet, but should still be visible requested.
+        app.setHasSurface(false);
+        app.mActivityRecord.setVisibility(true /* visible */, false /* deferHidingClient */);
+
+        assertFalse(app.isVisible());
+        assertTrue(app.isVisibleRequested());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 611b3f5..b2d4eea 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -21,6 +21,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
@@ -29,11 +30,13 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.os.Process.SYSTEM_UID;
 import static android.view.View.VISIBLE;
+import static android.view.ViewRootImpl.INSETS_LAYOUT_GENERALIZATION;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
@@ -53,6 +56,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.server.wm.StartingSurfaceController.DEBUG_ENABLE_SHELL_DRAWER;
 import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
 import static com.android.server.wm.WindowStateAnimator.HAS_DRAWN;
 
 import static org.junit.Assert.assertEquals;
@@ -63,6 +67,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.IApplicationThread;
@@ -72,7 +77,6 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
-import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.os.Build;
 import android.os.Bundle;
@@ -83,10 +87,12 @@
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.DisplayInfo;
+import android.view.Gravity;
 import android.view.IDisplayWindowInsetsController;
 import android.view.IWindow;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.View;
@@ -94,6 +100,8 @@
 import android.view.WindowManager.DisplayImePolicy;
 import android.window.ITransitionPlayer;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
+import android.window.TaskFragmentOrganizer;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
 
@@ -132,6 +140,9 @@
     DisplayInfo mDisplayInfo = new DisplayInfo();
     DisplayContent mDefaultDisplay;
 
+    static final int STATUS_BAR_HEIGHT = 10;
+    static final int NAV_BAR_HEIGHT = 15;
+
     /**
      * It is {@link #mDefaultDisplay} by default. If the test class or method is annotated with
      * {@link UseTestDisplay}, it will be an additional display.
@@ -212,6 +223,10 @@
         // {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier},
         // may be set on some device form factors.
         mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
+        // Ensure letterbox reachability treatment isn't overridden on any device target.
+        // {@link com.android.internal.R.bool.config_letterboxIsReachabilityEnabled},
+        // may be set on some device form factors.
+        mAtm.mWindowManager.mLetterboxConfiguration.setIsReachabilityEnabled(false);
 
         checkDeviceSpecificOverridesNotApplied();
     }
@@ -219,12 +234,9 @@
     @After
     public void tearDown() throws Exception {
         // Revert back to device overrides.
-        mAtm.mWindowManager.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(
-                mContext.getResources().getFloat(
-                        com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio));
-        mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
-                mContext.getResources().getFloat(
-                    com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier));
+        mAtm.mWindowManager.mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
+        mAtm.mWindowManager.mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
+        mAtm.mWindowManager.mLetterboxConfiguration.resetIsReachabilityEnabled();
     }
 
     /**
@@ -268,6 +280,14 @@
         }
         if (addAll || ArrayUtils.contains(requestedWindows, W_STATUS_BAR)) {
             mStatusBarWindow = createCommonWindow(null, TYPE_STATUS_BAR, "mStatusBarWindow");
+            if (INSETS_LAYOUT_GENERALIZATION) {
+                mStatusBarWindow.mAttrs.height = STATUS_BAR_HEIGHT;
+                mStatusBarWindow.mAttrs.gravity = Gravity.TOP;
+                mStatusBarWindow.mAttrs.layoutInDisplayCutoutMode =
+                        LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+                mStatusBarWindow.setRequestedSize(WindowManager.LayoutParams.MATCH_PARENT,
+                        STATUS_BAR_HEIGHT);
+            }
         }
         if (addAll || ArrayUtils.contains(requestedWindows, W_NOTIFICATION_SHADE)) {
             mNotificationShadeWindow = createCommonWindow(null, TYPE_NOTIFICATION_SHADE,
@@ -275,6 +295,15 @@
         }
         if (addAll || ArrayUtils.contains(requestedWindows, W_NAVIGATION_BAR)) {
             mNavBarWindow = createCommonWindow(null, TYPE_NAVIGATION_BAR, "mNavBarWindow");
+            if (INSETS_LAYOUT_GENERALIZATION) {
+                mNavBarWindow.mAttrs.height = NAV_BAR_HEIGHT;
+                mNavBarWindow.mAttrs.gravity = Gravity.BOTTOM;
+                mNavBarWindow.mAttrs.paramsForRotation = new WindowManager.LayoutParams[4];
+                for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) {
+                    mNavBarWindow.mAttrs.paramsForRotation[rot] =
+                            getNavBarLayoutParamsForRotation(rot);
+                }
+            }
         }
         if (addAll || ArrayUtils.contains(requestedWindows, W_DOCK_DIVIDER)) {
             mDockedDividerWindow = createCommonWindow(null, TYPE_DOCK_DIVIDER,
@@ -302,6 +331,37 @@
         waitUntilHandlersIdle();
     }
 
+    private WindowManager.LayoutParams getNavBarLayoutParamsForRotation(int rotation) {
+        int width = WindowManager.LayoutParams.MATCH_PARENT;
+        int height = WindowManager.LayoutParams.MATCH_PARENT;
+        int gravity = Gravity.BOTTOM;
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            switch (rotation) {
+                case ROTATION_UNDEFINED:
+                case Surface.ROTATION_0:
+                case Surface.ROTATION_180:
+                    height = NAV_BAR_HEIGHT;
+                    break;
+                case Surface.ROTATION_90:
+                    gravity = Gravity.RIGHT;
+                    width = NAV_BAR_HEIGHT;
+                    break;
+                case Surface.ROTATION_270:
+                    gravity = Gravity.LEFT;
+                    width = NAV_BAR_HEIGHT;
+                    break;
+            }
+        }
+        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR);
+        lp.width = width;
+        lp.height = height;
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            lp.gravity = gravity;
+        }
+        return lp;
+    }
+
     void beforeCreateTestDisplay() {
         // Called before display is created.
     }
@@ -533,7 +593,7 @@
     Task createTaskInRootTask(Task rootTask, int userId) {
         final Task task = new TaskBuilder(rootTask.mTaskSupervisor)
                 .setUserId(userId)
-                .setParentTask(rootTask)
+                .setParentTaskFragment(rootTask)
                 .build();
         return task;
     }
@@ -619,6 +679,35 @@
         activity.mVisibleRequested = true;
     }
 
+    /**
+     * Creates a {@link TaskFragment} and attach it to the {@code parentTask}.
+     *
+     * @param parentTask the {@link Task} this TaskFragment is going to be attached
+     * @param createEmbeddedTask Sets to {@code true} to create an embedded Task for this
+     *                           TaskFragment. Otherwise, create a {@link ActivityRecord}.
+     * @return the created TaskFragment
+     */
+    static TaskFragment createTaskFragmentWithParentTask(@NonNull Task parentTask,
+            boolean createEmbeddedTask) {
+        final TaskFragmentBuilder builder = new TaskFragmentBuilder(parentTask.mAtmService)
+                .setParentTask(parentTask);
+        if (createEmbeddedTask) {
+            builder.createEmbeddedTask();
+        } else {
+            builder.createActivityCount(1);
+        }
+        return builder.build();
+    }
+
+    static TaskFragment createTaskFragmentWithEmbeddedActivity(@NonNull Task parentTask,
+            TaskFragmentOrganizer organizer) {
+        return new TaskFragmentBuilder(parentTask.mAtmService)
+                .setParentTask(parentTask)
+                .createActivityCount(1)
+                .setOrganizer(organizer)
+                .build();
+    }
+
     /** Creates a {@link DisplayContent} that supports IME and adds it to the system. */
     DisplayContent createNewDisplay() {
         return createNewDisplayWithImeSupport(DISPLAY_IME_POLICY_LOCAL);
@@ -992,7 +1081,7 @@
                         // Apply the root activity info and intent
                         .setActivityInfo(aInfo)
                         .setIntent(intent)
-                        .setParentTask(mParentTask).build();
+                        .setParentTaskFragment(mParentTask).build();
             } else if (mTask == null && mParentTask != null && DisplayContent.alwaysCreateRootTask(
                     mParentTask.getWindowingMode(), mParentTask.getActivityType())) {
                 // The parent task can be the task root.
@@ -1052,6 +1141,81 @@
         }
     }
 
+    static class TaskFragmentBuilder {
+        private final ActivityTaskManagerService mAtm;
+        private Task mParentTask;
+        private boolean mCreateParentTask;
+        private boolean mCreateEmbeddedTask;
+        private int mCreateActivityCount = 0;
+        @Nullable
+        private TaskFragmentOrganizer mOrganizer;
+        private IBinder mFragmentToken;
+
+        TaskFragmentBuilder(ActivityTaskManagerService service) {
+            mAtm = service;
+        }
+
+        TaskFragmentBuilder setCreateParentTask() {
+            mCreateParentTask = true;
+            return this;
+        }
+
+        TaskFragmentBuilder setParentTask(Task task) {
+            mParentTask = task;
+            return this;
+        }
+
+        /** Creates a child embedded Task and its Activity */
+        TaskFragmentBuilder createEmbeddedTask() {
+            mCreateEmbeddedTask = true;
+            return this;
+        }
+
+        TaskFragmentBuilder createActivityCount(int count) {
+            mCreateActivityCount = count;
+            return this;
+        }
+
+        TaskFragmentBuilder setOrganizer(@Nullable TaskFragmentOrganizer organizer) {
+            mOrganizer = organizer;
+            return this;
+        }
+
+        TaskFragmentBuilder setFragmentToken(@Nullable IBinder fragmentToken) {
+            mFragmentToken = fragmentToken;
+            return this;
+        }
+
+        TaskFragment build() {
+            SystemServicesTestRule.checkHoldsLock(mAtm.mGlobalLock);
+
+            final TaskFragment taskFragment = new TaskFragment(mAtm, mFragmentToken,
+                    mOrganizer != null);
+            if (mParentTask == null && mCreateParentTask) {
+                mParentTask = new TaskBuilder(mAtm.mTaskSupervisor).build();
+            }
+            if (mParentTask != null) {
+                mParentTask.addChild(taskFragment, POSITION_TOP);
+            }
+            if (mCreateEmbeddedTask) {
+                new TaskBuilder(mAtm.mTaskSupervisor)
+                        .setParentTaskFragment(taskFragment)
+                        .setCreateActivity(true)
+                        .build();
+            }
+            while (mCreateActivityCount > 0) {
+                final ActivityRecord activity = new ActivityBuilder(mAtm).build();
+                taskFragment.addChild(activity);
+                mCreateActivityCount--;
+            }
+            if (mOrganizer != null) {
+                taskFragment.setTaskFragmentOrganizer(
+                        mOrganizer.getOrganizerToken(), 10000 /* pid */);
+            }
+            return taskFragment;
+        }
+    }
+
     /**
      * Builder for creating new tasks.
      */
@@ -1072,7 +1236,7 @@
         private IVoiceInteractionSession mVoiceSession;
 
         private boolean mCreateParentTask = false;
-        private Task mParentTask;
+        private TaskFragment mParentTaskFragment;
 
         private boolean mCreateActivity = false;
 
@@ -1156,8 +1320,8 @@
             return this;
         }
 
-        TaskBuilder setParentTask(Task parentTask) {
-            mParentTask = parentTask;
+        TaskBuilder setParentTaskFragment(TaskFragment parentTaskFragment) {
+            mParentTaskFragment = parentTaskFragment;
             return this;
         }
 
@@ -1170,12 +1334,13 @@
             SystemServicesTestRule.checkHoldsLock(mSupervisor.mService.mGlobalLock);
 
             // Create parent task.
-            if (mParentTask == null && mCreateParentTask) {
-                mParentTask = mTaskDisplayArea.createRootTask(
+            if (mParentTaskFragment == null && mCreateParentTask) {
+                mParentTaskFragment = mTaskDisplayArea.createRootTask(
                         WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
             }
-            if (mParentTask != null && !Mockito.mockingDetails(mParentTask).isSpy()) {
-                spyOn(mParentTask);
+            if (mParentTaskFragment != null
+                    && !Mockito.mockingDetails(mParentTaskFragment).isSpy()) {
+                spyOn(mParentTaskFragment);
             }
 
             // Create task.
@@ -1203,13 +1368,15 @@
                     .setOnTop(mOnTop)
                     .setVoiceSession(mVoiceSession);
             final Task task;
-            if (mParentTask == null) {
+            if (mParentTaskFragment == null) {
                 task = builder.setActivityType(mActivityType)
                         .setParent(mTaskDisplayArea)
                         .build();
             } else {
-                task = builder.setParent(mParentTask).build();
-                mParentTask.moveToFront("build-task");
+                task = builder.setParent(mParentTaskFragment).build();
+                if (mParentTaskFragment.asTask() != null) {
+                    mParentTaskFragment.asTask().moveToFront("build-task");
+                }
             }
             spyOn(task);
             task.mUserId = mUserId;
@@ -1290,12 +1457,11 @@
             }
         }
         @Override
-        public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) {
+        public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
             synchronized (mWMService.mGlobalLock) {
-                final IBinder appToken = mTaskAppMap.get(taskId);
+                final IBinder appToken = mTaskAppMap.get(removalInfo.taskId);
                 if (appToken != null) {
-                    mTaskAppMap.remove(taskId);
+                    mTaskAppMap.remove(removalInfo.taskId);
                     final ActivityRecord activity = mWMService.mRoot.getActivityRecord(
                             appToken);
                     WindowState win = mAppWindowMap.remove(appToken);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index d967891..22ea3d5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
@@ -37,6 +38,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.WindowStateAnimator.PRESERVED_SURFACE_LAYER;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -493,4 +495,27 @@
         assertZOrderGreaterThan(mTransaction, mNavBarWindow.mToken.getSurfaceControl(),
                 mDisplayContent.getImeContainer().getSurfaceControl());
     }
+
+    @Test
+    public void testPopupWindowAndParentIsImeTarget_expectHigherThanIme_inMultiWindow() {
+        // Simulate the app window is in multi windowing mode and being IME target
+        mAppWindow.getConfiguration().windowConfiguration.setWindowingMode(
+                WINDOWING_MODE_MULTI_WINDOW);
+        mDisplayContent.setImeLayeringTarget(mAppWindow);
+        mDisplayContent.setImeInputTarget(mAppWindow);
+
+        // Create a popupWindow
+        assertWindowHigher(mImeWindow, mAppWindow);
+        final WindowState popupWindow = createWindow(mAppWindow, TYPE_APPLICATION_PANEL,
+                mDisplayContent, "PopupWindow");
+        spyOn(popupWindow);
+
+        mDisplayContent.assignChildLayers(mTransaction);
+
+        // Verify the surface layer of the popupWindow should higher than IME
+        verify(popupWindow).needsRelativeLayeringToIme();
+        assertThat(popupWindow.needsRelativeLayeringToIme()).isTrue();
+        assertZOrderGreaterThan(mTransaction, popupWindow.getSurfaceControl(),
+                mDisplayContent.getImeContainer().getSurfaceControl());
+    }
 }
diff --git a/services/uwb/java/com/android/server/uwb/UwbInjector.java b/services/uwb/java/com/android/server/uwb/UwbInjector.java
index a7a0500..64f1da1 100644
--- a/services/uwb/java/com/android/server/uwb/UwbInjector.java
+++ b/services/uwb/java/com/android/server/uwb/UwbInjector.java
@@ -21,13 +21,10 @@
 
 import android.annotation.NonNull;
 import android.content.AttributionSource;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.PermissionChecker;
 import android.os.IBinder;
 import android.os.ServiceManager;
-import android.provider.Settings;
-import android.uwb.AdapterState;
 import android.uwb.IUwbAdapter;
 
 
@@ -83,23 +80,4 @@
                 mContext, UWB_RANGING, -1, attributionSource, message);
         return permissionCheckResult == PERMISSION_GRANTED;
     }
-
-    /** Returns true if UWB state saved in Settings is enabled. */
-    public boolean isPersistedUwbStateEnabled() {
-        final ContentResolver cr = mContext.getContentResolver();
-        try {
-            return Settings.Global.getInt(cr, Settings.Global.UWB_ENABLED)
-                    == AdapterState.STATE_ENABLED_ACTIVE;
-        } catch (Settings.SettingNotFoundException e) {
-            Settings.Global.putInt(cr, Settings.Global.UWB_ENABLED,
-                AdapterState.STATE_ENABLED_ACTIVE);
-            return true;
-        }
-    }
-
-    /** Returns true if airplane mode is turned on. */
-    public boolean isAirplaneModeOn() {
-        return Settings.Global.getInt(mContext.getContentResolver(),
-                Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
-    }
 }
diff --git a/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java
index 70d6aab..4dd26a6 100644
--- a/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java
+++ b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java
@@ -18,19 +18,13 @@
 
 import android.annotation.NonNull;
 import android.content.AttributionSource;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
-import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.uwb.AdapterState;
 import android.uwb.IUwbAdapter;
 import android.uwb.IUwbAdapterStateCallbacks;
 import android.uwb.IUwbRangingCallbacks;
@@ -231,20 +225,13 @@
         mVendorUwbAdapter = null;
     }
 
-    private synchronized IUwbAdapter getVendorUwbAdapter()
-            throws IllegalStateException, RemoteException {
+    private synchronized IUwbAdapter getVendorUwbAdapter() throws IllegalStateException {
         if (mVendorUwbAdapter != null) return mVendorUwbAdapter;
         mVendorUwbAdapter = mUwbInjector.getVendorService();
         if (mVendorUwbAdapter == null) {
             throw new IllegalStateException("No vendor service found!");
         }
         Log.i(TAG, "Retrieved vendor service");
-        long token = Binder.clearCallingIdentity();
-        try {
-            mVendorUwbAdapter.setEnabled(isEnabled());
-        } finally {
-          Binder.restoreCallingIdentity(token);
-        }
         linkToVendorServiceDeath();
         return mVendorUwbAdapter;
     }
@@ -252,7 +239,6 @@
     UwbServiceImpl(@NonNull Context context, @NonNull UwbInjector uwbInjector) {
         mContext = context;
         mUwbInjector = uwbInjector;
-        registerAirplaneModeReceiver();
     }
 
     private void enforceUwbPrivilegedPermission() {
@@ -334,34 +320,6 @@
 
     @Override
     public synchronized void setEnabled(boolean enabled) throws RemoteException {
-        persistUwbState(enabled);
-        getVendorUwbAdapter().setEnabled(isEnabled());
-    }
-
-    private void persistUwbState(boolean enabled) {
-        final ContentResolver cr = mContext.getContentResolver();
-        int state = enabled ? AdapterState.STATE_ENABLED_ACTIVE : AdapterState.STATE_DISABLED;
-        Settings.Global.putInt(cr, Settings.Global.UWB_ENABLED, state);
-    }
-
-    private void registerAirplaneModeReceiver() {
-        mContext.registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                handleAirplaneModeEvent();
-            }
-        }, new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
-    }
-
-    private void handleAirplaneModeEvent() {
-        try {
-            getVendorUwbAdapter().setEnabled(isEnabled());
-        } catch (RemoteException e) {
-            Log.e(TAG, "Unable to set UWB Adapter state.", e);
-        }
-    }
-
-    private boolean isEnabled() {
-        return mUwbInjector.isPersistedUwbStateEnabled() && !mUwbInjector.isAirplaneModeOn();
+        getVendorUwbAdapter().setEnabled(enabled);
     }
 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 4dc83ae..f05dd63 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -23,8 +23,11 @@
 import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_UNKNOWN;
 import static android.service.voice.HotwordDetectionService.KEY_INITIALIZATION_STATUS;
 
+import static com.android.server.voiceinteraction.SoundTriggerSessionPermissionsDecorator.enforcePermissionForPreflight;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.AppOpsManager;
 import android.content.ComponentName;
 import android.content.ContentCaptureOptions;
 import android.content.Context;
@@ -930,12 +933,11 @@
     // TODO: Share this code with SoundTriggerMiddlewarePermission.
     private void enforcePermissionsForDataDelivery() {
         Binder.withCleanCallingIdentity(() -> {
-            // Hack to make sure we show the mic privacy-indicator since the Trusted Hotword
-            // requirement isn't being enforced for now. Normally, we would note the HOTWORD op here
-            // instead.
-            enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
-                    RECORD_AUDIO, OP_MESSAGE);
-
+            enforcePermissionForPreflight(mContext, mVoiceInteractorIdentity, RECORD_AUDIO);
+            int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
+            mContext.getSystemService(AppOpsManager.class).noteOpNoThrow(hotwordOp,
+                    mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+                    mVoiceInteractorIdentity.attributionTag, OP_MESSAGE);
             enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
                     CAPTURE_AUDIO_HOTWORD, OP_MESSAGE);
         });
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 9ea2b7b..8445ed4 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -167,6 +167,16 @@
     public void onStart() {
         publishBinderService(Context.VOICE_INTERACTION_MANAGER_SERVICE, mServiceStub);
         publishLocalService(VoiceInteractionManagerInternal.class, new LocalService());
+        mAmInternal.setVoiceInteractionManagerProvider(
+                new ActivityManagerInternal.VoiceInteractionManagerProvider() {
+                    @Override
+                    public void notifyActivityEventChanged() {
+                        if (DEBUG) {
+                            Slog.d(TAG, "call notifyActivityEventChanged");
+                        }
+                        mServiceStub.notifyActivityEventChanged();
+                    }
+                });
     }
 
     @Override
@@ -386,6 +396,14 @@
             return mImpl.supportsLocalVoiceInteraction();
         }
 
+        void notifyActivityEventChanged() {
+            synchronized (this) {
+                if (mImpl == null) return;
+
+                Binder.withCleanCallingIdentity(() -> mImpl.notifyActivityEventChangedLocked());
+            }
+        }
+
         @Override
         public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
                 throws RemoteException {
@@ -1109,6 +1127,40 @@
             }
         }
 
+        @Override
+        public void startListeningVisibleActivityChanged(@NonNull IBinder token) {
+            synchronized (this) {
+                if (mImpl == null) {
+                    Slog.w(TAG, "startListeningVisibleActivityChanged without running"
+                            + " voice interaction service");
+                    return;
+                }
+                final long caller = Binder.clearCallingIdentity();
+                try {
+                    mImpl.startListeningVisibleActivityChangedLocked(token);
+                } finally {
+                    Binder.restoreCallingIdentity(caller);
+                }
+            }
+        }
+
+        @Override
+        public void stopListeningVisibleActivityChanged(@NonNull IBinder token) {
+            synchronized (this) {
+                if (mImpl == null) {
+                    Slog.w(TAG, "stopListeningVisibleActivityChanged without running"
+                            + " voice interaction service");
+                    return;
+                }
+                final long caller = Binder.clearCallingIdentity();
+                try {
+                    mImpl.stopListeningVisibleActivityChangedLocked(token);
+                } finally {
+                    Binder.restoreCallingIdentity(caller);
+                }
+            }
+        }
+
         //----------------- Hotword Detection/Validation APIs --------------------------------//
 
         @Override
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 558a9ac..52c5b6bb 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -414,6 +414,44 @@
         return mInfo.getSupportsLocalInteraction();
     }
 
+    public void startListeningVisibleActivityChangedLocked(@NonNull IBinder token) {
+        if (DEBUG) {
+            Slog.d(TAG, "startListeningVisibleActivityChangedLocked: token=" + token);
+        }
+        if (mActiveSession == null || token != mActiveSession.mToken) {
+            Slog.w(TAG, "startListeningVisibleActivityChangedLocked does not match"
+                    + " active session");
+            return;
+        }
+        mActiveSession.startListeningVisibleActivityChangedLocked();
+    }
+
+    public void stopListeningVisibleActivityChangedLocked(@NonNull IBinder token) {
+        if (DEBUG) {
+            Slog.d(TAG, "stopListeningVisibleActivityChangedLocked: token=" + token);
+        }
+        if (mActiveSession == null || token != mActiveSession.mToken) {
+            Slog.w(TAG, "stopListeningVisibleActivityChangedLocked does not match"
+                    + " active session");
+            return;
+        }
+        mActiveSession.stopListeningVisibleActivityChangedLocked();
+    }
+
+    public void notifyActivityEventChangedLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyActivityEventChangedLocked");
+        }
+        if (mActiveSession == null || !mActiveSession.mShown) {
+            if (DEBUG) {
+                Slog.d(TAG, "notifyActivityEventChangedLocked not allowed on no session or"
+                        + " hidden session");
+            }
+            return;
+        }
+        mActiveSession.notifyActivityEventChangedLocked();
+    }
+
     public void updateStateLocked(
             @NonNull Identity voiceInteractorIdentity,
             @Nullable PersistableBundle options,
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
index 08e9703..90ccec8 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
@@ -56,6 +56,7 @@
 import android.provider.Settings;
 import android.service.voice.IVoiceInteractionSession;
 import android.service.voice.IVoiceInteractionSessionService;
+import android.service.voice.VisibleActivityInfo;
 import android.service.voice.VoiceInteractionService;
 import android.service.voice.VoiceInteractionSession;
 import android.util.Slog;
@@ -71,16 +72,20 @@
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.uri.UriGrantsManagerInternal;
 import com.android.server.wm.ActivityAssistInfo;
+import com.android.server.wm.ActivityTaskManagerInternal;
 
 import java.io.PrintWriter;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 
 final class VoiceInteractionSessionConnection implements ServiceConnection,
         AssistDataRequesterCallbacks {
 
     static final String TAG = "VoiceInteractionServiceManager";
+    static final boolean DEBUG = false;
     static final int POWER_BOOST_TIMEOUT_MS = Integer.parseInt(
             System.getProperty("vendor.powerhal.interaction.max", "200"));
     static final int BOOST_TIMEOUT_MS = 300;
@@ -114,6 +119,10 @@
     ArrayList<IVoiceInteractionSessionShowCallback> mPendingShowCallbacks = new ArrayList<>();
     private List<ActivityAssistInfo> mPendingHandleAssistWithoutData = new ArrayList<>();
     AssistDataRequester mAssistDataRequester;
+    private boolean mListeningVisibleActivity;
+    private final ScheduledExecutorService mScheduledExecutorService =
+            Executors.newSingleThreadScheduledExecutor();
+    private final List<VisibleActivityInfo> mVisibleActivityInfos = new ArrayList<>();
     private final PowerManagerInternal mPowerManagerInternal;
     private PowerBoostSetter mSetPowerBoostRunnable;
     private final Handler mFgHandler;
@@ -496,6 +505,8 @@
     }
 
     public void cancelLocked(boolean finishTask) {
+        mListeningVisibleActivity = false;
+        mVisibleActivityInfos.clear();
         hideLocked();
         mCanceled = true;
         if (mBound) {
@@ -569,6 +580,156 @@
         mPendingShowCallbacks.clear();
     }
 
+    void startListeningVisibleActivityChangedLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "startListeningVisibleActivityChangedLocked");
+        }
+        mListeningVisibleActivity = true;
+        mVisibleActivityInfos.clear();
+
+        mScheduledExecutorService.execute(() -> {
+            if (DEBUG) {
+                Slog.d(TAG, "call updateVisibleActivitiesLocked from enable listening");
+            }
+            synchronized (mLock) {
+                updateVisibleActivitiesLocked();
+            }
+        });
+    }
+
+    void stopListeningVisibleActivityChangedLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "stopListeningVisibleActivityChangedLocked");
+        }
+        mListeningVisibleActivity = false;
+        mVisibleActivityInfos.clear();
+    }
+
+    void notifyActivityEventChangedLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyActivityEventChangedLocked");
+        }
+        if (!mListeningVisibleActivity) {
+            if (DEBUG) {
+                Slog.d(TAG, "not enable listening visible activity");
+            }
+            return;
+        }
+        mScheduledExecutorService.execute(() -> {
+            if (DEBUG) {
+                Slog.d(TAG, "call updateVisibleActivitiesLocked from activity event");
+            }
+            synchronized (mLock) {
+                updateVisibleActivitiesLocked();
+            }
+        });
+    }
+
+    private List<VisibleActivityInfo> getVisibleActivityInfosLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "getVisibleActivityInfosLocked");
+        }
+        List<ActivityAssistInfo> allVisibleActivities =
+                LocalServices.getService(ActivityTaskManagerInternal.class)
+                        .getTopVisibleActivities();
+        if (DEBUG) {
+            Slog.d(TAG,
+                    "getVisibleActivityInfosLocked: allVisibleActivities=" + allVisibleActivities);
+        }
+        if (allVisibleActivities == null || allVisibleActivities.isEmpty()) {
+            Slog.w(TAG, "no visible activity");
+            return null;
+        }
+        final int count = allVisibleActivities.size();
+        final List<VisibleActivityInfo> visibleActivityInfos = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            ActivityAssistInfo info = allVisibleActivities.get(i);
+            if (DEBUG) {
+                Slog.d(TAG, " : activityToken=" + info.getActivityToken()
+                        + ", assistToken=" + info.getAssistToken()
+                        + ", taskId=" + info.getTaskId());
+            }
+            visibleActivityInfos.add(
+                    new VisibleActivityInfo(info.getTaskId(), info.getAssistToken()));
+        }
+        return visibleActivityInfos;
+    }
+
+    private void updateVisibleActivitiesLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "updateVisibleActivitiesLocked");
+        }
+        if (mSession == null) {
+            return;
+        }
+        if (!mShown || !mListeningVisibleActivity || mCanceled) {
+            return;
+        }
+        final List<VisibleActivityInfo> newVisibleActivityInfos = getVisibleActivityInfosLocked();
+
+        if (newVisibleActivityInfos == null || newVisibleActivityInfos.isEmpty()) {
+            updateVisibleActivitiesChangedLocked(mVisibleActivityInfos,
+                    VisibleActivityInfo.TYPE_ACTIVITY_REMOVED);
+            mVisibleActivityInfos.clear();
+            return;
+        }
+        if (mVisibleActivityInfos.isEmpty()) {
+            updateVisibleActivitiesChangedLocked(newVisibleActivityInfos,
+                    VisibleActivityInfo.TYPE_ACTIVITY_ADDED);
+            mVisibleActivityInfos.addAll(newVisibleActivityInfos);
+            return;
+        }
+
+        final List<VisibleActivityInfo> addedActivities = new ArrayList<>();
+        final List<VisibleActivityInfo> removedActivities = new ArrayList<>();
+
+        removedActivities.addAll(mVisibleActivityInfos);
+        for (int i = 0; i < newVisibleActivityInfos.size(); i++) {
+            final VisibleActivityInfo candidateVisibleActivityInfo = newVisibleActivityInfos.get(i);
+            if (!removedActivities.isEmpty() && removedActivities.contains(
+                    candidateVisibleActivityInfo)) {
+                removedActivities.remove(candidateVisibleActivityInfo);
+            } else {
+                addedActivities.add(candidateVisibleActivityInfo);
+            }
+        }
+
+        if (!addedActivities.isEmpty()) {
+            updateVisibleActivitiesChangedLocked(addedActivities,
+                    VisibleActivityInfo.TYPE_ACTIVITY_ADDED);
+        }
+        if (!removedActivities.isEmpty()) {
+            updateVisibleActivitiesChangedLocked(removedActivities,
+                    VisibleActivityInfo.TYPE_ACTIVITY_REMOVED);
+        }
+
+        mVisibleActivityInfos.clear();
+        mVisibleActivityInfos.addAll(newVisibleActivityInfos);
+    }
+
+    private void updateVisibleActivitiesChangedLocked(
+            List<VisibleActivityInfo> visibleActivityInfos, int type) {
+        if (visibleActivityInfos == null || visibleActivityInfos.isEmpty()) {
+            return;
+        }
+        if (mSession == null) {
+            return;
+        }
+        try {
+            for (int i = 0; i < visibleActivityInfos.size(); i++) {
+                mSession.updateVisibleActivityInfo(visibleActivityInfos.get(i), type);
+            }
+        } catch (RemoteException e) {
+            if (DEBUG) {
+                Slog.w(TAG, "updateVisibleActivitiesChangedLocked RemoteException : " + e);
+            }
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "updateVisibleActivitiesChangedLocked type=" + type + ", count="
+                    + visibleActivityInfos.size());
+        }
+    }
+
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
         synchronized (mLock) {
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index 4d81b5e..7a424c8 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -308,6 +308,12 @@
         return checkPrivilegedReadPermissionOrCarrierPrivilegePermission(
                 context, subId, callingPackage, callingFeatureId, message, false, reportFailure);
     }
+
+    private static void throwSecurityExceptionAsUidDoesNotHaveAccess(String message, int uid) {
+        throw new SecurityException(message + ": The uid " + uid
+                + " does not meet the requirements to access device identifiers.");
+    }
+
     /**
      * Checks whether the app with the given pid/uid can read device identifiers.
      *
@@ -343,9 +349,14 @@
 
         LegacyPermissionManager permissionManager = (LegacyPermissionManager)
                 context.getSystemService(Context.LEGACY_PERMISSION_SERVICE);
-        if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message, callingFeatureId,
-                pid, uid) == PackageManager.PERMISSION_GRANTED) {
-            return true;
+        try {
+            if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message,
+                    callingFeatureId,
+                    pid, uid) == PackageManager.PERMISSION_GRANTED) {
+                return true;
+            }
+        } catch (SecurityException se) {
+            throwSecurityExceptionAsUidDoesNotHaveAccess(message, uid);
         }
 
         if (reportFailure) {
@@ -410,8 +421,8 @@
                 return false;
             }
         }
-        throw new SecurityException(message + ": The user " + uid
-                + " does not meet the requirements to access device identifiers.");
+        throwSecurityExceptionAsUidDoesNotHaveAccess(message, uid);
+        return true;
     }
 
     /**
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 217a72b..7731e09 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -25,11 +25,17 @@
 
 android_test {
     name: "FlickerTests",
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     manifest: "AndroidManifest.xml",
     test_config: "AndroidTest.xml",
     platform_apis: true,
     certificate: "platform",
+    optimize: {
+        enabled: false,
+    },
     test_suites: ["device-tests"],
     libs: ["android.test.runner"],
     static_libs: [
@@ -46,6 +52,9 @@
 java_library {
     name: "wm-flicker-common-assertions",
     platform_apis: true,
+    optimize: {
+        enabled: false,
+    },
     srcs: [
         "src/**/*Assertions.java",
         "src/**/*Assertions.kt",
@@ -56,20 +65,23 @@
     static_libs: [
         "flickerlib",
         "truth-prebuilt",
-        "app-helpers-core"
+        "app-helpers-core",
     ],
 }
 
 java_library {
     name: "wm-flicker-common-app-helpers",
     platform_apis: true,
+    optimize: {
+        enabled: false,
+    },
     srcs: [
-        "**/helpers/*"
+        "**/helpers/*",
     ],
     static_libs: [
         "flickerlib",
         "flickertestapplib",
         "truth-prebuilt",
-        "app-helpers-core"
+        "app-helpers-core",
     ],
-}
\ No newline at end of file
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
index a540dff..64cb790 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -14,210 +14,157 @@
  * limitations under the License.
  */
 
+@file:JvmName("CommonAssertions")
 package com.android.server.wm.flicker
 
-import android.platform.helpers.IAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper.Companion.NAV_BAR_LAYER_NAME
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper.Companion.NAV_BAR_WINDOW_NAME
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper.Companion.STATUS_BAR_LAYER_NAME
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper.Companion.STATUS_BAR_WINDOW_NAME
+import com.android.server.wm.traces.common.FlickerComponentName
 
-val HOME_WINDOW_TITLE = arrayOf("Wallpaper", "Launcher")
+val LAUNCHER_COMPONENT = FlickerComponentName("com.google.android.apps.nexuslauncher",
+        "com.google.android.apps.nexuslauncher.NexusLauncherActivity")
 
-fun FlickerTestParameter.statusBarWindowIsAlwaysVisible() {
+/**
+ * Checks that [FlickerComponentName.STATUS_BAR] window is visible and above the app windows in
+ * all WM trace entries
+ */
+fun FlickerTestParameter.statusBarWindowIsVisible() {
     assertWm {
-        this.showsAboveAppWindow(STATUS_BAR_WINDOW_NAME)
+        this.isAboveAppWindowVisible(FlickerComponentName.STATUS_BAR)
     }
 }
 
-fun FlickerTestParameter.navBarWindowIsAlwaysVisible() {
+/**
+ * Checks that [FlickerComponentName.NAV_BAR] window is visible and above the app windows in
+ * all WM trace entries
+ */
+fun FlickerTestParameter.navBarWindowIsVisible() {
     assertWm {
-        this.showsAboveAppWindow(NAV_BAR_WINDOW_NAME)
+        this.isAboveAppWindowVisible(FlickerComponentName.NAV_BAR)
     }
 }
 
-fun FlickerTestParameter.launcherReplacesAppWindowAsTopWindow(testApp: IAppHelper) {
-    assertWm {
-        this.showsAppWindowOnTop(testApp.getPackage())
-            .then()
-            .showsAppWindowOnTop(*HOME_WINDOW_TITLE)
-    }
-}
-
-fun FlickerTestParameter.launcherWindowBecomesVisible() {
-    assertWm {
-        this.hidesBelowAppWindow(*HOME_WINDOW_TITLE)
-            .then()
-            .showsBelowAppWindow(*HOME_WINDOW_TITLE)
-    }
-}
-
-fun FlickerTestParameter.launcherWindowBecomesInvisible() {
-    assertWm {
-        this.showsBelowAppWindow(*HOME_WINDOW_TITLE)
-            .then()
-            .hidesBelowAppWindow(*HOME_WINDOW_TITLE)
-    }
-}
-
-fun FlickerTestParameter.appWindowAlwaysVisibleOnTop(packageName: String) {
-    assertWm {
-        this.showsAppWindowOnTop(packageName)
-    }
-}
-
-fun FlickerTestParameter.appWindowBecomesVisible(appName: String) {
-    assertWm {
-        this.hidesAppWindow(appName)
-            .then()
-            .showsAppWindow(appName)
-    }
-}
-
-fun FlickerTestParameter.appWindowBecomesInVisible(appName: String) {
-    assertWm {
-        this.showsAppWindow(appName)
-            .then()
-            .hidesAppWindow(appName)
-    }
-}
-
+/**
+ * If [allStates] is true, checks if the stack space of all displays is fully covered
+ * by any visible layer, during the whole transitions
+ *
+ * Otherwise, checks if the stack space of all displays is fully covered
+ * by any visible layer, at the start and end of the transition
+ *
+ * @param allStates if all states should be checked, othersie, just initial and final
+ */
 @JvmOverloads
-fun FlickerTestParameter.noUncoveredRegions(
-    beginRotation: Int,
-    endRotation: Int = beginRotation,
-    allStates: Boolean = true
-) {
-    val startingBounds = WindowUtils.getDisplayBounds(beginRotation)
-    val endingBounds = WindowUtils.getDisplayBounds(endRotation)
+fun FlickerTestParameter.entireScreenCovered(allStates: Boolean = true) {
     if (allStates) {
         assertLayers {
-            if (startingBounds == endingBounds) {
-                this.coversAtLeast(startingBounds)
-            } else {
-                this.coversAtLeast(startingBounds)
-                    .then()
-                    .coversAtLeast(endingBounds)
+            this.invoke("entireScreenCovered") { entry ->
+                entry.entry.displays.forEach { display ->
+                    entry.visibleRegion().coversAtLeast(display.layerStackSpace)
+                }
             }
         }
     } else {
         assertLayersStart {
-            this.visibleRegion().coversAtLeast(startingBounds)
+            this.entry.displays.forEach { display ->
+                this.visibleRegion().coversAtLeast(display.layerStackSpace)
+            }
         }
         assertLayersEnd {
-            this.visibleRegion().coversAtLeast(endingBounds)
+            this.entry.displays.forEach { display ->
+                this.visibleRegion().coversAtLeast(display.layerStackSpace)
+            }
         }
     }
 }
 
-@JvmOverloads
-fun FlickerTestParameter.navBarLayerIsAlwaysVisible(rotatesScreen: Boolean = false) {
-    if (rotatesScreen) {
-        assertLayers {
-            this.isVisible(NAV_BAR_LAYER_NAME)
-                .then()
-                .isInvisible(NAV_BAR_LAYER_NAME)
-                .then()
-                .isVisible(NAV_BAR_LAYER_NAME)
-        }
-    } else {
-        assertLayers {
-            this.isVisible(NAV_BAR_LAYER_NAME)
-        }
-    }
-}
-
-@JvmOverloads
-fun FlickerTestParameter.statusBarLayerIsAlwaysVisible(rotatesScreen: Boolean = false) {
-    if (rotatesScreen) {
-        assertLayers {
-            this.isVisible(STATUS_BAR_LAYER_NAME)
-                .then()
-                .isInvisible(STATUS_BAR_LAYER_NAME)
-                .then()
-                .isVisible(STATUS_BAR_LAYER_NAME)
-        }
-    } else {
-        assertLayers {
-            this.isVisible(STATUS_BAR_LAYER_NAME)
-        }
-    }
-}
-
-@JvmOverloads
-fun FlickerTestParameter.navBarLayerRotatesAndScales(
-    beginRotation: Int,
-    endRotation: Int = beginRotation
-) {
-    val startingPos = WindowUtils.getNavigationBarPosition(beginRotation)
-    val endingPos = WindowUtils.getNavigationBarPosition(endRotation)
-
+/**
+ * Checks that [FlickerComponentName.NAV_BAR] layer is visible at the start and end of the SF
+ * trace
+ */
+fun FlickerTestParameter.navBarLayerIsVisible() {
     assertLayersStart {
-        this.visibleRegion(NAV_BAR_LAYER_NAME).coversExactly(startingPos)
+        this.isVisible(FlickerComponentName.NAV_BAR)
     }
     assertLayersEnd {
-        this.visibleRegion(NAV_BAR_LAYER_NAME).coversExactly(endingPos)
+        this.isVisible(FlickerComponentName.NAV_BAR)
     }
 }
 
-@JvmOverloads
-fun FlickerTestParameter.statusBarLayerRotatesScales(
-    beginRotation: Int,
-    endRotation: Int = beginRotation
-) {
-    val startingPos = WindowUtils.getStatusBarPosition(beginRotation)
-    val endingPos = WindowUtils.getStatusBarPosition(endRotation)
-
+/**
+ * Checks that [FlickerComponentName.STATUS_BAR] layer is visible at the start and end of the SF
+ * trace
+ */
+fun FlickerTestParameter.statusBarLayerIsVisible() {
     assertLayersStart {
-        this.visibleRegion(STATUS_BAR_LAYER_NAME).coversExactly(startingPos)
+        this.isVisible(FlickerComponentName.STATUS_BAR)
     }
     assertLayersEnd {
-        this.visibleRegion(STATUS_BAR_LAYER_NAME).coversExactly(endingPos)
+        this.isVisible(FlickerComponentName.STATUS_BAR)
     }
 }
 
-fun FlickerTestParameter.appLayerReplacesLauncher(appName: String) {
+fun FlickerTestParameter.navBarLayerRotatesAndScales() {
+    assertLayersStart {
+        val display = this.entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("There is no display!")
+        this.visibleRegion(FlickerComponentName.NAV_BAR)
+                .coversExactly(WindowUtils.getNavigationBarPosition(display))
+    }
+    assertLayersEnd {
+        val display = this.entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("There is no display!")
+        this.visibleRegion(FlickerComponentName.NAV_BAR)
+                .coversExactly(WindowUtils.getNavigationBarPosition(display))
+    }
+}
+
+fun FlickerTestParameter.statusBarLayerRotatesScales() {
+    assertLayersStart {
+        val display = this.entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("There is no display!")
+        this.visibleRegion(FlickerComponentName.STATUS_BAR)
+            .coversExactly(WindowUtils.getStatusBarPosition(display))
+    }
+    assertLayersEnd {
+        val display = this.entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("There is no display!")
+        this.visibleRegion(FlickerComponentName.STATUS_BAR)
+            .coversExactly(WindowUtils.getStatusBarPosition(display))
+    }
+}
+
+/**
+ * Asserts that:
+ *     [originalLayer] is visible at the start of the trace
+ *     [originalLayer] becomes invisible during the trace and (in the same entry) [newLayer]
+ *         becomes visible
+ *     [newLayer] remains visible until the end of the trace
+ *
+ * @param originalLayer Layer that should be visible at the start
+ * @param newLayer Layer that should be visible at the end
+ * @param ignoreSnapshot If the snapshot layer should be ignored during the transition
+ *     (useful mostly for app launch)
+ */
+fun FlickerTestParameter.replacesLayer(
+    originalLayer: FlickerComponentName,
+    newLayer: FlickerComponentName,
+    ignoreSnapshot: Boolean = false
+) {
     assertLayers {
-        this.isVisible(*HOME_WINDOW_TITLE)
-            .then()
-            .isVisible(appName)
+        val assertion = this.isVisible(originalLayer)
+        if (ignoreSnapshot) {
+            assertion.then()
+                    .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+        }
+        assertion.then().isVisible(newLayer)
+    }
+
+    assertLayersStart {
+        this.isVisible(originalLayer)
+                .isInvisible(newLayer)
+    }
+
+    assertLayersEnd {
+        this.isInvisible(originalLayer)
+                .isVisible(newLayer)
     }
 }
-
-fun FlickerTestParameter.launcherLayerReplacesApp(testApp: IAppHelper) {
-    assertLayers {
-        this.isVisible(testApp.getPackage())
-            .then()
-            .isInvisible(testApp.getPackage())
-            .isVisible(*HOME_WINDOW_TITLE)
-    }
-}
-
-fun FlickerTestParameter.layerBecomesVisible(packageName: String) {
-    assertLayers {
-        this.isInvisible(packageName)
-            .then()
-            .isVisible(packageName)
-    }
-}
-
-fun FlickerTestParameter.layerBecomesInvisible(packageName: String) {
-    assertLayers {
-        this.isVisible(packageName)
-            .then()
-            .isInvisible(packageName)
-    }
-}
-
-fun FlickerTestParameter.focusChanges(vararg windows: String) {
-    assertEventLog {
-        this.focusChanges(windows)
-    }
-}
-
-fun FlickerTestParameter.focusDoesNotChange() {
-    assertEventLog {
-        this.focusDoesNotChange()
-    }
-}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
index 71184c2..9f26c31 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
@@ -1,3 +1,4 @@
+
 /*
  * Copyright (C) 2020 The Android Open Source Project
  *
@@ -16,26 +17,53 @@
 
 package com.android.server.wm.flicker.close
 
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.FixMethodOrder
+import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
 
 /**
  * Test app closes by pressing back button
+ *
  * To run this test: `atest FlickerTests:CloseAppBackButtonTest`
+ *
+ * Actions:
+ *     Make sure no apps are running on the device
+ *     Launch an app [testApp] and wait animation to complete
+ *     Press back button
+ *
+ * To run only the presubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Presubmit`
+ *
+ * To run only the postsubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Postsubmit`
+ *
+ * To run only the flaky assertions add: `--
+ *      --module-arg FlickerTests:include-annotation:androidx.test.filters.FlakyTest`
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [CloseAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
+@Group4
 class CloseAppBackButtonTest(testSpec: FlickerTestParameter) : CloseAppTransition(testSpec) {
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = {
@@ -46,7 +74,18 @@
             }
         }
 
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
+
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): List<FlickerTestParameter> {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
index 6786279..795766f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
@@ -16,26 +16,53 @@
 
 package com.android.server.wm.flicker.close
 
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.FixMethodOrder
+import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
 
 /**
- * Test app closes by pressing home button.
+ * Test app closes by pressing home button
+ *
  * To run this test: `atest FlickerTests:CloseAppHomeButtonTest`
+ *
+ * Actions:
+ *     Make sure no apps are running on the device
+ *     Launch an app [testApp] and wait animation to complete
+ *     Press home button
+ *
+ * To run only the presubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Presubmit`
+ *
+ * To run only the postsubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Postsubmit`
+ *
+ * To run only the flaky assertions add: `--
+ *      --module-arg FlickerTests:include-annotation:androidx.test.filters.FlakyTest`
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [CloseAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
+@Group4
 class CloseAppHomeButtonTest(testSpec: FlickerTestParameter) : CloseAppTransition(testSpec) {
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = {
@@ -46,7 +73,18 @@
             }
         }
 
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
+
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
index f7f977d..511fc26 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
@@ -18,31 +18,35 @@
 
 import android.app.Instrumentation
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
 import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.StandardAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.launcherReplacesAppWindowAsTopWindow
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.launcherLayerReplacesApp
-import com.android.server.wm.flicker.launcherWindowBecomesVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.flicker.replacesLayer
 import org.junit.Test
 
+/**
+ * Base test class for transitions that close an app back to the launcher screen
+ */
 abstract class CloseAppTransition(protected val testSpec: FlickerTestParameter) {
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
+
+    /**
+     * Specification of the test transition to execute
+     */
     protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit = {
         setup {
             eachRun {
@@ -57,6 +61,10 @@
         }
     }
 
+    /**
+     * Entry point for the test runner. It will use this method to initialize and cache
+     * flicker executions
+     */
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
@@ -64,42 +72,60 @@
         }
     }
 
+    /**
+     * Checks that the navigation bar window is visible during the whole transition
+     */
     @Presubmit
     @Test
-    open fun navBarWindowIsAlwaysVisible() {
-        testSpec.navBarWindowIsAlwaysVisible()
+    open fun navBarWindowIsVisible() {
+        testSpec.navBarWindowIsVisible()
     }
 
+    /**
+     * Checks that the status bar window is visible during the whole transition
+     */
     @Presubmit
     @Test
-    open fun statusBarWindowIsAlwaysVisible() {
-        testSpec.statusBarWindowIsAlwaysVisible()
+    open fun statusBarWindowIsVisible() {
+        testSpec.statusBarWindowIsVisible()
     }
 
-    @FlakyTest
-    @Test
-    open fun navBarLayerIsAlwaysVisible() {
-        testSpec.navBarLayerIsAlwaysVisible(rotatesScreen = testSpec.isRotated)
-    }
-
+    /**
+     * Checks that the navigation bar layer is visible during the whole transition
+     */
     @Presubmit
     @Test
-    open fun statusBarLayerIsAlwaysVisible() {
-        testSpec.statusBarLayerIsAlwaysVisible(rotatesScreen = testSpec.isRotated)
+    open fun navBarLayerIsVisible() {
+        testSpec.navBarLayerIsVisible()
     }
 
-    @FlakyTest
-    @Test
-    open fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, Surface.ROTATION_0)
-    }
-
+    /**
+     * Checks that the status bar layer is visible during the whole transition
+     */
     @Presubmit
     @Test
-    open fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0)
+    open fun statusBarLayerIsVisible() {
+        testSpec.statusBarLayerIsVisible()
     }
 
+    /**
+     * Checks the position of the navigation bar at the start and end of the transition
+     */
+    @Presubmit
+    @Test
+    open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
+
+    /**
+     * Checks the position of the status bar at the start and end of the transition
+     */
+    @Presubmit
+    @Test
+    open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    /**
+     * Checks that all windows that are visible on the trace, are visible for at least 2
+     * consecutive entries.
+     */
     @Presubmit
     @Test
     open fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
@@ -108,6 +134,10 @@
         }
     }
 
+    /**
+     * Checks that all layers that are visible on the trace, are visible for at least 2
+     * consecutive entries.
+     */
     @Presubmit
     @Test
     open fun visibleLayersShownMoreThanOneConsecutiveEntry() {
@@ -116,27 +146,47 @@
         }
     }
 
+    /**
+     * Checks that all parts of the screen are covered during the transition
+     */
     @Presubmit
     @Test
-    open fun noUncoveredRegions() {
-        testSpec.noUncoveredRegions(testSpec.config.startRotation, Surface.ROTATION_0)
-    }
+    open fun entireScreenCovered() = testSpec.entireScreenCovered()
 
+    /**
+     * Checks that [testApp] is the top visible app window at the start of the transition and
+     * that it is replaced by [LAUNCHER_COMPONENT] during the transition
+     */
     @Presubmit
     @Test
     open fun launcherReplacesAppWindowAsTopWindow() {
-        testSpec.launcherReplacesAppWindowAsTopWindow(testApp)
+        testSpec.assertWm {
+            this.isAppWindowOnTop(testApp.component)
+                    .then()
+                    .isAppWindowOnTop(LAUNCHER_COMPONENT)
+        }
     }
 
+    /**
+     * Checks that [LAUNCHER_COMPONENT] is invisible at the start of the transition and that
+     * it becomes visible during the transition
+     */
     @Presubmit
     @Test
     open fun launcherWindowBecomesVisible() {
-        testSpec.launcherWindowBecomesVisible()
+        testSpec.assertWm {
+            this.isAppWindowNotOnTop(LAUNCHER_COMPONENT)
+                    .then()
+                    .isAppWindowOnTop(LAUNCHER_COMPONENT)
+        }
     }
 
+    /**
+     * Checks that [LAUNCHER_COMPONENT] layer becomes visible when [testApp] becomes invisible
+     */
     @Presubmit
     @Test
     open fun launcherLayerReplacesApp() {
-        testSpec.launcherLayerReplacesApp(testApp)
+        testSpec.replacesLayer(testApp.component, LAUNCHER_COMPONENT)
     }
 }
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
index fad25b4..75900df 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+@file:JvmName("FlickerExtensions")
 package com.android.server.wm.flicker.helpers
 
 import com.android.server.wm.flicker.Flicker
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
index bd7c185..0b1748a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
@@ -17,9 +17,10 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
 import androidx.test.uiautomator.UiDevice
 import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 
 class ImeAppAutoFocusHelper @JvmOverloads constructor(
@@ -27,7 +28,8 @@
     private val rotation: Int,
     private val imePackageName: String = IME_PACKAGE,
     launcherName: String = ActivityOptions.IME_ACTIVITY_AUTO_FOCUS_LAUNCHER_NAME,
-    component: ComponentName = ActivityOptions.IME_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME
+    component: FlickerComponentName =
+        ActivityOptions.IME_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent()
 ) : ImeAppHelper(instr, launcherName, component) {
     override fun openIME(
         device: UiDevice,
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
index 83fddae..1c2164a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
@@ -17,19 +17,21 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
 import android.support.test.launcherhelper.ILauncherStrategy
 import android.support.test.launcherhelper.LauncherStrategyFactory
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
 import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 
 open class ImeAppHelper @JvmOverloads constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.IME_ACTIVITY_LAUNCHER_NAME,
-    component: ComponentName = ActivityOptions.IME_ACTIVITY_COMPONENT_NAME,
+    component: FlickerComponentName =
+        ActivityOptions.IME_ACTIVITY_COMPONENT_NAME.toFlickerComponent(),
     launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
             .getInstance(instr)
             .launcherStrategy
@@ -61,7 +63,8 @@
         if (wmHelper == null) {
             device.waitForIdle()
         } else {
-            wmHelper.waitImeWindowShown()
+            wmHelper.waitImeShown()
+            wmHelper.waitForAppTransitionIdle()
         }
     }
 
@@ -78,7 +81,7 @@
         if (wmHelper == null) {
             device.waitForIdle()
         } else {
-            wmHelper.waitImeWindowGone()
+            wmHelper.waitImeGone()
         }
     }
 }
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
new file mode 100644
index 0000000..be68704
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+
+class NewTasksAppHelper @JvmOverloads constructor(
+    instr: Instrumentation,
+    launcherName: String = ActivityOptions.LAUNCH_NEW_TASK_ACTIVITY_LAUNCHER_NAME,
+    component: FlickerComponentName =
+        ActivityOptions.LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME.toFlickerComponent(),
+    launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
+        .getInstance(instr)
+        .launcherStrategy
+) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    fun openNewTask(device: UiDevice, wmHelper: WindowManagerStateHelper) {
+        val button = device.wait(
+            Until.findObject(By.res(getPackage(), "launch_new_task")),
+            FIND_TIMEOUT)
+
+        require(button != null) {
+            "Button not found, this usually happens when the device " +
+                    "was left in an unknown state (e.g. in split screen)"
+        }
+        button.click()
+        wmHelper.waitForAppTransitionIdle()
+        wmHelper.waitForFullScreenApp(component)
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
new file mode 100644
index 0000000..f7ca5ce
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
+
+class NonResizeableAppHelper @JvmOverloads constructor(
+    instr: Instrumentation,
+    launcherName: String = ActivityOptions.NON_RESIZEABLE_ACTIVITY_LAUNCHER_NAME,
+    component: FlickerComponentName =
+        ActivityOptions.NON_RESIZEABLE_ACTIVITY_COMPONENT_NAME.toFlickerComponent(),
+    launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
+        .getInstance(instr)
+        .launcherStrategy
+) : StandardAppHelper(instr, launcherName, component, launcherStrategy)
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
index 02be3cf..7bab981 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
@@ -17,15 +17,17 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
 import android.support.test.launcherhelper.ILauncherStrategy
 import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
 
 class SeamlessRotationAppHelper @JvmOverloads constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.SEAMLESS_ACTIVITY_LAUNCHER_NAME,
-    component: ComponentName = ActivityOptions.SEAMLESS_ACTIVITY_COMPONENT_NAME,
+    component: FlickerComponentName =
+        ActivityOptions.SEAMLESS_ACTIVITY_COMPONENT_NAME.toFlickerComponent(),
     launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
         .getInstance(instr)
         .launcherStrategy
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
index d7cbaae..f6a8817 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
@@ -17,15 +17,17 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.content.ComponentName
 import android.support.test.launcherhelper.ILauncherStrategy
 import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
 
 class SimpleAppHelper @JvmOverloads constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.SIMPLE_ACTIVITY_LAUNCHER_NAME,
-    component: ComponentName = ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME,
+    component: FlickerComponentName =
+        ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent(),
     launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
         .getInstance(instr)
         .launcherStrategy
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
new file mode 100644
index 0000000..59e8dc8
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+
+class TwoActivitiesAppHelper @JvmOverloads constructor(
+    instr: Instrumentation,
+    launcherName: String = ActivityOptions.BUTTON_ACTIVITY_LAUNCHER_NAME,
+    component: FlickerComponentName =
+        ActivityOptions.BUTTON_ACTIVITY_COMPONENT_NAME.toFlickerComponent(),
+    launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
+        .getInstance(instr)
+        .launcherStrategy
+) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    fun openSecondActivity(device: UiDevice, wmHelper: WindowManagerStateHelper) {
+        val button = device.wait(
+                Until.findObject(By.res(getPackage(), "launch_second_activity")),
+                FIND_TIMEOUT)
+
+        require(button != null) {
+            "Button not found, this usually happens when the device " +
+                    "was left in an unknown state (e.g. in split screen)"
+        }
+        button.click()
+        wmHelper.waitForAppTransitionIdle()
+        wmHelper.waitForFullScreenApp(component)
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
index b5757fd..5e21aff 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
@@ -18,8 +18,8 @@
 
 import android.app.Instrumentation
 import android.platform.test.annotations.Presubmit
+import android.view.Surface
 import android.view.WindowManagerPolicyConstants
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
@@ -28,16 +28,16 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -46,6 +46,14 @@
 
 /**
  * Test IME window closing back to app window transitions.
+ *
+ * This test doesn't work on 90 degrees. According to the InputMethodService documentation:
+ *
+ *     Don't show if this is not explicitly requested by the user and the input method
+ *     is fullscreen. That would be too disruptive.
+ *
+ * More details on b/190352379
+ *
  * To run this test: `atest FlickerTests:CloseImeAutoOpenWindowToAppTest`
  */
 @RequiresDevice
@@ -79,37 +87,55 @@
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
     fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
         testSpec.assertWm {
-            this.visibleWindowsShownMoreThanOneConsecutiveEntry(listOf(IME_WINDOW_TITLE,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME))
+            this.visibleWindowsShownMoreThanOneConsecutiveEntry()
         }
     }
 
     @Presubmit
     @Test
-    fun imeAppWindowIsAlwaysVisible() = testSpec.imeAppWindowIsAlwaysVisible(testApp)
+    fun imeAppWindowIsAlwaysVisible() {
+        testSpec.assertWm {
+            this.isAppWindowOnTop(testApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
-    @FlakyTest
+    @Presubmit
     @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
+
+    @Presubmit
+    @Test
+    fun imeLayerVisibleStart() {
+        testSpec.assertLayersStart {
+            this.isVisible(FlickerComponentName.IME)
+        }
+    }
+
+    @Presubmit
+    @Test
+    fun imeLayerInvisibleEnd() {
+        testSpec.assertLayersEnd {
+            this.isInvisible(FlickerComponentName.IME)
+        }
+    }
 
     @Presubmit
     @Test
@@ -117,19 +143,19 @@
 
     @Presubmit
     @Test
-    fun imeAppLayerIsAlwaysVisible() = testSpec.imeAppLayerIsAlwaysVisible(testApp)
-
-    @FlakyTest
-    @Test
-    fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation)
+    fun imeAppLayerIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
     }
 
-    @FlakyTest
+    @Presubmit
     @Test
-    fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation)
-    }
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
+
+    @Presubmit
+    @Test
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
     @Presubmit
     @Test
@@ -145,8 +171,11 @@
         fun getParams(): Collection<FlickerTestParameter> {
             return FlickerTestParameterFactory.getInstance()
                 .getConfigNonRotationTests(repetitions = 5,
+                    // b/190352379 (IME doesn't show on app launch in 90 degrees)
+                    supportedRotations = listOf(Surface.ROTATION_0),
                     supportedNavigationModes = listOf(
-                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY)
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
                 )
         }
     }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
index 549e44c..0582685 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
@@ -30,15 +30,15 @@
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -47,6 +47,14 @@
 
 /**
  * Test IME window closing back to app window transitions.
+ *
+ * This test doesn't work on 90 degrees. According to the InputMethodService documentation:
+ *
+ *     Don't show if this is not explicitly requested by the user and the input method
+ *     is fullscreen. That would be too disruptive.
+ *
+ * More details on b/190352379
+ *
  * To run this test: `atest FlickerTests:CloseImeAutoOpenWindowToHomeTest`
  */
 @RequiresDevice
@@ -75,76 +83,94 @@
             transitions {
                 device.pressHome()
                 wmHelper.waitForHomeActivityVisible()
-                wmHelper.waitImeWindowGone()
+                wmHelper.waitImeGone()
             }
         }
     }
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
     fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
         testSpec.assertWm {
-            this.visibleWindowsShownMoreThanOneConsecutiveEntry(listOf(IME_WINDOW_TITLE,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME))
+            this.visibleWindowsShownMoreThanOneConsecutiveEntry()
         }
     }
 
-    @FlakyTest
+    @FlakyTest(bugId = 190189685)
     @Test
-    fun imeWindowBecomesInvisible() = testSpec.imeWindowBecomesInvisible()
-
-    @FlakyTest
-    @Test
-    fun imeAppWindowBecomesInvisible() = testSpec.imeAppWindowBecomesInvisible(testApp)
+    fun imeAppWindowBecomesInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowOnTop(testApp.component)
+                .then()
+                .isAppWindowNotOnTop(testApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation,
-        Surface.ROTATION_0)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
 
-    @FlakyTest
+    @Presubmit
+    @Test
+    fun imeLayerVisibleStart() {
+        testSpec.assertLayersStart {
+            this.isVisible(FlickerComponentName.IME)
+        }
+    }
+
+    @Presubmit
+    @Test
+    fun imeLayerInvisibleEnd() {
+        testSpec.assertLayersEnd {
+            this.isInvisible(FlickerComponentName.IME)
+        }
+    }
+
+    @Presubmit
     @Test
     fun imeLayerBecomesInvisible() = testSpec.imeLayerBecomesInvisible()
 
     @Presubmit
     @Test
-    fun imeAppLayerBecomesInvisible() = testSpec.imeAppLayerBecomesInvisible(testApp)
-
-    @FlakyTest
-    @Test
-    fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, Surface.ROTATION_0)
+    fun imeAppLayerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+                    .then()
+                    .isInvisible(testApp.component)
+        }
     }
 
     @Presubmit
     @Test
-    fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0)
-    }
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Presubmit
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
-    @FlakyTest
+    @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
+
+    @Presubmit
+    @Test
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
     @Presubmit
     @Test
     fun visibleLayersShownMoreThanOneConsecutiveEntry() {
         testSpec.assertLayers {
-            this.visibleLayersShownMoreThanOneConsecutiveEntry(
-                listOf(IME_WINDOW_TITLE, WindowManagerStateHelper.SPLASH_SCREEN_NAME))
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(listOf(
+                FlickerComponentName.IME,
+                FlickerComponentName.SPLASH_SCREEN))
         }
     }
 
@@ -154,8 +180,11 @@
         fun getParams(): Collection<FlickerTestParameter> {
             return FlickerTestParameterFactory.getInstance()
                 .getConfigNonRotationTests(repetitions = 1,
+                    // b/190352379 (IME doesn't show on app launch in 90 degrees)
+                    supportedRotations = listOf(Surface.ROTATION_0),
                     supportedNavigationModes = listOf(
-                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY)
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
                 )
         }
     }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
index 82ca074..91b3d3d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
@@ -28,15 +28,14 @@
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
-import com.android.server.wm.flicker.startRotation
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.Assume
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -61,7 +60,7 @@
         return FlickerBuilder(instrumentation).apply {
             setup {
                 test {
-                    testApp.launchViaIntent()
+                    testApp.launchViaIntent(wmHelper)
                 }
                 eachRun {
                     testApp.openIME(device, wmHelper)
@@ -80,57 +79,60 @@
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
     fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
         testSpec.assertWm {
-            this.visibleWindowsShownMoreThanOneConsecutiveEntry(listOf(IME_WINDOW_TITLE,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME))
+            this.visibleWindowsShownMoreThanOneConsecutiveEntry(listOf(
+                FlickerComponentName.IME,
+                FlickerComponentName.SPLASH_SCREEN,
+                FlickerComponentName.SNAPSHOT))
         }
     }
 
     @Presubmit
     @Test
-    fun imeAppWindowIsAlwaysVisible() = testSpec.imeAppWindowIsAlwaysVisible(testApp)
+    fun imeAppWindowIsAlwaysVisible() {
+        testSpec.assertWm {
+            this.isAppWindowOnTop(testApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun statusBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
 
     @Presubmit
     @Test
     fun navBarLayerRotatesAndScales() {
         Assume.assumeFalse(testSpec.isRotated)
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation)
+        testSpec.navBarLayerRotatesAndScales()
     }
 
     @FlakyTest
     @Test
     fun navBarLayerRotatesAndScales_Flaky() {
         Assume.assumeTrue(testSpec.isRotated)
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation)
+        testSpec.navBarLayerRotatesAndScales()
     }
 
     @Presubmit
     @Test
-    fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation)
-    }
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
     @Presubmit
     @Test
@@ -146,7 +148,11 @@
 
     @Presubmit
     @Test
-    fun imeAppLayerIsAlwaysVisible() = testSpec.imeAppLayerIsAlwaysVisible(testApp)
+    fun imeAppLayerIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
+    }
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
index 703e4a1..b589969 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
@@ -30,14 +30,13 @@
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
-import com.android.server.wm.flicker.startRotation
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -68,7 +67,7 @@
             transitions {
                 device.pressHome()
                 wmHelper.waitForHomeActivityVisible()
-                wmHelper.waitImeWindowGone()
+                wmHelper.waitImeGone()
             }
             teardown {
                 eachRun {
@@ -84,19 +83,20 @@
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
     fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
         testSpec.assertWm {
-            this.visibleWindowsShownMoreThanOneConsecutiveEntry(listOf(IME_WINDOW_TITLE,
-                WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME))
+            this.visibleWindowsShownMoreThanOneConsecutiveEntry(listOf(
+                FlickerComponentName.IME,
+                FlickerComponentName.SPLASH_SCREEN,
+                FlickerComponentName.SNAPSHOT))
         }
     }
 
@@ -106,20 +106,25 @@
 
     @FlakyTest
     @Test
-    fun imeAppWindowBecomesInvisible() = testSpec.imeAppWindowBecomesInvisible(testApp)
+    fun imeAppWindowBecomesInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(testApp.component)
+                    .then()
+                    .isAppWindowInvisible(testApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun statusBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation,
-        Surface.ROTATION_0)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
 
     @Presubmit
     @Test
@@ -127,25 +132,29 @@
 
     @Presubmit
     @Test
-    fun imeAppLayerBecomesInvisible() = testSpec.imeAppLayerBecomesInvisible(testApp)
-
-    @Presubmit
-    @Test
-    fun navBarLayerRotatesAndScales() =
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, Surface.ROTATION_0)
-
-    @Presubmit
-    @Test
-    fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0)
+    fun imeAppLayerBecomesInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+                    .then()
+                    .isInvisible(testApp.component)
+        }
     }
 
     @Presubmit
     @Test
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
+
+    @Presubmit
+    @Test
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    @Presubmit
+    @Test
     fun visibleLayersShownMoreThanOneConsecutiveEntry() {
         testSpec.assertLayers {
-            this.visibleLayersShownMoreThanOneConsecutiveEntry(
-                listOf(IME_WINDOW_TITLE, WindowManagerStateHelper.SPLASH_SCREEN_NAME))
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(listOf(
+                FlickerComponentName.IME,
+                FlickerComponentName.SPLASH_SCREEN))
         }
     }
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
index 7e34469..ba78e25 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
@@ -14,128 +14,56 @@
  * limitations under the License.
  */
 
+@file:JvmName("CommonAssertions")
 package com.android.server.wm.flicker.ime
 
-import android.platform.helpers.IAppHelper
 import com.android.server.wm.flicker.FlickerTestParameter
-
-const val IME_WINDOW_TITLE = "InputMethod"
-
-fun FlickerTestParameter.imeLayerIsAlwaysVisible(rotatesScreen: Boolean = false) {
-    if (rotatesScreen) {
-        assertLayers {
-            this.isVisible(IME_WINDOW_TITLE)
-                .then()
-                .isInvisible(IME_WINDOW_TITLE)
-                .then()
-                .isVisible(IME_WINDOW_TITLE)
-        }
-    } else {
-        assertLayers {
-            this.isVisible(IME_WINDOW_TITLE)
-        }
-    }
-}
+import com.android.server.wm.traces.common.FlickerComponentName
 
 fun FlickerTestParameter.imeLayerBecomesVisible() {
     assertLayers {
-        this.isInvisible(IME_WINDOW_TITLE)
+        this.isInvisible(FlickerComponentName.IME)
             .then()
-            .isVisible(IME_WINDOW_TITLE)
+            .isVisible(FlickerComponentName.IME)
     }
 }
 
 fun FlickerTestParameter.imeLayerBecomesInvisible() {
     assertLayers {
-        this.isVisible(IME_WINDOW_TITLE)
+        this.isVisible(FlickerComponentName.IME)
             .then()
-            .isInvisible(IME_WINDOW_TITLE)
-    }
-}
-
-fun FlickerTestParameter.imeAppLayerIsAlwaysVisible(testApp: IAppHelper) {
-    assertLayers {
-        this.isVisible(testApp.getPackage())
-    }
-}
-
-fun FlickerTestParameter.imeAppWindowIsAlwaysVisible(testApp: IAppHelper) {
-    assertWm {
-        this.showsAppWindowOnTop(testApp.getPackage())
+            .isInvisible(FlickerComponentName.IME)
     }
 }
 
 fun FlickerTestParameter.imeWindowIsAlwaysVisible(rotatesScreen: Boolean = false) {
     if (rotatesScreen) {
         assertWm {
-            this.showsNonAppWindow(IME_WINDOW_TITLE)
+            this.isNonAppWindowVisible(FlickerComponentName.IME)
                 .then()
-                .hidesNonAppWindow(IME_WINDOW_TITLE)
+                .isNonAppWindowInvisible(FlickerComponentName.IME)
                 .then()
-                .showsNonAppWindow(IME_WINDOW_TITLE)
+                .isNonAppWindowVisible(FlickerComponentName.IME)
         }
     } else {
         assertWm {
-            this.showsNonAppWindow(IME_WINDOW_TITLE)
+            this.isNonAppWindowVisible(FlickerComponentName.IME)
         }
     }
 }
 
 fun FlickerTestParameter.imeWindowBecomesVisible() {
     assertWm {
-        this.hidesNonAppWindow(IME_WINDOW_TITLE)
+        this.isNonAppWindowInvisible(FlickerComponentName.IME)
             .then()
-            .showsNonAppWindow(IME_WINDOW_TITLE)
+            .isNonAppWindowVisible(FlickerComponentName.IME)
     }
 }
 
 fun FlickerTestParameter.imeWindowBecomesInvisible() {
     assertWm {
-        this.showsNonAppWindow(IME_WINDOW_TITLE)
+        this.isNonAppWindowVisible(FlickerComponentName.IME)
             .then()
-            .hidesNonAppWindow(IME_WINDOW_TITLE)
+            .isNonAppWindowInvisible(FlickerComponentName.IME)
     }
 }
-
-fun FlickerTestParameter.imeAppWindowIsAlwaysVisible(
-    testApp: IAppHelper,
-    rotatesScreen: Boolean = false
-) {
-    if (rotatesScreen) {
-        assertWm {
-            this.showsAppWindow(testApp.getPackage())
-                .then()
-                .hidesAppWindow(testApp.getPackage())
-                .then()
-                .showsAppWindow(testApp.getPackage())
-        }
-    } else {
-        assertWm {
-            this.showsAppWindow(testApp.getPackage())
-        }
-    }
-}
-
-fun FlickerTestParameter.imeAppWindowBecomesVisible(windowName: String) {
-    assertWm {
-        this.hidesAppWindow(windowName)
-            .then()
-            .showsAppWindow(windowName)
-    }
-}
-
-fun FlickerTestParameter.imeAppWindowBecomesInvisible(testApp: IAppHelper) {
-    assertWm {
-        this.showsAppWindowOnTop(testApp.getPackage())
-            .then()
-            .appWindowNotOnTop(testApp.getPackage())
-    }
-}
-
-fun FlickerTestParameter.imeAppLayerBecomesInvisible(testApp: IAppHelper) {
-    assertLayers {
-        this.isVisible(testApp.getPackage())
-            .then()
-            .isInvisible(testApp.getPackage())
-    }
-}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
new file mode 100644
index 0000000..a9568b3
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.ime
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Presubmit
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.startRotation
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Launch an app that automatically displays the IME
+ *
+ * To run this test: `atest FlickerTests:LaunchAppShowImeOnStartTest`
+ *
+ * Actions:
+ *     Make sure no apps are running on the device
+ *     Launch an app [testApp] that automatically displays IME and wait animation to complete
+ *
+ * To run only the presubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Presubmit`
+ *
+ * To run only the postsubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Postsubmit`
+ *
+ * To run only the flaky assertions add: `--
+ *      --module-arg FlickerTests:include-annotation:androidx.test.filters.FlakyTest`
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [CloseAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LaunchAppShowImeOnStartTest(private val testSpec: FlickerTestParameter) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.config.startRotation)
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            setup {
+                eachRun {
+                    this.setRotation(testSpec.config.startRotation)
+                }
+            }
+            teardown {
+                eachRun {
+                    testApp.exit()
+                }
+            }
+            transitions {
+                testApp.launchViaIntent(wmHelper)
+                wmHelper.waitImeShown()
+            }
+        }
+    }
+
+    /**
+     * Checks that [FlickerComponentName.IME] window becomes visible during the transition
+     */
+    @Presubmit
+    @Test
+    fun imeWindowBecomesVisible() = testSpec.imeWindowBecomesVisible()
+
+    /**
+     * Checks that [FlickerComponentName.IME] layer becomes visible during the transition
+     */
+    @Presubmit
+    @Test
+    fun imeLayerBecomesVisible() = testSpec.imeLayerBecomesVisible()
+
+    /**
+     * Checks that [FlickerComponentName.IME] layer is invisible at the start of the transition
+     */
+    @Presubmit
+    @Test
+    fun imeLayerNotExistsStart() {
+        testSpec.assertLayersStart {
+            this.isInvisible(FlickerComponentName.IME)
+        }
+    }
+
+    /**
+     * Checks that [FlickerComponentName.IME] layer is visible at the end of the transition
+     */
+    @Presubmit
+    @Test
+    fun imeLayerExistsEnd() {
+        testSpec.assertLayersEnd {
+            this.isVisible(FlickerComponentName.IME)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                .getConfigNonRotationTests(
+                    repetitions = 5,
+                    supportedRotations = listOf(Surface.ROTATION_0),
+                    supportedNavigationModes = listOf(
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
+                    )
+                )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
index cae1b16..7bf0186 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
@@ -20,6 +20,7 @@
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
@@ -28,16 +29,14 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
-import com.android.server.wm.flicker.appWindowAlwaysVisibleOnTop
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -81,11 +80,11 @@
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
@@ -93,19 +92,23 @@
 
     @Presubmit
     @Test
-    fun appWindowAlwaysVisibleOnTop() = testSpec.appWindowAlwaysVisibleOnTop(testApp.`package`)
+    fun appWindowAlwaysVisibleOnTop() {
+        testSpec.assertWm {
+            this.isAppWindowOnTop(testApp.component)
+        }
+    }
 
     @Presubmit
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
 
     @Presubmit
     @Test
@@ -115,21 +118,17 @@
     @Test
     fun layerAlwaysVisible() {
         testSpec.assertLayers {
-            this.isVisible(testApp.`package`)
+            this.isVisible(testApp.component)
         }
     }
 
     @Presubmit
     @Test
-    fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation)
-    }
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
     @Presubmit
     @Test
-    fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation)
-    }
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
     @Presubmit
     @Test
@@ -139,7 +138,7 @@
         }
     }
 
-    @Presubmit
+    @FlakyTest
     @Test
     fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
         testSpec.assertWm {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
index b7673d5..f6febe9e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
@@ -17,6 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.app.Instrumentation
+import android.os.SystemProperties
 import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import android.view.WindowManagerPolicyConstants
@@ -26,23 +27,22 @@
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
 import com.android.server.wm.flicker.annotation.Group2
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
 import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.launcherWindowBecomesInvisible
-import com.android.server.wm.flicker.appLayerReplacesLauncher
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.endRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -61,7 +61,8 @@
 class ReOpenImeWindowTest(private val testSpec: FlickerTestParameter) {
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.config.startRotation)
-    private val testAppComponentName = ActivityOptions.IME_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME
+    private val isShellTransitionsEnabled =
+            SystemProperties.getBoolean("persist.debug.shell_transit", false)
 
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
@@ -73,14 +74,14 @@
                 }
                 eachRun {
                     device.pressRecentApps()
-                    wmHelper.waitImeWindowGone()
+                    wmHelper.waitImeGone()
                     wmHelper.waitForAppTransitionIdle()
                     this.setRotation(testSpec.config.startRotation)
                 }
             }
             transitions {
                 device.reopenAppFromOverview(wmHelper)
-                wmHelper.waitImeWindowShown()
+                wmHelper.waitImeShown()
             }
             teardown {
                 test {
@@ -92,72 +93,136 @@
 
     @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     @Presubmit
     @Test
     fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+        val component = FlickerComponentName("", "RecentTaskScreenshotSurface")
         testSpec.assertWm {
-            this.visibleWindowsShownMoreThanOneConsecutiveEntry()
+            this.visibleWindowsShownMoreThanOneConsecutiveEntry(
+                    ignoreWindows = listOf(FlickerComponentName.SPLASH_SCREEN,
+                        FlickerComponentName.SNAPSHOT,
+                        component)
+            )
         }
     }
 
     @Presubmit
     @Test
-    fun launcherWindowBecomesInvisible() = testSpec.launcherWindowBecomesInvisible()
+    fun launcherWindowBecomesInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(LAUNCHER_COMPONENT)
+                    .then()
+                    .isAppWindowInvisible(LAUNCHER_COMPONENT)
+        }
+    }
 
     @Presubmit
     @Test
-    fun imeWindowIsAlwaysVisible() = testSpec.imeWindowIsAlwaysVisible(true)
+    fun imeWindowIsAlwaysVisible() = testSpec.imeWindowIsAlwaysVisible(!isShellTransitionsEnabled)
 
     @Presubmit
     @Test
-    fun imeAppWindowIsAlwaysVisible() = testSpec.imeAppWindowIsAlwaysVisible(testApp, true)
+    fun imeAppWindowVisibilityLegacy() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+        // the app starts visible in live tile, and stays visible for the duration of entering
+        // and exiting overview. However, legacy transitions seem to have a bug which causes
+        // everything to restart during the test, so expect the app to disappear and come back.
+        // Since we log 1x per frame, sometimes the activity visibility and the app visibility
+        // are updated together, sometimes not, thus ignore activity check at the start
+        testSpec.assertWm {
+            this.isAppWindowVisible(testApp.component)
+                    .then()
+                    .isAppWindowInvisible(testApp.component)
+                    .then()
+                    .isAppWindowVisible(testApp.component)
+        }
+    }
+
+    @Presubmit
+    @Test
+    fun imeAppWindowVisibility() {
+        Assume.assumeTrue(isShellTransitionsEnabled)
+        // the app starts visible in live tile, and stays visible for the duration of entering
+        // and exiting overview. Since we log 1x per frame, sometimes the activity visibility
+        // and the app visibility are updated together, sometimes not, thus ignore activity
+        // check at the start
+        testSpec.assertWm {
+            this.isAppWindowVisible(testApp.component)
+        }
+    }
 
     @Presubmit
     @Test
     // During testing the launcher is always in portrait mode
-    fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation,
-        testSpec.config.endRotation)
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
 
     @Presubmit
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
 
     @Presubmit
     @Test
-    fun imeLayerIsAlwaysVisible() = testSpec.imeLayerIsAlwaysVisible(true)
-
-    @Presubmit
-    @Test
-    fun appLayerReplacesLauncher() =
-        testSpec.appLayerReplacesLauncher(testAppComponentName.className)
-
-    @Presubmit
-    @Test
-    fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(Surface.ROTATION_0, testSpec.config.endRotation)
+    fun imeLayerIsBecomesVisibleLegacy() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+        testSpec.assertLayers {
+            this.isVisible(FlickerComponentName.IME)
+                    .then()
+                    .isInvisible(FlickerComponentName.IME)
+                    .then()
+                    .isVisible(FlickerComponentName.IME)
+        }
     }
 
     @Presubmit
     @Test
-    fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(Surface.ROTATION_0, testSpec.config.endRotation)
+    fun imeLayerIsBecomesVisible() {
+        Assume.assumeTrue(isShellTransitionsEnabled)
+        testSpec.assertLayers {
+            this.isVisible(FlickerComponentName.IME)
+        }
     }
 
     @Presubmit
     @Test
+    fun appLayerReplacesLauncher() {
+        testSpec.assertLayers {
+            this.isVisible(LAUNCHER_COMPONENT)
+                .then()
+                .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+                .then()
+                .isVisible(testApp.component)
+        }
+    }
+
+    @Presubmit
+    @Test
+    fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
+
+    @Presubmit
+    @Test
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    @Presubmit
+    @Test
     fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        // depends on how much of the animation transactions are sent to SF at once
+        // sometimes this layer appears for 2-3 frames, sometimes for only 1
+        val recentTaskComponent = FlickerComponentName("", "RecentTaskScreenshotSurface")
         testSpec.assertLayers {
-            this.visibleLayersShownMoreThanOneConsecutiveEntry()
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                    listOf(FlickerComponentName.SPLASH_SCREEN,
+                        FlickerComponentName.SNAPSHOT, recentTaskComponent)
+            )
         }
     }
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
index 0cae37c..4c506b0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
@@ -18,26 +18,24 @@
 
 import android.app.Instrumentation
 import android.platform.test.annotations.Presubmit
+import android.view.Surface
 import android.view.WindowManagerPolicyConstants
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.platform.app.InstrumentationRegistry
-
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.FlickerBuilderProvider
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group2
-import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -53,11 +51,12 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group2
+@Group4
+@Presubmit
 class SwitchImeWindowsFromGestureNavTest(private val testSpec: FlickerTestParameter) {
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val testApp = SimpleAppHelper(instrumentation)
-    private val imeTestApp = ImeAppHelper(instrumentation)
+    private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, testSpec.config.startRotation)
 
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
@@ -66,7 +65,13 @@
                 eachRun {
                     this.setRotation(testSpec.config.startRotation)
                     testApp.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(testApp.component)
+                    wmHelper.waitForAppTransitionIdle()
+
                     imeTestApp.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(testApp.component)
+                    wmHelper.waitForAppTransitionIdle()
+
                     imeTestApp.openIME(device, wmHelper)
                 }
             }
@@ -74,57 +79,87 @@
                 eachRun {
                     device.pressHome()
                     wmHelper.waitForHomeActivityVisible()
-                }
-                test {
-                    imeTestApp.exit(wmHelper)
+                    testApp.exit()
+                    imeTestApp.exit()
                 }
             }
             transitions {
                 // [Step1]: Swipe right from imeTestApp to testApp task
+                createTag(TAG_IME_VISIBLE)
                 val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
-                val displayCenterX = displayBounds.bounds.width() / 2
-                device.swipe(displayCenterX, displayBounds.bounds.height(),
-                        displayBounds.bounds.width(), displayBounds.bounds.height(), 20)
+                device.swipe(0, displayBounds.bounds.height(),
+                        displayBounds.bounds.width(), displayBounds.bounds.height(), 50)
+
                 wmHelper.waitForFullScreenApp(testApp.component)
+                wmHelper.waitForAppTransitionIdle()
+                createTag(TAG_IME_INVISIBLE)
             }
             transitions {
                 // [Step2]: Swipe left to back to imeTestApp task
                 val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
-                val displayCenterX = displayBounds.bounds.width() / 2
                 device.swipe(displayBounds.bounds.width(), displayBounds.bounds.height(),
-                        displayCenterX, displayBounds.bounds.height(), 20)
+                        0, displayBounds.bounds.height(), 50)
                 wmHelper.waitForFullScreenApp(imeTestApp.component)
             }
         }
     }
 
-    @FlakyTest
     @Test
-    fun imeAppWindowIsAlwaysVisible() = testSpec.imeAppWindowIsAlwaysVisible(imeTestApp)
+    fun imeAppWindowVisibility() {
+        testSpec.assertWm {
+            isAppWindowVisible(imeTestApp.component)
+                .then()
+                .isAppWindowVisible(testApp.component)
+                .then()
+                .isAppWindowVisible(imeTestApp.component)
+        }
+    }
 
-    @FlakyTest
     @Test
-    fun imeLayerBecomesVisible() = testSpec.imeLayerBecomesVisible()
+    fun navBarLayerIsVisibleAroundSwitching() {
+        testSpec.assertLayersStart {
+            isVisible(FlickerComponentName.NAV_BAR)
+        }
+        testSpec.assertLayersEnd {
+            isVisible(FlickerComponentName.NAV_BAR)
+        }
+    }
 
-    @FlakyTest
     @Test
-    fun imeLayerBecomesInvisible() = testSpec.imeLayerBecomesInvisible()
+    fun statusBarLayerIsVisibleAroundSwitching() {
+        testSpec.assertLayersStart {
+            isVisible(FlickerComponentName.STATUS_BAR)
+        }
+        testSpec.assertLayersEnd {
+            isVisible(FlickerComponentName.STATUS_BAR)
+        }
+    }
 
-    @Presubmit
     @Test
-    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible()
+    fun imeLayerIsVisibleWhenSwitchingToImeApp() {
+        testSpec.assertLayersStart {
+            isVisible(FlickerComponentName.IME)
+        }
+        testSpec.assertLayersTag(TAG_IME_VISIBLE) {
+            isVisible(FlickerComponentName.IME)
+        }
+        testSpec.assertLayersEnd {
+            isVisible(FlickerComponentName.IME)
+        }
+    }
 
-    @FlakyTest
     @Test
-    fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible()
+    fun imeLayerIsInvisibleWhenSwitchingToTestApp() {
+        testSpec.assertLayersTag(TAG_IME_INVISIBLE) {
+            isInvisible(FlickerComponentName.IME)
+        }
+    }
 
-    @Presubmit
     @Test
-    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
 
-    @FlakyTest
     @Test
-    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible()
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
@@ -134,10 +169,13 @@
                     .getConfigNonRotationTests(
                             repetitions = 3,
                             supportedNavigationModes = listOf(
-                                    WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
                                     WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                            )
+                            ),
+                            supportedRotations = listOf(Surface.ROTATION_0)
                     )
         }
+
+        private const val TAG_IME_VISIBLE = "imeVisible"
+        private const val TAG_IME_INVISIBLE = "imeInVisible"
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt
new file mode 100644
index 0000000..f74a771
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.launch
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Presubmit
+import androidx.test.filters.RequiresDevice
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.entireScreenCovered
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.repetitions
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.TwoActivitiesAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.parser.toFlickerComponent
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test the back and forward transition between 2 activities.
+ *
+ * To run this test: `atest FlickerTests:ActivitiesTransitionTest`
+ *
+ * Actions:
+ *     Launch an app
+ *     Launch a secondary activity within the app
+ *     Close the secondary activity back to the initial one
+ *
+ * Notes:
+ *     1. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group4
+class ActivitiesTransitionTest(val testSpec: FlickerTestParameter) {
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val testApp: TwoActivitiesAppHelper = TwoActivitiesAppHelper(instrumentation)
+
+    /**
+     * Entry point for the test runner. It will use this method to initialize and cache
+     * flicker executions
+     */
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            withTestName { testSpec.name }
+            repeat { testSpec.config.repetitions }
+            setup {
+                eachRun {
+                    testApp.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(testApp.component)
+                }
+            }
+            teardown {
+                test {
+                    testApp.exit()
+                }
+            }
+            transitions {
+                testApp.openSecondActivity(device, wmHelper)
+                device.pressBack()
+                wmHelper.waitForAppTransitionIdle()
+                wmHelper.waitForFullScreenApp(testApp.component)
+            }
+        }
+    }
+
+    /**
+     * Checks that the [ActivityOptions.BUTTON_ACTIVITY_COMPONENT_NAME] activity is visible at
+     * the start of the transition, that
+     * [ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME] becomes visible during the
+     * transition, and that [ActivityOptions.BUTTON_ACTIVITY_COMPONENT_NAME] is again visible
+     * at the end
+     */
+    @Presubmit
+    @Test
+    fun finishSubActivity() {
+        val buttonActivityComponent = ActivityOptions
+            .BUTTON_ACTIVITY_COMPONENT_NAME.toFlickerComponent()
+        val imeAutoFocusActivityComponent = ActivityOptions
+            .SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent()
+        testSpec.assertWm {
+            this.isAppWindowOnTop(buttonActivityComponent)
+                .then()
+                .isAppWindowOnTop(imeAutoFocusActivityComponent)
+                .then()
+                .isAppWindowOnTop(buttonActivityComponent)
+        }
+    }
+
+    /**
+     * Checks that all parts of the screen are covered during the transition
+     */
+    @Presubmit
+    @Test
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
+
+    /**
+     * Checks that the [LAUNCHER_COMPONENT] window is not on top. The launcher cannot be
+     * asserted with `isAppWindowVisible` because it contains 2 windows with the exact same name,
+     * and both are never simultaneously visible
+     */
+    @Presubmit
+    @Test
+    fun launcherWindowNotOnTop() {
+        testSpec.assertWm {
+            this.isAppWindowNotOnTop(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that the [LAUNCHER_COMPONENT] layer is never visible during the transition
+     */
+    @Presubmit
+    @Test
+    fun launcherLayerNotVisible() {
+        testSpec.assertLayers { this.isInvisible(LAUNCHER_COMPONENT) }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(repetitions = 5)
+        }
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/CommonAssertions.kt
deleted file mode 100644
index 01e34d9..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/CommonAssertions.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.launch
-
-import android.platform.helpers.IAppHelper
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.HOME_WINDOW_TITLE
-
-fun FlickerTestParameter.appWindowReplacesLauncherAsTopWindow(testApp: IAppHelper) {
-    assertWm {
-        this.showsAppWindowOnTop(*HOME_WINDOW_TITLE)
-            .then()
-            .showsAppWindowOnTop("Snapshot", testApp.getPackage())
-    }
-}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
index 9ff0bdf..be919cd 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.server.wm.flicker.launch
 
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
@@ -33,8 +35,21 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test cold launch app from launcher.
+ * Test cold launching an app from launcher
+ *
  * To run this test: `atest FlickerTests:OpenAppColdTest`
+ *
+ * Actions:
+ *     Make sure no apps are running on the device
+ *     Launch an app [testApp] and wait animation to complete
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
@@ -42,6 +57,9 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group1
 class OpenAppColdTest(testSpec: FlickerTestParameter) : OpenAppTransition(testSpec) {
+    /**
+     * Defines the transition used to run the test
+     */
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = {
             super.transition(this, it)
@@ -62,43 +80,46 @@
             }
         }
 
-    @FlakyTest
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-    }
-
-    @FlakyTest
-    @Test
-    override fun navBarLayerIsAlwaysVisible() {
-        super.navBarLayerIsAlwaysVisible()
-    }
-
+    /** {@inheritDoc} */
     @FlakyTest
     @Test
     override fun navBarLayerRotatesAndScales() {
         super.navBarLayerRotatesAndScales()
     }
 
-    @FlakyTest
+    /** {@inheritDoc} */
+    @Postsubmit
     @Test
-    override fun statusBarLayerIsAlwaysVisible() {
-        super.statusBarLayerIsAlwaysVisible()
-    }
+    override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
 
-    @FlakyTest
+    /** {@inheritDoc} */
+    @Postsubmit
     @Test
-    override fun appLayerReplacesLauncher() {
-        super.appLayerReplacesLauncher()
-    }
-
-    @FlakyTest
-    @Test
-    override fun appWindowReplacesLauncherAsTopWindow() {
+    override fun appWindowReplacesLauncherAsTopWindow() =
         super.appWindowReplacesLauncherAsTopWindow()
-    }
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun launcherWindowBecomesInvisible() = super.launcherWindowBecomesInvisible()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
 
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
index b073a7c..663af70 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.wm.flicker.launch
 
+import android.platform.test.annotations.Presubmit
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
@@ -26,6 +27,7 @@
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.startRotation
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -33,8 +35,23 @@
 import org.junit.runners.Parameterized
 
 /**
- * Launch an app from the recents app view (the overview)
+ * Test launching an app from the recents app view (the overview)
+ *
  * To run this test: `atest FlickerTests:OpenAppFromOverviewTest`
+ *
+ * Actions:
+ *     Launch [testApp]
+ *     Press recents
+ *     Relaunch an app [testApp] by selecting it in the overview screen, and wait animation to
+ *     complete (only this action is traced)
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
@@ -42,6 +59,9 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group1
 class OpenAppFromOverviewTest(testSpec: FlickerTestParameter) : OpenAppTransition(testSpec) {
+    /**
+     * Defines the transition used to run the test
+     */
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = {
             super.transition(this, it)
@@ -59,41 +79,47 @@
             }
             transitions {
                 device.reopenAppFromOverview(wmHelper)
+                wmHelper.waitFor(
+                        WindowManagerConditionsFactory.hasLayersAnimating().negate(),
+                        WindowManagerConditionsFactory.isWMStateComplete(),
+                        WindowManagerConditionsFactory.isHomeActivityVisible().negate()
+                )
                 wmHelper.waitForFullScreenApp(testApp.component)
             }
         }
 
+    /** {@inheritDoc} */
     @FlakyTest
     @Test
-    override fun navBarLayerIsAlwaysVisible() {
-        super.navBarLayerIsAlwaysVisible()
-    }
+    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
 
-    @FlakyTest
+    /** {@inheritDoc} */
+    @Presubmit
     @Test
-    override fun statusBarLayerIsAlwaysVisible() {
-        super.statusBarLayerIsAlwaysVisible()
-    }
+    override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
 
-    @FlakyTest
+    /** {@inheritDoc} */
+    @Presubmit
     @Test
-    override fun navBarLayerRotatesAndScales() {
-        super.navBarLayerRotatesAndScales()
-    }
+    override fun launcherWindowBecomesInvisible() = super.launcherWindowBecomesInvisible()
 
-    @FlakyTest
+    /** {@inheritDoc} */
+    @Presubmit
     @Test
-    override fun statusBarLayerRotatesScales() {
-        super.statusBarLayerRotatesScales()
-    }
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
 
-    @FlakyTest
+    /** {@inheritDoc} */
+    @Presubmit
     @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-    }
+    override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
 
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
new file mode 100644
index 0000000..cf10c53
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.launch
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching an app while the device is locked
+ *
+ * To run this test: `atest FlickerTests:OpenAppNonResizeableTest`
+ *
+ * Actions:
+ *     Lock the device.
+ *     Launch an app on top of the lock screen [testApp] and wait animation to complete
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class OpenAppNonResizeableTest(testSpec: FlickerTestParameter) : OpenAppTransition(testSpec) {
+    override val testApp = NonResizeableAppHelper(instrumentation)
+    private val colorFadComponent = FlickerComponentName("", "ColorFade BLAST#")
+
+    /**
+     * Defines the transition used to run the test
+     */
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = { args ->
+            super.transition(this, args)
+            setup {
+                eachRun {
+                    device.sleep()
+                    wmHelper.waitFor("noAppWindowsOnTop") {
+                        it.wmState.topVisibleAppWindow.isEmpty()
+                    }
+                }
+            }
+            teardown {
+                eachRun {
+                    testApp.exit(wmHelper)
+                }
+            }
+            transitions {
+                testApp.launchViaIntent(wmHelper)
+                wmHelper.waitForFullScreenApp(testApp.component)
+            }
+        }
+
+    /**
+     * Checks that the nav bar layer starts visible, becomes invisible during unlocking animation
+     * and becomes visible at the end
+     */
+    @Postsubmit
+    @Test
+    fun navBarLayerVisibilityChanges() {
+        testSpec.assertLayers {
+            this.isVisible(FlickerComponentName.NAV_BAR)
+                .then()
+                .isInvisible(FlickerComponentName.NAV_BAR)
+                .then()
+                .isVisible(FlickerComponentName.NAV_BAR)
+        }
+    }
+
+    /**
+     * Checks that the app layer doesn't exist at the start of the transition, that it is
+     * created (invisible) and becomes visible during the transition
+     */
+    @FlakyTest
+    @Test
+    fun appLayerBecomesVisible() {
+        testSpec.assertLayers {
+            this.notContains(testApp.component)
+                    .then()
+                    .isInvisible(testApp.component)
+                    .then()
+                    .isVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that the app window doesn't exist at the start of the transition, that it is
+     * created (invisible - optional) and becomes visible during the transition
+     *
+     * The `isAppWindowInvisible` step is optional because we log once per frame, upon logging,
+     * the window may be visible or not depending on what was processed until that moment.
+     */
+    @Presubmit
+    @Test
+    fun appWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.notContains(testApp.component)
+                    .then()
+                    .isAppWindowInvisible(testApp.component, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks if [testApp] is visible at the end of the transition
+     */
+    @Presubmit
+    @Test
+    fun appWindowBecomesVisibleAtEnd() {
+        testSpec.assertWmEnd {
+            this.isAppWindowVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that the nav bar starts the transition visible, then becomes invisible during
+     * then unlocking animation and becomes visible at the end of the transition
+     */
+    @Postsubmit
+    @Test
+    fun navBarWindowsVisibilityChanges() {
+        testSpec.assertWm {
+            this.isAboveAppWindowVisible(FlickerComponentName.NAV_BAR)
+                .then()
+                .isNonAppWindowInvisible(FlickerComponentName.NAV_BAR)
+                .then()
+                .isAboveAppWindowVisible(FlickerComponentName.NAV_BAR)
+        }
+    }
+
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
+
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+            super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+    @FlakyTest
+    @Test
+    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
+
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+            super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun entireScreenCovered() = super.entireScreenCovered()
+
+    /**
+     * Checks that the focus changes from the launcher to [testApp]
+     */
+    @FlakyTest
+    @Test
+    override fun focusChanges() = super.focusChanges()
+
+    /**
+     * Checks that the screen is locked at the start of the transition ([colorFadComponent])
+     * layer is visible
+     */
+    @Postsubmit
+    @Test
+    fun screenLockedStart() {
+        testSpec.assertLayersStart {
+            isVisible(colorFadComponent)
+        }
+    }
+
+    /**
+     * This test checks if the launcher is visible at the start and the app at the end,
+     * it cannot use the regular assertion (check over time), because on lock screen neither
+     * the app not the launcher are visible, and there is no top visible window.
+     */
+    @Postsubmit
+    @Test
+    override fun appWindowReplacesLauncherAsTopWindow() {
+        testSpec.assertWm {
+            this.invoke("noAppWindowsOnTop") {
+                    Truth.assertWithMessage("Should not have any app window on top " +
+                        "when the screen is locked")
+                        .that(it.wmState.topVisibleAppWindow)
+                        .isEmpty()
+                }.then()
+                .isAppWindowOnTop(testApp.component)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(
+                            repetitions = 5,
+                            supportedNavigationModes =
+                            listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
+                            supportedRotations = listOf(Surface.ROTATION_0)
+                    )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
index b304d5f..7af7b3a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
@@ -18,34 +18,38 @@
 
 import android.app.Instrumentation
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
 import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.appLayerReplacesLauncher
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.endRotation
-import com.android.server.wm.flicker.focusChanges
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.StandardAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.repetitions
+import com.android.server.wm.flicker.replacesLayer
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.launcherWindowBecomesInvisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.Test
 
+/**
+ * Base class for app launch tests
+ */
 abstract class OpenAppTransition(protected val testSpec: FlickerTestParameter) {
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
-    protected val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
+    protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
 
+    /**
+     * Defines the transition used to run the test
+     */
     protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit = {
         withTestName { testSpec.name }
         repeat { testSpec.config.repetitions }
@@ -62,6 +66,10 @@
         }
     }
 
+    /**
+     * Entry point for the test runner. It will use this method to initialize and cache
+     * flicker executions
+     */
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
@@ -69,42 +77,56 @@
         }
     }
 
-    @Presubmit
-    @Test
-    open fun navBarWindowIsAlwaysVisible() {
-        testSpec.navBarWindowIsAlwaysVisible()
+    /**
+     * Checks that the navigation bar window is visible during the whole transition
+     */
+    open fun navBarWindowIsVisible() {
+        testSpec.navBarWindowIsVisible()
     }
 
-    @Presubmit
-    @Test
-    open fun navBarLayerIsAlwaysVisible() {
-        testSpec.navBarLayerIsAlwaysVisible(rotatesScreen = testSpec.isRotated)
+    /**
+     * Checks that the navigation bar layer is visible at the start and end of the trace
+     */
+    open fun navBarLayerIsVisible() {
+        testSpec.navBarLayerIsVisible()
     }
 
+    /**
+     * Checks the position of the navigation bar at the start and end of the transition
+     */
     @Presubmit
     @Test
-    open fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(Surface.ROTATION_0, testSpec.config.endRotation)
-    }
+    open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
+    /**
+     * Checks that the status bar window is visible during the whole transition
+     */
     @Presubmit
     @Test
-    open fun statusBarWindowIsAlwaysVisible() {
-        testSpec.statusBarWindowIsAlwaysVisible()
+    open fun statusBarWindowIsVisible() {
+        testSpec.statusBarWindowIsVisible()
     }
 
+    /**
+     * Checks that the status bar layer is visible at the start and end of the trace
+     */
     @Presubmit
     @Test
-    open fun statusBarLayerIsAlwaysVisible() {
-        testSpec.statusBarLayerIsAlwaysVisible(rotatesScreen = testSpec.isRotated)
+    open fun statusBarLayerIsVisible() {
+        testSpec.statusBarLayerIsVisible()
     }
 
+    /**
+     * Checks the position of the status bar at the start and end of the transition
+     */
     @Presubmit
     @Test
-    open fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(Surface.ROTATION_0, testSpec.config.endRotation)
-    }
+    open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
+    /**
+     * Checks that all windows that are visible on the trace, are visible for at least 2
+     * consecutive entries.
+     */
     @Presubmit
     @Test
     open fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
@@ -113,6 +135,10 @@
         }
     }
 
+    /**
+     * Checks that all layers that are visible on the trace, are visible for at least 2
+     * consecutive entries.
+     */
     @Presubmit
     @Test
     open fun visibleLayersShownMoreThanOneConsecutiveEntry() {
@@ -121,34 +147,60 @@
         }
     }
 
+    /**
+     * Checks that all parts of the screen are covered during the transition
+     */
     @Presubmit
     @Test
-    // During testing the launcher is always in portrait mode
-    open fun noUncoveredRegions() {
-        testSpec.noUncoveredRegions(Surface.ROTATION_0, testSpec.config.endRotation)
-    }
+    open fun entireScreenCovered() = testSpec.entireScreenCovered()
 
+    /**
+     * Checks that the focus changes from the launcher to [testApp]
+     */
     @Presubmit
     @Test
     open fun focusChanges() {
-        testSpec.focusChanges("NexusLauncherActivity", testApp.`package`)
+        testSpec.assertEventLog {
+            this.focusChanges("NexusLauncherActivity", testApp.`package`)
+        }
     }
 
-    @Presubmit
-    @Test
+    /**
+     * Checks that [LAUNCHER_COMPONENT] layer is visible at the start of the transition, and
+     * is replaced by [testApp], which remains visible until the end
+     */
     open fun appLayerReplacesLauncher() {
-        testSpec.appLayerReplacesLauncher(testApp.`package`)
+        testSpec.replacesLayer(LAUNCHER_COMPONENT, testApp.component)
     }
 
+    /**
+     * Checks that [LAUNCHER_COMPONENT] window is visible at the start of the transition, and
+     * is replaced by a snapshot or splash screen (optional), and finally, is replaced by
+     * [testApp], which remains visible until the end
+     */
     @Presubmit
     @Test
     open fun appWindowReplacesLauncherAsTopWindow() {
-        testSpec.appWindowReplacesLauncherAsTopWindow(testApp)
+        testSpec.assertWm {
+            this.isAppWindowOnTop(LAUNCHER_COMPONENT)
+                    .then()
+                    .isAppWindowOnTop(FlickerComponentName.SNAPSHOT, isOptional = true)
+                    .then()
+                    .isAppWindowOnTop(FlickerComponentName.SPLASH_SCREEN, isOptional = true)
+                    .then()
+                    .isAppWindowOnTop(testApp.component)
+        }
     }
 
-    @Presubmit
-    @Test
+    /**
+     * Checks that [LAUNCHER_COMPONENT] window is visible at the start, and
+     * becomes invisible during the transition
+     */
     open fun launcherWindowBecomesInvisible() {
-        testSpec.launcherWindowBecomesInvisible()
+        testSpec.assertWm {
+            this.isAppWindowOnTop(LAUNCHER_COMPONENT)
+                    .then()
+                    .isAppWindowNotOnTop(LAUNCHER_COMPONENT)
+        }
     }
 }
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
index e2705c7..5edee0c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.wm.flicker.launch
 
+import android.platform.test.annotations.Presubmit
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
@@ -32,8 +33,22 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test warm launch app.
+ * Test warm launching an app from launcher
+ *
  * To run this test: `atest FlickerTests:OpenAppWarmTest`
+ *
+ * Actions:
+ *     Launch [testApp]
+ *     Press home
+ *     Relaunch an app [testApp] and wait animation to complete (only this action is traced)
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
@@ -41,6 +56,9 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group1
 class OpenAppWarmTest(testSpec: FlickerTestParameter) : OpenAppTransition(testSpec) {
+    /**
+     * Defines the transition used to run the test
+     */
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = {
             super.transition(this, it)
@@ -65,19 +83,38 @@
             }
         }
 
+    /** {@inheritDoc} */
     @FlakyTest
     @Test
-    override fun navBarLayerIsAlwaysVisible() {
-        super.navBarLayerIsAlwaysVisible()
-    }
+    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
 
-    @FlakyTest
+    /** {@inheritDoc} */
+    @Presubmit
     @Test
-    override fun navBarLayerRotatesAndScales() {
-        super.navBarLayerRotatesAndScales()
-    }
+    override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun launcherWindowBecomesInvisible() = super.launcherWindowBecomesInvisible()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun navBarWindowIsVisible() = super.navBarWindowIsVisible()
 
     companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
new file mode 100644
index 0000000..495e2d6
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.launch
+
+import android.app.Instrumentation
+import android.app.WallpaperManager
+import android.platform.test.annotations.Postsubmit
+import androidx.test.filters.RequiresDevice
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.entireScreenCovered
+import com.android.server.wm.flicker.helpers.NewTasksAppHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.navBarLayerIsVisible
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.repetitions
+import com.android.server.wm.flicker.startRotation
+import com.android.server.wm.flicker.statusBarLayerIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.flicker.testapp.ActivityOptions.LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME
+import com.android.server.wm.flicker.testapp.ActivityOptions.SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.FlickerComponentName.Companion.SPLASH_SCREEN
+import com.android.server.wm.traces.common.FlickerComponentName.Companion.WALLPAPER_BBQ_WRAPPER
+import com.android.server.wm.traces.parser.toFlickerComponent
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test the back and forward transition between 2 activities.
+ *
+ * To run this test: `atest FlickerTests:ActivitiesTransitionTest`
+ *
+ * Actions:
+ *     Launch the NewTaskLauncherApp [mTestApp]
+ *     Open a new task (SimpleActivity) from the NewTaskLauncherApp [mTestApp]
+ *     Go back to the NewTaskLauncherApp [mTestApp]
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group4
+class TaskTransitionTest(val testSpec: FlickerTestParameter) {
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val mTestApp: NewTasksAppHelper = NewTasksAppHelper(instrumentation)
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            withTestName { testSpec.name }
+            repeat { testSpec.config.repetitions }
+            setup {
+                eachRun {
+                    mTestApp.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(mTestApp.component)
+                }
+            }
+            teardown {
+                test {
+                    mTestApp.exit()
+                }
+            }
+            transitions {
+                mTestApp.openNewTask(device, wmHelper)
+                device.pressBack()
+                wmHelper.waitForAppTransitionIdle()
+                wmHelper.waitForFullScreenApp(mTestApp.component)
+            }
+        }
+    }
+
+    /**
+     * Checks that the wallpaper window is never visible when performing task transitions.
+     * A solid color background should be shown instead.
+     */
+    @Postsubmit
+    @Test
+    fun wallpaperWindowIsNeverVisible() {
+        testSpec.assertWm {
+            this.isNonAppWindowInvisible(WALLPAPER)
+        }
+    }
+
+    /**
+     * Checks that the wallpaper layer is never visible when performing task transitions.
+     * A solid color background should be shown instead.
+     */
+    @Postsubmit
+    @Test
+    fun wallpaperLayerIsNeverVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(WALLPAPER)
+            this.isInvisible(WALLPAPER_BBQ_WRAPPER)
+        }
+    }
+
+    /**
+     * Check that the launcher window is never visible when performing task transitions.
+     * A solid color background should be shown above it.
+     */
+    @Postsubmit
+    @Test
+    fun launcherWindowIsNeverVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that the launcher layer is never visible when performing task transitions.
+     * A solid color background should be shown above it.
+     */
+    @Postsubmit
+    @Test
+    fun launcherLayerIsNeverVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that a color background is visible while the task transition is occurring.
+     */
+    @Postsubmit
+    @Test
+    fun colorLayerIsVisibleDuringTransition() {
+        val bgColorLayer = FlickerComponentName("", "colorBackgroundLayer")
+        val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
+
+        testSpec.assertLayers {
+            this.coversExactly(displayBounds, LAUNCH_NEW_TASK_ACTIVITY)
+                .isInvisible(bgColorLayer)
+                .then()
+                // Transitioning
+                .isVisible(bgColorLayer)
+                .then()
+                // Fully transitioned to simple SIMPLE_ACTIVITY
+                .coversExactly(displayBounds, SIMPLE_ACTIVITY)
+                .isInvisible(bgColorLayer)
+                .then()
+                // Transitioning back
+                .isVisible(bgColorLayer)
+                .then()
+                // Fully transitioned back to LAUNCH_NEW_TASK_ACTIVITY
+                .isInvisible(bgColorLayer)
+                .coversExactly(displayBounds, LAUNCH_NEW_TASK_ACTIVITY)
+        }
+    }
+
+    /**
+     * Checks that we start with the LaunchNewTask activity on top and then open up
+     * the SimpleActivity and then go back to the LaunchNewTask activity.
+     */
+    @Postsubmit
+    @Test
+    fun newTaskOpensOnTopAndThenCloses() {
+        testSpec.assertWm {
+            this.isAppWindowOnTop(LAUNCH_NEW_TASK_ACTIVITY)
+                    .then()
+                    .isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
+                    .then()
+                    .isAppWindowOnTop(SIMPLE_ACTIVITY)
+                    .then()
+                    .isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
+                    .then()
+                    .isAppWindowOnTop(LAUNCH_NEW_TASK_ACTIVITY)
+        }
+    }
+
+    /**
+     * Checks that all parts of the screen are covered at the start and end of the transition
+     */
+    @Postsubmit
+    @Test
+    fun entireScreenCovered() = testSpec.entireScreenCovered()
+
+    /**
+     * Checks that the navbar window is visible throughout the transition
+     */
+    @Postsubmit
+    @Test
+    fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
+
+    /**
+     * Checks that the navbar layer is visible throughout the transition
+     */
+    @Postsubmit
+    @Test
+    fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
+
+    /**
+     * Checks that the status bar window is visible throughout the transition
+     */
+    @Postsubmit
+    @Test
+    fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
+
+    /**
+     * Checks that the status bar layer is visible throughout the transition
+     */
+    @Postsubmit
+    @Test
+    fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
+
+    companion object {
+        private val WALLPAPER = getWallpaperPackage(InstrumentationRegistry.getInstrumentation())
+        private val LAUNCH_NEW_TASK_ACTIVITY =
+                LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME.toFlickerComponent()
+        private val SIMPLE_ACTIVITY = SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME.toFlickerComponent()
+
+        private fun getWallpaperPackage(instrumentation: Instrumentation): FlickerComponentName {
+            val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext)
+
+            return wallpaperManager.wallpaperInfo.component.toFlickerComponent()
+        }
+
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(repetitions = 5)
+        }
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
new file mode 100644
index 0000000..52904cc
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.quickswitch
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.RequiresDevice
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.helpers.isRotated
+import com.android.server.wm.flicker.navBarLayerIsVisible
+import com.android.server.wm.flicker.navBarLayerRotatesAndScales
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.startRotation
+import com.android.server.wm.flicker.statusBarLayerIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switching back to previous app from last opened app
+ *
+ * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsBackTest`
+ *
+ * Actions:
+ *     Launch an app [testApp1]
+ *     Launch another app [testApp2]
+ *     Swipe right from the bottom of the screen to quick switch back to the first app [testApp1]
+ *
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestParameter) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    private val testApp1 = SimpleAppHelper(instrumentation)
+    private val testApp2 = NonResizeableAppHelper(instrumentation)
+
+    private val startDisplayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            setup {
+                eachRun {
+                    testApp1.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(testApp1.component)
+
+                    testApp2.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(testApp2.component)
+                }
+            }
+            transitions {
+                // Swipe right from bottom to quick switch back
+                // NOTE: We don't perform an edge-to-edge swipe but instead only swipe in the middle
+                // as to not accidentally trigger a swipe back or forward action which would result
+                // in the same behavior but not testing quick swap.
+                device.swipe(
+                        startDisplayBounds.bounds.right / 3,
+                        startDisplayBounds.bounds.bottom,
+                        2 * startDisplayBounds.bounds.right / 3,
+                        startDisplayBounds.bounds.bottom,
+                        if (testSpec.config.startRotation.isRotated()) 75 else 30
+                )
+
+                wmHelper.waitForFullScreenApp(testApp1.component)
+                wmHelper.waitForAppTransitionIdle()
+            }
+
+            teardown {
+                test {
+                    testApp1.exit()
+                    testApp2.exit()
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks that the transition starts with [testApp2]'s windows filling/covering exactly the
+     * entirety of the display.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithApp2WindowsCoverFullScreen() {
+        testSpec.assertWmStart {
+            this.frameRegion(testApp2.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with [testApp2]'s layers filling/covering exactly the
+     * entirety of the display.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithApp2LayersCoverFullScreen() {
+        testSpec.assertLayersStart {
+            this.visibleRegion(testApp2.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with [testApp2] being the top window.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithApp2WindowBeingOnTop() {
+        testSpec.assertWmStart {
+            this.isAppWindowOnTop(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp1] windows fill the entire screen (i.e. is "fullscreen") at the end of the
+     * transition once we have fully quick switched from [testApp2] back to the [testApp1].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithApp1WindowsCoveringFullScreen() {
+        testSpec.assertWmEnd {
+            this.frameRegion(testApp1.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that [testApp1] layers fill the entire screen (i.e. is "fullscreen") at the end of the
+     * transition once we have fully quick switched from [testApp2] back to the [testApp1].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithApp1LayersCoveringFullScreen() {
+        testSpec.assertLayersEnd {
+            this.visibleRegion(testApp1.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that [testApp1] is the top window at the end of the transition once we have fully quick
+     * switched from [testApp2] back to the [testApp1].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithApp1BeingOnTop() {
+        testSpec.assertWmEnd {
+            this.isAppWindowOnTop(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp1]'s window starts off invisible and becomes visible at some point before
+     * the end of the transition and then stays visible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app1WindowBecomesAndStaysVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(testApp1.component)
+                    .then()
+                    .isAppWindowVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp1]'s layer starts off invisible and becomes visible at some point before
+     * the end of the transition and then stays visible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app1LayerBecomesAndStaysVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(testApp1.component)
+                    .then()
+                    .isVisible(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp2]'s window starts off visible and becomes invisible at some point before
+     * the end of the transition and then stays invisible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app2WindowBecomesAndStaysInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(testApp2.component)
+                    .then()
+                    .isAppWindowInvisible(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp2]'s layer starts off visible and becomes invisible at some point before
+     * the end of the transition and then stays invisible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app2LayerBecomesAndStaysInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp2.component)
+                    .then()
+                    .isInvisible(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp2]'s window is visible at least until [testApp1]'s window is visible.
+     * Ensures that at any point, either [testApp1] or [testApp2]'s windows are at least partially
+     * visible.
+     */
+    @Postsubmit
+    @Test
+    fun app1WindowIsVisibleOnceApp2WindowIsInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(testApp2.component)
+                    .then()
+                    // TODO: Do we actually want to test this? Seems too implementation specific...
+                    .isAppWindowVisible(LAUNCHER_COMPONENT, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp2]'s layer is visible at least until [testApp1]'s window is visible.
+     * Ensures that at any point, either [testApp1] or [testApp2]'s windows are at least partially
+     * visible.
+     */
+    @Postsubmit
+    @Test
+    fun app1LayerIsVisibleOnceApp2LayerIsInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp2.component)
+                    .then()
+                    .isVisible(LAUNCHER_COMPONENT, isOptional = true)
+                    .then()
+                    .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+                    .then()
+                    .isVisible(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that the navbar window is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsVisible()
+
+    /**
+     * Checks that the navbar layer is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun navBarLayerAlwaysIsVisible() = testSpec.navBarLayerIsVisible()
+
+    /**
+     * Checks that the navbar is always in the right position and covers the expected region.
+     *
+     * NOTE: This doesn't check that the navbar is visible or not.
+     */
+    @Postsubmit
+    @Test
+    fun navbarIsAlwaysInRightPosition() = testSpec.navBarLayerRotatesAndScales()
+
+    /**
+     * Checks that the status bar window is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsVisible()
+
+    /**
+     * Checks that the status bar layer is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsVisible()
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(
+                            repetitions = 5,
+                            supportedNavigationModes = listOf(
+                                    WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
+                            ),
+                            supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)
+                    )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
new file mode 100644
index 0000000..842aa2b
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.quickswitch
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.RequiresDevice
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.helpers.isRotated
+import com.android.server.wm.flicker.navBarLayerIsVisible
+import com.android.server.wm.flicker.navBarLayerRotatesAndScales
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.repetitions
+import com.android.server.wm.flicker.startRotation
+import com.android.server.wm.flicker.statusBarLayerIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switching back to previous app from last opened app
+ *
+ * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsBackTest`
+ *
+ * Actions:
+ *     Launch an app [testApp1]
+ *     Launch another app [testApp2]
+ *     Swipe right from the bottom of the screen to quick switch back to the first app [testApp1]
+ *     Swipe left from the bottom of the screen to quick switch forward to the second app [testApp2]
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class QuickSwitchBetweenTwoAppsForwardTest(private val testSpec: FlickerTestParameter) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    private val testApp1 = SimpleAppHelper(instrumentation)
+    private val testApp2 = NonResizeableAppHelper(instrumentation)
+
+    private val startDisplayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            withTestName { testSpec.name }
+            repeat { testSpec.config.repetitions }
+            setup {
+                eachRun {
+                    testApp1.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(testApp1.component)
+
+                    testApp2.launchViaIntent(wmHelper)
+                    wmHelper.waitForFullScreenApp(testApp2.component)
+
+                    // Swipe right from bottom to quick switch back
+                    // NOTE: We don't perform an edge-to-edge swipe but instead only swipe in the middle
+                    // as to not accidentally trigger a swipe back or forward action which would result
+                    // in the same behavior but not testing quick swap.
+                    device.swipe(
+                            startDisplayBounds.bounds.right / 3,
+                            startDisplayBounds.bounds.bottom,
+                            2 * startDisplayBounds.bounds.right / 3,
+                            startDisplayBounds.bounds.bottom,
+                            if (testSpec.config.startRotation.isRotated()) 75 else 30
+                    )
+
+                    wmHelper.waitForFullScreenApp(testApp1.component)
+                    wmHelper.waitForAppTransitionIdle()
+                }
+            }
+            transitions {
+                // Swipe left from bottom to quick switch forward
+                // NOTE: We don't perform an edge-to-edge swipe but instead only swipe in the middle
+                // as to not accidentally trigger a swipe back or forward action which would result
+                // in the same behavior but not testing quick swap.
+                device.swipe(
+                        2 * startDisplayBounds.bounds.right / 3,
+                        startDisplayBounds.bounds.bottom,
+                        startDisplayBounds.bounds.right / 3,
+                        startDisplayBounds.bounds.bottom,
+                        if (testSpec.config.startRotation.isRotated()) 75 else 30
+                )
+
+                wmHelper.waitForFullScreenApp(testApp2.component)
+                wmHelper.waitForAppTransitionIdle()
+            }
+
+            teardown {
+                test {
+                    testApp1.exit()
+                    testApp2.exit()
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks that the transition starts with [testApp1]'s windows filling/covering exactly the
+     * entirety of the display.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithApp1WindowsCoverFullScreen() {
+        testSpec.assertWmStart {
+            this.frameRegion(testApp1.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with [testApp1]'s layers filling/covering exactly the
+     * entirety of the display.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithApp1LayersCoverFullScreen() {
+        testSpec.assertLayersStart {
+            this.visibleRegion(testApp1.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with [testApp1] being the top window.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithApp1WindowBeingOnTop() {
+        testSpec.assertWmStart {
+            this.isAppWindowOnTop(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp2] windows fill the entire screen (i.e. is "fullscreen") at the end of the
+     * transition once we have fully quick switched from [testApp1] back to the [testApp2].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithApp2WindowsCoveringFullScreen() {
+        testSpec.assertWmEnd {
+            this.frameRegion(testApp2.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that [testApp2] layers fill the entire screen (i.e. is "fullscreen") at the end of the
+     * transition once we have fully quick switched from [testApp1] back to the [testApp2].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithApp2LayersCoveringFullScreen() {
+        testSpec.assertLayersEnd {
+            this.visibleRegion(testApp2.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that [testApp2] is the top window at the end of the transition once we have fully quick
+     * switched from [testApp1] back to the [testApp2].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithApp2BeingOnTop() {
+        testSpec.assertWmEnd {
+            this.isAppWindowOnTop(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp2]'s window starts off invisible and becomes visible at some point before
+     * the end of the transition and then stays visible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app2WindowBecomesAndStaysVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(testApp2.component)
+                    .then()
+                    .isAppWindowVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp2]'s layer starts off invisible and becomes visible at some point before
+     * the end of the transition and then stays visible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app2LayerBecomesAndStaysVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(testApp2.component)
+                    .then()
+                    .isVisible(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp1]'s window starts off visible and becomes invisible at some point before
+     * the end of the transition and then stays invisible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app1WindowBecomesAndStaysInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(testApp1.component)
+                    .then()
+                    .isAppWindowInvisible(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp1]'s layer starts off visible and becomes invisible at some point before
+     * the end of the transition and then stays invisible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun app1LayerBecomesAndStaysInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp1.component)
+                    .then()
+                    .isInvisible(testApp1.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp1]'s window is visible at least until [testApp2]'s window is visible.
+     * Ensures that at any point, either [testApp2] or [testApp1]'s windows are at least partially
+     * visible.
+     */
+    @Postsubmit
+    @Test
+    fun app2WindowIsVisibleOnceApp1WindowIsInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowVisible(testApp1.component)
+                    .then()
+                    .isAppWindowVisible(LAUNCHER_COMPONENT, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+                    .then()
+                    .isAppWindowVisible(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp1]'s layer is visible at least until [testApp2]'s window is visible.
+     * Ensures that at any point, either [testApp2] or [testApp1]'s windows are at least partially
+     * visible.
+     */
+    @Postsubmit
+    @Test
+    fun app2LayerIsVisibleOnceApp1LayerIsInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp1.component)
+                    .then()
+                    .isVisible(LAUNCHER_COMPONENT, isOptional = true)
+                    .then()
+                    .isVisible(FlickerComponentName.SNAPSHOT, isOptional = true)
+                    .then()
+                    .isVisible(testApp2.component)
+        }
+    }
+
+    /**
+     * Checks that the navbar window is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsVisible()
+
+    /**
+     * Checks that the navbar layer is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun navBarLayerAlwaysIsVisible() = testSpec.navBarLayerIsVisible()
+
+    /**
+     * Checks that the navbar is always in the right position and covers the expected region.
+     *
+     * NOTE: This doesn't check that the navbar is visible or not.
+     */
+    @Postsubmit
+    @Test
+    fun navbarIsAlwaysInRightPosition() = testSpec.navBarLayerRotatesAndScales()
+
+    /**
+     * Checks that the status bar window is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsVisible()
+
+    /**
+     * Checks that the status bar layer is visible throughout the entire transition.
+     */
+    @Postsubmit
+    @Test
+    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsVisible()
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(
+                            repetitions = 5,
+                            supportedNavigationModes = listOf(
+                                    WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
+                            ),
+                            supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)
+                    )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
new file mode 100644
index 0000000..10ca0d9
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.quickswitch
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.RequiresDevice
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.entireScreenCovered
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.navBarLayerIsVisible
+import com.android.server.wm.flicker.navBarLayerRotatesAndScales
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.startRotation
+import com.android.server.wm.flicker.statusBarLayerIsVisible
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switching to last opened app from launcher
+ *
+ * To run this test: `atest FlickerTests:QuickSwitchFromLauncherTest`
+ *
+ * Actions:
+ *     Launch an app
+ *     Navigate home to show launcher
+ *     Swipe right from the bottom of the screen to quick switch back to the app
+ *
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group4
+class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val testApp = SimpleAppHelper(instrumentation)
+    private val startDisplayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            setup {
+                eachRun {
+                    testApp.launchViaIntent(wmHelper)
+                    device.pressHome()
+                    wmHelper.waitForHomeActivityVisible()
+                    wmHelper.waitForWindowSurfaceDisappeared(testApp.component)
+                }
+            }
+            transitions {
+                // Swipe right from bottom to quick switch back
+                // NOTE: We don't perform an edge-to-edge swipe but instead only swipe in the middle
+                // as to not accidentally trigger a swipe back or forward action which would result
+                // in the same behavior but not testing quick swap.
+                device.swipe(
+                        startDisplayBounds.bounds.right / 3,
+                        startDisplayBounds.bounds.bottom,
+                        2 * startDisplayBounds.bounds.right / 3,
+                        startDisplayBounds.bounds.bottom,
+                        50
+                )
+
+                wmHelper.waitForFullScreenApp(testApp.component)
+                wmHelper.waitForAppTransitionIdle()
+            }
+
+            teardown {
+                eachRun {
+                    testApp.exit()
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks that [testApp] windows fill the entire screen (i.e. is "fullscreen") at the end of the
+     * transition once we have fully quick switched from the launcher back to the [testApp].
+     */
+    @Presubmit
+    @Test
+    fun endsWithAppWindowsCoveringFullScreen() {
+        testSpec.assertWmEnd {
+            this.frameRegion(testApp.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that [testApp] layers fill the entire screen (i.e. is "fullscreen") at the end of the
+     * transition once we have fully quick switched from the launcher back to the [testApp].
+     */
+    @Presubmit
+    @Test
+    fun endsWithAppLayersCoveringFullScreen() {
+        testSpec.assertLayersEnd {
+            this.visibleRegion(testApp.component).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that [testApp] is the top window at the end of the transition once we have fully quick
+     * switched from the launcher back to the [testApp].
+     */
+    @Presubmit
+    @Test
+    fun endsWithAppBeingOnTop() {
+        testSpec.assertWmEnd {
+            this.isAppWindowOnTop(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with the home activity being tagged as visible.
+     */
+    @Presubmit
+    @Test
+    fun startsWithHomeActivityFlaggedVisible() {
+        testSpec.assertWmStart {
+            this.isHomeActivityVisible()
+        }
+    }
+
+    /**
+     * Checks that the transition starts with the launcher windows filling/covering exactly the
+     * entirety of the display.
+     */
+    @Presubmit
+    @Test
+    fun startsWithLauncherWindowsCoverFullScreen() {
+        testSpec.assertWmStart {
+            this.frameRegion(LAUNCHER_COMPONENT).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with the launcher layers filling/covering exactly the
+     * entirety of the display.
+     */
+    @Presubmit
+    @Test
+    fun startsWithLauncherLayersCoverFullScreen() {
+        testSpec.assertLayersStart {
+            this.visibleRegion(LAUNCHER_COMPONENT).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with the launcher being the top window.
+     */
+    @Presubmit
+    @Test
+    fun startsWithLauncherBeingOnTop() {
+        testSpec.assertWmStart {
+            this.isAppWindowOnTop(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that the transition ends with the home activity being flagged as not visible. By this
+     * point we should have quick switched away from the launcher back to the [testApp].
+     */
+    @Presubmit
+    @Test
+    fun endsWithHomeActivityFlaggedInvisible() {
+        testSpec.assertWmEnd {
+            this.isHomeActivityInvisible()
+        }
+    }
+
+    /**
+     * Checks that [testApp]'s window starts off invisible and becomes visible at some point before
+     * the end of the transition and then stays visible until the end of the transition.
+     */
+    @Presubmit
+    @Test
+    fun appWindowBecomesAndStaysVisible() {
+        testSpec.assertWm {
+            this.isAppWindowInvisible(testApp.component)
+                    .then()
+                    .isAppWindowVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that [testApp]'s layer starts off invisible and becomes visible at some point before
+     * the end of the transition and then stays visible until the end of the transition.
+     */
+    @Presubmit
+    @Test
+    fun appLayerBecomesAndStaysVisible() {
+        testSpec.assertLayers {
+            this.isInvisible(testApp.component)
+                    .then()
+                    .isVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that the launcher window starts off visible and becomes invisible at some point before
+     * the end of the transition and then stays invisible until the end of the transition.
+     */
+    @Presubmit
+    @Test
+    fun launcherWindowBecomesAndStaysInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowOnTop(LAUNCHER_COMPONENT)
+                    .then()
+                    .isAppWindowNotOnTop(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that the launcher layer starts off visible and becomes invisible at some point before
+     * the end of the transition and then stays invisible until the end of the transition.
+     */
+    @Presubmit
+    @Test
+    fun launcherLayerBecomesAndStaysInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(LAUNCHER_COMPONENT)
+                    .then()
+                    .isInvisible(LAUNCHER_COMPONENT)
+        }
+    }
+
+    /**
+     * Checks that the launcher window is visible at least until the app window is visible. Ensures
+     * that at any point, either the launcher or [testApp] windows are at least partially visible.
+     */
+    @Presubmit
+    @Test
+    fun appWindowIsVisibleOnceLauncherWindowIsInvisible() {
+        testSpec.assertWm {
+            this.isAppWindowOnTop(LAUNCHER_COMPONENT)
+                    .then()
+                    .isAppWindowVisible(FlickerComponentName.SNAPSHOT)
+                    .then()
+                    .isAppWindowVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that the launcher layer is visible at least until the app layer is visible. Ensures
+     * that at any point, either the launcher or [testApp] layers are at least partially visible.
+     */
+    @Presubmit
+    @Test
+    fun appLayerIsVisibleOnceLauncherLayerIsInvisible() {
+        testSpec.assertLayers {
+            this.isVisible(LAUNCHER_COMPONENT)
+                    .then()
+                    .isVisible(FlickerComponentName.SNAPSHOT)
+                    .then()
+                    .isVisible(testApp.component)
+        }
+    }
+
+    /**
+     * Checks that the navbar window is visible throughout the entire transition.
+     */
+    @Presubmit
+    @Test
+    fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsVisible()
+
+    /**
+     * Checks that the navbar layer is visible throughout the entire transition.
+     */
+    @Presubmit
+    @Test
+    fun navBarLayerAlwaysIsVisible() = testSpec.navBarLayerIsVisible()
+
+    /**
+     * Checks that the navbar is always in the right position and covers the expected region.
+     *
+     * NOTE: This doesn't check that the navbar is visible or not.
+     */
+    @Presubmit
+    @Test
+    fun navbarIsAlwaysInRightPosition() = testSpec.navBarLayerRotatesAndScales()
+
+    /**
+     * Checks that the status bar window is visible throughout the entire transition.
+     */
+    @Presubmit
+    @Test
+    fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsVisible()
+
+    /**
+     * Checks that the status bar layer is visible throughout the entire transition.
+     */
+    @Presubmit
+    @Test
+    fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsVisible()
+
+    /**
+     * Checks that the screen is always fully covered by visible layers throughout the transition.
+     */
+    @Presubmit
+    @Test
+    fun screenIsAlwaysFilled() = testSpec.entireScreenCovered()
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(
+                            repetitions = 5,
+                            supportedNavigationModes = listOf(
+                                    WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
+                            ),
+                            // TODO: Test with 90 rotation
+                            supportedRotations = listOf(Surface.ROTATION_0)
+                    )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
index 69e8a8d..fd8abc6 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
@@ -26,15 +26,52 @@
 import com.android.server.wm.flicker.annotation.Group3
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.rules.WMFlickerServiceRuleForTestSpec
+import com.android.server.wm.flicker.statusBarLayerIsVisible
+import com.android.server.wm.flicker.statusBarLayerRotatesScales
+import com.android.server.wm.flicker.statusBarWindowIsVisible
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.FixMethodOrder
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
 
 /**
- * Cycle through supported app rotations.
+ * Test opening an app and cycling through app rotations
+ *
+ * Currently runs:
+ *      0 -> 90 degrees
+ *      90 -> 0 degrees
+ *
+ * Actions:
+ *     Launch an app (via intent)
+ *     Set initial device orientation
+ *     Start tracing
+ *     Change device orientation
+ *     Stop tracing
+ *
  * To run this test: `atest FlickerTests:ChangeAppRotationTest`
+ *
+ * To run only the presubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Presubmit`
+ *
+ * To run only the postsubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Postsubmit`
+ *
+ * To run only the flaky assertions add: `--
+ *      --module-arg FlickerTests:include-annotation:androidx.test.filters.FlakyTest`
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [RotationTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
@@ -44,6 +81,9 @@
 class ChangeAppRotationTest(
     testSpec: FlickerTestParameter
 ) : RotationTransition(testSpec) {
+    @get:Rule
+    val flickerRule = WMFlickerServiceRuleForTestSpec(testSpec)
+
     override val testApp = SimpleAppHelper(instrumentation)
     override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
         get() = {
@@ -55,45 +95,94 @@
             }
         }
 
+    @Postsubmit
+    @Test
+    fun runPresubmitAssertion() {
+        flickerRule.checkPresubmitAssertions()
+    }
+
+    @Postsubmit
+    @Test
+    fun runPostsubmitAssertion() {
+        flickerRule.checkPostsubmitAssertions()
+    }
+
+    @FlakyTest
+    @Test
+    fun runFlakyAssertion() {
+        flickerRule.checkFlakyAssertions()
+    }
+
+    /** {@inheritDoc} */
     @FlakyTest(bugId = 190185577)
     @Test
     override fun focusDoesNotChange() {
         super.focusDoesNotChange()
     }
 
-    @Postsubmit
+    /**
+     * Checks that the [FlickerComponentName.ROTATION] layer appears during the transition,
+     * doesn't flicker, and disappears before the transition is complete
+     */
+    @Presubmit
     @Test
-    fun screenshotLayerBecomesInvisible() {
+    fun rotationLayerAppearsAndVanishes() {
         testSpec.assertLayers {
-            this.isVisible(testApp.getPackage())
+            this.isVisible(testApp.component)
                 .then()
-                .isVisible(SCREENSHOT_LAYER)
+                .isVisible(FlickerComponentName.ROTATION)
                 .then()
-                .isVisible(testApp.getPackage())
+                .isVisible(testApp.component)
+                .isInvisible(FlickerComponentName.ROTATION)
         }
     }
 
-    @Postsubmit
-    @Test
-    override fun statusBarLayerRotatesScales() {
-        super.statusBarLayerRotatesScales()
-    }
-
+    /**
+     * Checks that the status bar window is visible and above the app windows in all WM
+     * trace entries
+     */
     @Presubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() {
-        super.navBarWindowIsAlwaysVisible()
+    fun statusBarWindowIsVisible() {
+        testSpec.statusBarWindowIsVisible()
     }
 
+    /**
+     * Checks that the status bar layer is visible at the start and end of the transition
+     */
+    @Presubmit
+    @Test
+    fun statusBarLayerIsVisible() {
+        testSpec.statusBarLayerIsVisible()
+    }
+
+    /**
+     * Checks the position of the status bar at the start and end of the transition
+     */
+    @Presubmit
+    @Test
+    fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+
+    /** {@inheritDoc} */
     @FlakyTest
     @Test
-    override fun statusBarLayerIsAlwaysVisible() {
-        super.statusBarLayerIsAlwaysVisible()
+    override fun navBarLayerRotatesAndScales() {
+        super.navBarLayerRotatesAndScales()
     }
 
-    companion object {
-        private const val SCREENSHOT_LAYER = "RotationLayer"
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
index 4b888cd..e850632 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
@@ -18,33 +18,28 @@
 
 import android.app.Instrumentation
 import android.platform.test.annotations.Presubmit
-import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.endRotation
-import com.android.server.wm.flicker.focusDoesNotChange
 import com.android.server.wm.flicker.helpers.StandardAppHelper
-import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible
+import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
-import com.android.server.wm.flicker.noUncoveredRegions
+import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.startRotation
-import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.Test
 
+/**
+ * Base class for app rotation tests
+ */
 abstract class RotationTransition(protected val testSpec: FlickerTestParameter) {
     protected abstract val testApp: StandardAppHelper
 
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
-    protected val startingPos get() = WindowUtils.getDisplayBounds(testSpec.config.startRotation)
-    protected val endingPos get() = WindowUtils.getDisplayBounds(testSpec.config.endRotation)
 
     protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit = {
         setup {
@@ -62,6 +57,10 @@
         }
     }
 
+    /**
+     * Entry point for the test runner. It will use this method to initialize and cache
+     * flicker executions
+     */
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
@@ -69,57 +68,53 @@
         }
     }
 
-    @FlakyTest
-    @Test
-    open fun navBarWindowIsAlwaysVisible() {
-        testSpec.navBarWindowIsAlwaysVisible()
-    }
-
-    @FlakyTest
-    @Test
-    open fun navBarLayerIsAlwaysVisible() {
-        testSpec.navBarLayerIsAlwaysVisible(rotatesScreen = true)
-    }
-
-    @FlakyTest
-    @Test
-    open fun navBarLayerRotatesAndScales() {
-        testSpec.navBarLayerRotatesAndScales(
-            testSpec.config.startRotation, testSpec.config.endRotation)
-    }
-
+    /**
+     * Checks that the navigation bar window is visible and above the app windows in all WM
+     * trace entries
+     */
     @Presubmit
     @Test
-    open fun statusBarWindowIsAlwaysVisible() {
-        testSpec.statusBarWindowIsAlwaysVisible()
+    open fun navBarWindowIsVisible() {
+        testSpec.navBarWindowIsVisible()
     }
 
-    @FlakyTest
+    /**
+     * Checks that the navigation bar layer is visible at the start and end of the transition
+     */
+    @Presubmit
     @Test
-    open fun statusBarLayerIsAlwaysVisible() {
-        testSpec.statusBarLayerIsAlwaysVisible(rotatesScreen = true)
+    open fun navBarLayerIsVisible() {
+        testSpec.navBarLayerIsVisible()
     }
 
-    @FlakyTest
+    /**
+     * Checks the position of the navigation bar at the start and end of the transition
+     */
+    @Presubmit
     @Test
-    open fun statusBarLayerRotatesScales() {
-        testSpec.statusBarLayerRotatesScales(
-            testSpec.config.startRotation, testSpec.config.endRotation)
-    }
+    open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
 
-    @FlakyTest
+    /**
+     * Checks that all layers that are visible on the trace, are visible for at least 2
+     * consecutive entries.
+     */
+    @Presubmit
     @Test
     open fun visibleLayersShownMoreThanOneConsecutiveEntry() {
         testSpec.assertLayers {
             this.visibleLayersShownMoreThanOneConsecutiveEntry(
-                ignoreLayers = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-                    WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME,
-                    "SecondaryHomeHandle"
+                ignoreLayers = listOf(FlickerComponentName.SPLASH_SCREEN,
+                    FlickerComponentName.SNAPSHOT,
+                    FlickerComponentName("", "SecondaryHomeHandle")
                 )
             )
         }
     }
 
+    /**
+     * Checks that all windows that are visible on the trace, are visible for at least 2
+     * consecutive entries.
+     */
     @Presubmit
     @Test
     open fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
@@ -128,32 +123,47 @@
         }
     }
 
+    /**
+     * Checks that all parts of the screen are covered during the transition
+     */
     @Presubmit
     @Test
-    open fun noUncoveredRegions() {
-        testSpec.noUncoveredRegions(testSpec.config.startRotation,
-            testSpec.config.endRotation, allStates = false)
-    }
+    open fun entireScreenCovered() = testSpec.entireScreenCovered()
 
+    /**
+     * Checks that the focus doesn't change during animation
+     */
     @Presubmit
     @Test
     open fun focusDoesNotChange() {
-        testSpec.focusDoesNotChange()
+        testSpec.assertEventLog {
+            this.focusDoesNotChange()
+        }
     }
 
+    /**
+     * Checks that [testApp] layer covers the entire screen at the start of the transition
+     */
     @Presubmit
     @Test
     open fun appLayerRotates_StartingPos() {
         testSpec.assertLayersStart {
-            this.visibleRegion(testApp.getPackage()).coversExactly(startingPos)
+            this.entry.displays.map { display ->
+                this.visibleRegion(testApp.component).coversExactly(display.layerStackSpace)
+            }
         }
     }
 
+    /**
+     * Checks that [testApp] layer covers the entire screen at the end of the transition
+     */
     @Presubmit
     @Test
     open fun appLayerRotates_EndingPos() {
         testSpec.assertLayersEnd {
-            this.visibleRegion(testApp.getPackage()).coversExactly(endingPos)
+            this.entry.displays.map { display ->
+                this.visibleRegion(testApp.component).coversExactly(display.layerStackSpace)
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
index b153bec..310f04b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
@@ -16,8 +16,8 @@
 
 package com.android.server.wm.flicker.rotation
 
-import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
+import android.view.WindowManager
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
@@ -27,6 +27,7 @@
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.SeamlessRotationAppHelper
 import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -34,8 +35,41 @@
 import org.junit.runners.Parameterized
 
 /**
- * Cycle through supported app rotations using seamless rotations.
+ * Test opening an app and cycling through app rotations using seamless rotations
+ *
+ * Currently runs:
+ *      0 -> 90 degrees
+ *      0 -> 90 degrees (with starved UI thread)
+ *      90 -> 0 degrees
+ *      90 -> 0 degrees (with starved UI thread)
+ *
+ * Actions:
+ *     Launch an app in fullscreen and supporting seamless rotation (via intent)
+ *     Set initial device orientation
+ *     Start tracing
+ *     Change device orientation
+ *     Stop tracing
+ *
  * To run this test: `atest FlickerTests:SeamlessAppRotationTest`
+ *
+ * To run only the presubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Presubmit`
+ *
+ * To run only the postsubmit assertions add: `--
+ *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
+ *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Postsubmit`
+ *
+ * To run only the flaky assertions add: `--
+ *      --module-arg FlickerTests:include-annotation:androidx.test.filters.FlakyTest`
+ *
+ * Notes:
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [RotationTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
@@ -60,45 +94,97 @@
             }
         }
 
-    @FlakyTest(bugId = 140855415)
+    /**
+     * Checks that [testApp] window is always in full screen
+     */
+    @Presubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() {
-        super.statusBarWindowIsAlwaysVisible()
+    fun appWindowFullScreen() {
+        testSpec.assertWm {
+            this.invoke("isFullScreen") {
+                val appWindow = it.windowState(testApp.`package`)
+                val flags = appWindow.windowState?.attributes?.flags ?: 0
+                appWindow.verify("isFullScreen")
+                    .that(flags.and(WindowManager.LayoutParams.FLAG_FULLSCREEN))
+                    .isGreaterThan(0)
+            }
+        }
     }
 
-    @FlakyTest(bugId = 140855415)
+    /**
+     * Checks that [testApp] window is always with seamless rotation
+     */
+    @Presubmit
     @Test
-    override fun statusBarLayerIsAlwaysVisible() {
-        super.statusBarLayerIsAlwaysVisible()
+    fun appWindowSeamlessRotation() {
+        testSpec.assertWm {
+            this.invoke("isRotationSeamless") {
+                val appWindow = it.windowState(testApp.`package`)
+                val rotationAnimation = appWindow.windowState?.attributes?.rotationAnimation ?: 0
+                appWindow.verify("isRotationSeamless")
+                    .that(rotationAnimation
+                        .and(WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS))
+                    .isGreaterThan(0)
+            }
+        }
     }
 
+    /**
+     * Checks that [testApp] window is always visible
+     */
     @Presubmit
     @Test
     fun appLayerAlwaysVisible() {
         testSpec.assertLayers {
-            isVisible(testApp.`package`)
+            isVisible(testApp.component)
         }
     }
 
-    @FlakyTest(bugId = 185400889)
+    /**
+     * Checks that [testApp] layer covers the entire screen during the whole transition
+     */
+    @Presubmit
     @Test
     fun appLayerRotates() {
         testSpec.assertLayers {
-            this.coversExactly(startingPos, testApp.`package`)
-                .then()
-                .coversExactly(endingPos, testApp.`package`)
+            this.invoke("entireScreenCovered") { entry ->
+                entry.entry.displays.map { display ->
+                    entry.visibleRegion(testApp.component).coversExactly(display.layerStackSpace)
+                }
+            }
         }
     }
 
-    @Postsubmit
+    /**
+     * Checks that the [FlickerComponentName.STATUS_BAR] window is invisible during the whole
+     * transition
+     */
+    @Presubmit
     @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+    fun statusBarWindowIsAlwaysInvisible() {
+        testSpec.assertWm {
+            this.isAboveAppWindowInvisible(FlickerComponentName.STATUS_BAR)
+        }
     }
 
-    companion object {
-        private val testFactory = FlickerTestParameterFactory.getInstance()
+    /**
+     * Checks that the [FlickerComponentName.STATUS_BAR] layer is invisible during the whole
+     * transition
+     */
+    @Presubmit
+    @Test
+    fun statusBarLayerIsAlwaysInvisible() {
+        testSpec.assertLayers {
+            this.isInvisible(FlickerComponentName.STATUS_BAR)
+        }
+    }
 
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
+
+    companion object {
         private val Map<String, Any?>.starveUiThread
             get() = this.getOrDefault(ActivityOptions.EXTRA_STARVE_UI_THREAD, false) as Boolean
 
@@ -110,20 +196,34 @@
             return config
         }
 
+        /**
+         * Creates the test configurations for seamless rotation based on the default rotation
+         * tests from [FlickerTestParameterFactory.getConfigRotationTests], but adding an
+         * additional flag ([ActivityOptions.EXTRA_STARVE_UI_THREAD]) to indicate if the app
+         * should starve the UI thread of not
+         */
         @JvmStatic
         private fun getConfigurations(): List<FlickerTestParameter> {
-            return testFactory.getConfigRotationTests(repetitions = 2).flatMap {
-                val defaultRun = it.createConfig(starveUiThread = false)
-                val busyUiRun = it.createConfig(starveUiThread = true)
-                listOf(
-                    FlickerTestParameter(defaultRun),
-                    FlickerTestParameter(busyUiRun,
-                        name = "${FlickerTestParameter.defaultName(busyUiRun)}_BUSY_UI_THREAD"
+            return FlickerTestParameterFactory.getInstance()
+                .getConfigRotationTests(repetitions = 2)
+                .flatMap {
+                    val defaultRun = it.createConfig(starveUiThread = false)
+                    val busyUiRun = it.createConfig(starveUiThread = true)
+                    listOf(
+                        FlickerTestParameter(defaultRun),
+                        FlickerTestParameter(busyUiRun,
+                            name = "${FlickerTestParameter.defaultName(busyUiRun)}_BUSY_UI_THREAD"
+                        )
                     )
-                )
-            }
+                }
         }
 
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 1599ed4..cb37fc7 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -59,5 +59,36 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <activity android:name=".NonResizeableActivity"
+            android:resizeableActivity="false"
+            android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableActivity"
+            android:label="NonResizeableApp"
+            android:exported="true"
+            android:showOnLockScreen="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <activity android:name=".ButtonActivity"
+            android:taskAffinity="com.android.server.wm.flicker.testapp.ButtonActivity"
+            android:configChanges="orientation|screenSize"
+            android:label="ButtonActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <activity android:name=".LaunchNewTaskActivity"
+                  android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchNewTaskActivity"
+                  android:configChanges="orientation|screenSize"
+                  android:label="LaunchNewTaskActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_button.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_button.xml
new file mode 100644
index 0000000..fe7bced
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/holo_orange_light">
+    <Button
+        android:id="@+id/launch_second_activity"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Second activity" />
+</LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
index 4708cfd..c55e7c2 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -23,5 +23,6 @@
     <EditText android:id="@+id/plain_text_input"
               android:layout_height="wrap_content"
               android:layout_width="match_parent"
+	      android:imeOptions="flagNoExtractUi"
               android:inputType="text"/>
 </LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_non_resizeable.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_non_resizeable.xml
new file mode 100644
index 0000000..6d5a9dd
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_non_resizeable.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@android:color/holo_orange_light">
+
+  <TextView
+      android:id="@+id/NonResizeableTest"
+      android:layout_width="fill_parent"
+      android:layout_height="fill_parent"
+      android:gravity="center_vertical|center_horizontal"
+      android:text="NonResizeableActivity"
+      android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/task_button.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/task_button.xml
new file mode 100644
index 0000000..8f75d17
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/task_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/holo_orange_light">
+    <Button
+        android:id="@+id/launch_new_task"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="New task" />
+</LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 0ccc498..baf36ab 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -41,4 +41,19 @@
     public static final ComponentName SIMPLE_ACTIVITY_AUTO_FOCUS_COMPONENT_NAME =
             new ComponentName(FLICKER_APP_PACKAGE,
                     FLICKER_APP_PACKAGE + ".SimpleActivity");
+
+    public static final String NON_RESIZEABLE_ACTIVITY_LAUNCHER_NAME = "NonResizeableApp";
+    public static final ComponentName NON_RESIZEABLE_ACTIVITY_COMPONENT_NAME =
+            new ComponentName(FLICKER_APP_PACKAGE,
+                    FLICKER_APP_PACKAGE + ".NonResizeableActivity");
+
+    public static final String BUTTON_ACTIVITY_LAUNCHER_NAME = "ButtonApp";
+    public static final ComponentName BUTTON_ACTIVITY_COMPONENT_NAME =
+            new ComponentName(FLICKER_APP_PACKAGE,
+                    FLICKER_APP_PACKAGE + ".ButtonActivity");
+
+    public static final String LAUNCH_NEW_TASK_ACTIVITY_LAUNCHER_NAME = "LaunchNewTaskApp";
+    public static final ComponentName LAUNCH_NEW_TASK_ACTIVITY_COMPONENT_NAME =
+            new ComponentName(FLICKER_APP_PACKAGE,
+                    FLICKER_APP_PACKAGE + ".LaunchNewTaskActivity");
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ButtonActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ButtonActivity.java
new file mode 100644
index 0000000..b42ac2a
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ButtonActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.WindowManager;
+import android.widget.Button;
+
+public class ButtonActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        WindowManager.LayoutParams p = getWindow().getAttributes();
+        p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
+                .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+        getWindow().setAttributes(p);
+        setContentView(R.layout.activity_button);
+
+        Button button = findViewById(R.id.launch_second_activity);
+        button.setOnClickListener(v -> {
+            Intent intent = new Intent(ButtonActivity.this, SimpleActivity.class);
+            startActivity(intent);
+        });
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewTaskActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewTaskActivity.java
new file mode 100644
index 0000000..1809781
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewTaskActivity.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.WindowManager;
+import android.widget.Button;
+
+public class LaunchNewTaskActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        WindowManager.LayoutParams p = getWindow().getAttributes();
+        p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
+                .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+        getWindow().setAttributes(p);
+        setContentView(R.layout.task_button);
+
+        Button button = findViewById(R.id.launch_new_task);
+        button.setOnClickListener(v -> {
+            Intent intent = new Intent(LaunchNewTaskActivity.this, SimpleActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK);
+            startActivity(intent);
+        });
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableActivity.java
new file mode 100644
index 0000000..61019d8
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.os.Bundle;
+
+public class NonResizeableActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.activity_non_resizeable);
+
+        setShowWhenLocked(true);
+        setTurnScreenOn(true);
+        KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
+        if (keyguardManager != null) {
+            keyguardManager.requestDismissKeyguard(this, null);
+        }
+    }
+}
diff --git a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
index bcd6ed7..824f91e 100644
--- a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
+++ b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
@@ -45,6 +45,7 @@
             // Test specifications for FrameworksMockingCoreTests.
             "android.app.activity.ActivityThreadClientTest",
             "android.view.DisplayTest",
+            "android.window.ConfigurationHelperTest",
             // Test specifications for FrameworksCoreTests.
             "android.app.servertransaction.", // all tests under the package.
             "android.view.CutoutSpecificationTest",
@@ -59,10 +60,8 @@
             "android.view.RoundedCornersTest",
             "android.view.WindowMetricsTest",
             "android.view.PendingInsetsControllerTest",
-            "android.window.WindowContextTest",
-            "android.window.WindowMetricsHelperTest",
+            "android.window.", // all tests under the package.
             "android.app.activity.ActivityThreadTest",
-            "android.window.WindowContextControllerTest"
     };
 
     public FrameworksTestsFilter(Bundle testArgs) {
diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp
index bfb7ce4..81b14d4 100644
--- a/tools/aapt2/SdkConstants.cpp
+++ b/tools/aapt2/SdkConstants.cpp
@@ -27,7 +27,7 @@
 
 static ApiVersion sDevelopmentSdkLevel = 10000;
 static const auto sDevelopmentSdkCodeNames =
-    std::unordered_set<StringPiece>({"Q", "R", "S", "Tiramisu"});
+    std::unordered_set<StringPiece>({"Q", "R", "S", "Sv2", "Tiramisu"});
 
 static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = {
     {0x021c, 1},